Skip to content

Commit a540409

Browse files
committed
feat(cli): add builds commands
1 parent 82add14 commit a540409

15 files changed

Lines changed: 1001 additions & 31 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ dist/
99
# Turborepo
1010
.turbo
1111
# NPM
12-
.npmrc
12+
.npmrc
13+
# macOS
14+
.DS_Store

packages/api-client/src/schema.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ export interface paths {
100100
patch?: never;
101101
trace?: never;
102102
};
103+
"/project/builds/{buildNumber}": {
104+
parameters: {
105+
query?: never;
106+
header?: never;
107+
path?: never;
108+
cookie?: never;
109+
};
110+
get: operations["getAuthBuildByNumber"];
111+
put?: never;
112+
post?: never;
113+
delete?: never;
114+
options?: never;
115+
head?: never;
116+
patch?: never;
117+
trace?: never;
118+
};
103119
}
104120
export type webhooks = Record<string, never>;
105121
export interface components {
@@ -1097,4 +1113,63 @@ export interface operations {
10971113
};
10981114
};
10991115
};
1116+
getAuthBuildByNumber: {
1117+
parameters: {
1118+
query?: never;
1119+
header?: never;
1120+
path: {
1121+
/** @description The build number */
1122+
buildNumber: number;
1123+
};
1124+
cookie?: never;
1125+
};
1126+
requestBody?: never;
1127+
responses: {
1128+
/** @description Build */
1129+
200: {
1130+
headers: {
1131+
[name: string]: unknown;
1132+
};
1133+
content: {
1134+
"application/json": components["schemas"]["Build"];
1135+
};
1136+
};
1137+
/** @description Invalid parameters */
1138+
400: {
1139+
headers: {
1140+
[name: string]: unknown;
1141+
};
1142+
content: {
1143+
"application/json": components["schemas"]["Error"];
1144+
};
1145+
};
1146+
/** @description Unauthorized */
1147+
401: {
1148+
headers: {
1149+
[name: string]: unknown;
1150+
};
1151+
content: {
1152+
"application/json": components["schemas"]["Error"];
1153+
};
1154+
};
1155+
/** @description Not found */
1156+
404: {
1157+
headers: {
1158+
[name: string]: unknown;
1159+
};
1160+
content: {
1161+
"application/json": components["schemas"]["Error"];
1162+
};
1163+
};
1164+
/** @description Server error */
1165+
500: {
1166+
headers: {
1167+
[name: string]: unknown;
1168+
};
1169+
content: {
1170+
"application/json": components["schemas"]["Error"];
1171+
};
1172+
};
1173+
};
1174+
}
11001175
}

packages/cli/e2e/builds.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* E2E tests for `argos builds` commands.
3+
* Requires ARGOS_TOKEN env var.
4+
* Optional: ARGOS_API_BASE_URL env var.
5+
*
6+
* Usage:
7+
* ARGOS_TOKEN=xxx node e2e/builds.js
8+
* ARGOS_TOKEN=xxx ARGOS_API_BASE_URL=https://api.argos-ci.dev:4001/v2 NODE_OPTIONS=--use-system-ca pnpm -C packages/cli exec node e2e/builds.js
9+
*/
10+
11+
import { assert, run } from "./utils.js";
12+
13+
const token = process.env.ARGOS_TOKEN;
14+
const apiBaseURL = process.env.ARGOS_API_BASE_URL;
15+
16+
if (!token) {
17+
console.error(
18+
"Usage: ARGOS_TOKEN=xxx [ARGOS_API_BASE_URL=<url>] node e2e/builds.js",
19+
);
20+
process.exit(1);
21+
}
22+
23+
function envWith(overrides = {}) {
24+
return { ...process.env, ...overrides };
25+
}
26+
27+
const baseEnv = apiBaseURL
28+
? envWith({ ARGOS_API_BASE_URL: apiBaseURL })
29+
: process.env;
30+
31+
// --- builds get: authentication tests ---
32+
33+
console.log("\nTest failing build commands (builds get):");
34+
35+
// No token → error + exit 1
36+
try {
37+
run(["builds", "get", "1"], { ...baseEnv, ARGOS_TOKEN: "" });
38+
assert(false, "Missing token should exit with code 1");
39+
} catch (err) {
40+
assert(err.status !== 0, "Exit code 1 when no token");
41+
assert(
42+
err.stderr.includes("No Argos token found"),
43+
"Error message includes 'No Argos token found'",
44+
);
45+
}
46+
47+
try {
48+
run(
49+
[
50+
"builds",
51+
"get",
52+
"https://app.argos-ci.com/argos-ci/argos-javascript/builds/1",
53+
],
54+
{ ...baseEnv, ARGOS_TOKEN: "" },
55+
);
56+
assert(false, "Missing token should exit with code 1");
57+
} catch (err) {
58+
assert(err.status !== 0, "Exit code 1 when no token");
59+
assert(
60+
err.stderr.includes("No Argos token found"),
61+
"Error message includes 'No Argos token found'",
62+
);
63+
}
64+
65+
// Unknown build number
66+
try {
67+
run(["builds", "get", "999999"], {
68+
...baseEnv,
69+
ARGOS_TOKEN: token,
70+
});
71+
assert(false, "Unknown build number should exit with code 1");
72+
} catch (err) {
73+
assert(err.status !== 0, "Unknown build number: exit code 1");
74+
assert(
75+
err.stderr.includes("Error:"),
76+
"Unknown build number: human-readable error message",
77+
);
78+
}
79+
80+
// Invalid build number format
81+
try {
82+
run(["builds", "get", "not-a-number"], {
83+
...baseEnv,
84+
ARGOS_TOKEN: token,
85+
});
86+
assert(false, "Invalid build number should exit with code 1");
87+
} catch (err) {
88+
assert(err.status !== 0, "Invalid build number: exit code 1");
89+
assert(
90+
err.stderr.includes("valid build number or Argos build URL"),
91+
"Invalid build reference: human-readable error message",
92+
);
93+
}
94+
95+
// --- builds snapshots: authentication tests ---
96+
97+
console.log("\nTest failing build commands (builds snapshots):");
98+
99+
// No token → error + exit 1
100+
try {
101+
run(["builds", "snapshots", "1"], { ...baseEnv, ARGOS_TOKEN: "" });
102+
assert(false, "Missing token should exit with code 1");
103+
} catch (err) {
104+
assert(err.status !== 0, "Exit code 1 when no token");
105+
assert(
106+
err.stderr.includes("No Argos token found"),
107+
"Error message includes 'No Argos token found'",
108+
);
109+
}

packages/cli/e2e/skip.js

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import { exec } from "node:child_process";
2-
3-
exec(
4-
`node bin/argos-cli.js skip --build-name "argos-cli-e2e-skipped-node-${process.env.NODE_VERSION}-${process.env.OS}"`,
5-
(err, stdout, stderr) => {
6-
if (err) {
7-
console.error(err);
8-
process.exit(1);
9-
}
10-
11-
console.log(stdout);
12-
console.error(stderr);
13-
},
14-
);
1+
import { assert, run } from "./utils.js";
2+
3+
const buildName = `argos-cli-e2e-skipped-node-${process.env.NODE_VERSION}-${process.env.OS}`;
4+
5+
const skipResult = run(["skip", "--build-name", buildName]);
6+
7+
console.log(skipResult.stdout);
8+
console.error(skipResult.stderr);
9+
10+
const buildNumberMatch = skipResult.combined.match(/\/builds\/(\d+)/);
11+
assert(buildNumberMatch, "skip returns a build URL");

packages/cli/e2e/upload.js

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,87 @@
1-
import { exec } from "node:child_process";
2-
3-
exec(
4-
`node bin/argos-cli.js upload ../../__fixtures__ --build-name "argos-cli-e2e-node-${process.env.NODE_VERSION}-${process.env.OS}"`,
5-
(err, stdout, stderr) => {
6-
if (err) {
7-
console.error(err);
8-
process.exit(1);
9-
}
10-
11-
console.log(stdout);
12-
console.error(stderr);
13-
},
1+
import { assert, run } from "./utils.js";
2+
3+
const buildName = `argos-cli-e2e-node-${process.env.NODE_VERSION}-${process.env.OS}`;
4+
5+
const uploadResult = run([
6+
"upload",
7+
"../../__fixtures__",
8+
"--build-name",
9+
buildName,
10+
]);
11+
12+
console.log(uploadResult.stdout);
13+
console.error(uploadResult.stderr);
14+
15+
console.log("- Fetch a build:");
16+
17+
const buildUrlMatch = uploadResult.combined.match(
18+
/https?:\/\/\S+\/builds\/\d+/,
19+
);
20+
assert(buildUrlMatch, "upload returns a full build URL");
21+
22+
const buildNumberMatch = uploadResult.combined.match(/\/builds\/(\d+)/);
23+
assert(buildNumberMatch, "upload returns a build URL");
24+
25+
const buildUrl = buildUrlMatch[0];
26+
const buildNumber = buildNumberMatch[1];
27+
28+
const getHumanOutput = run(["builds", "get", buildNumber]);
29+
assert(
30+
getHumanOutput.stdout.includes(`Build #${buildNumber}`),
31+
"builds get prints the build number in human-readable mode",
32+
);
33+
assert(
34+
getHumanOutput.stdout.includes("Snapshots:"),
35+
"builds get prints snapshot stats in human-readable mode",
36+
);
37+
assert(
38+
getHumanOutput.stdout.includes(`URL: ${buildUrl}`),
39+
"builds get prints the build URL in human-readable mode",
40+
);
41+
42+
const getOutput = run(["builds", "get", buildNumber, "--json"]);
43+
const buildJson = JSON.parse(getOutput.stdout);
44+
assert(buildJson.id !== undefined, "builds get returns build id");
45+
assert(buildJson.url !== undefined, "builds get returns build url");
46+
assert(
47+
buildJson.number === Number(buildNumber),
48+
"builds get returns the requested build number",
49+
);
50+
51+
const getUrlJsonOutput = run(["builds", "get", "--json", buildUrl]);
52+
const buildUrlJson = JSON.parse(getUrlJsonOutput.stdout);
53+
assert(
54+
buildUrlJson.number === Number(buildNumber),
55+
"builds get accepts an Argos build URL",
56+
);
57+
58+
const snapshotsHumanOutput = run([
59+
"builds",
60+
"snapshots",
61+
buildNumber,
62+
"--needs-review",
63+
]);
64+
65+
console.log(snapshotsHumanOutput.stdout);
66+
67+
assert(
68+
snapshotsHumanOutput.stdout.includes(`Snapshots for build #${buildNumber}`),
69+
"builds snapshots prints the build heading in human-readable mode",
70+
);
71+
assert(
72+
snapshotsHumanOutput.stdout.includes("Summary:"),
73+
"builds snapshots prints a summary in human-readable mode",
74+
);
75+
76+
const snapshotsJsonOutput = run([
77+
"builds",
78+
"snapshots",
79+
buildNumber,
80+
"--needs-review",
81+
"--json",
82+
]);
83+
const snapshotsJson = JSON.parse(snapshotsJsonOutput.stdout);
84+
assert(
85+
Array.isArray(snapshotsJson),
86+
"builds snapshots returns an array in JSON mode",
1487
);

packages/cli/e2e/utils.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { spawnSync } from "node:child_process";
2+
3+
const cliPath = "bin/argos-cli.js";
4+
5+
export function run(args, env = process.env) {
6+
const result = spawnSync("node", [cliPath, ...args], {
7+
encoding: "utf8",
8+
env,
9+
});
10+
11+
const stdout = result.stdout ?? "";
12+
const stderr = result.stderr ?? "";
13+
14+
if (result.status !== 0) {
15+
const error = new Error(
16+
`Command failed: node ${cliPath} ${args.join(" ")}`,
17+
);
18+
error.status = result.status;
19+
error.stdout = stdout;
20+
error.stderr = stderr;
21+
throw error;
22+
}
23+
24+
return {
25+
stdout,
26+
stderr,
27+
combined: `${stdout}${stderr}`,
28+
};
29+
}
30+
31+
export function assert(condition, message) {
32+
if (!condition) {
33+
console.error(`✘ FAIL: ${message}`);
34+
process.exit(1);
35+
}
36+
console.log(`✔ PASS: ${message}`);
37+
}

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
"access": "public"
3535
},
3636
"dependencies": {
37+
"@argos-ci/api-client": "workspace:*",
3738
"@argos-ci/core": "workspace:*",
3839
"commander": "^14.0.3",
3940
"ora": "^9.3.0",
4041
"update-notifier": "^7.3.1"
4142
},
4243
"scripts": {
4344
"build": "tsdown",
44-
"e2e": "node e2e/upload.js && node e2e/skip.js",
45+
"e2e": "node e2e/upload.js && node e2e/skip.js && node e2e/builds.js",
4546
"check-types": "tsc",
4647
"check-format": "prettier --check --ignore-unknown --ignore-path=../../.gitignore --ignore-path=../../.prettierignore .",
4748
"lint": "eslint ."

0 commit comments

Comments
 (0)