Skip to content

Commit 9f53061

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

15 files changed

Lines changed: 1025 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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
const buildNumber = process.env.ARGOS_BUILD_NUMBER || "28022";
16+
17+
if (!token) {
18+
console.error(
19+
"Usage: ARGOS_TOKEN=xxx [ARGOS_API_BASE_URL=<url>] node e2e/builds.js",
20+
);
21+
process.exit(1);
22+
}
23+
24+
function envWith(overrides = {}) {
25+
return { ...process.env, ...overrides };
26+
}
27+
28+
const baseEnv = apiBaseURL
29+
? envWith({ ARGOS_API_BASE_URL: apiBaseURL })
30+
: process.env;
31+
32+
// --- builds get: authentication tests ---
33+
34+
console.log("\nTest failing build commands (builds get):");
35+
36+
try {
37+
run(["builds", "get", "1"], { ...baseEnv, ARGOS_TOKEN: "" });
38+
assert(false, "Missing token with build number should exit with code 1");
39+
} catch (err) {
40+
assert(err.status !== 0, "Exit code 1 when no token for build number");
41+
assert(
42+
err.stderr.includes("No Argos token found"),
43+
"Error message includes 'No Argos token found' for build number",
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 with build URL should exit with code 1");
57+
} catch (err) {
58+
assert(err.status !== 0, "Exit code 1 when no token for build URL");
59+
assert(
60+
err.stderr.includes("No Argos token found"),
61+
"Error message includes 'No Argos token found' for build URL",
62+
);
63+
}
64+
65+
try {
66+
run(["builds", "get", "999999"], {
67+
...baseEnv,
68+
ARGOS_TOKEN: token,
69+
});
70+
assert(false, "Unknown build number should exit with code 1");
71+
} catch (err) {
72+
assert(err.status !== 0, "Unknown build number: exit code 1");
73+
assert(
74+
err.stderr.includes("Error:"),
75+
"Unknown build number: human-readable error message",
76+
);
77+
}
78+
79+
try {
80+
run(["builds", "get", "not-a-number"], {
81+
...baseEnv,
82+
ARGOS_TOKEN: token,
83+
});
84+
assert(false, "Invalid build number should exit with code 1");
85+
} catch (err) {
86+
assert(err.status !== 0, "Invalid build number: exit code 1");
87+
assert(
88+
err.stderr.includes("valid build number or Argos build URL"),
89+
"Invalid build reference: human-readable error message",
90+
);
91+
}
92+
93+
// --- builds snapshots: authentication tests ---
94+
95+
console.log("\nTest failing build commands (builds snapshots):");
96+
97+
try {
98+
run(["builds", "snapshots", "1"], { ...baseEnv, ARGOS_TOKEN: "" });
99+
assert(
100+
false,
101+
"Missing token for snapshots with build number should exit with code 1",
102+
);
103+
} catch (err) {
104+
assert(
105+
err.status !== 0,
106+
"Exit code 1 when no token for snapshots with build number",
107+
);
108+
assert(
109+
err.stderr.includes("No Argos token found"),
110+
"Error message includes 'No Argos token found' for snapshots with build number",
111+
);
112+
}
113+
114+
// --- builds get/builds snapshots: successful flow ---
115+
116+
console.log("\nTest successful build commands:");
117+
const buildByNumberJsonOutput = run(["builds", "get", buildNumber, "--json"], {
118+
...baseEnv,
119+
ARGOS_TOKEN: token,
120+
});
121+
const buildByNumberJson = JSON.parse(buildByNumberJsonOutput.stdout);
122+
const buildUrl = buildByNumberJson.url;
123+
124+
const buildByNumberHumanOutput = run(["builds", "get", buildNumber], {
125+
...baseEnv,
126+
ARGOS_TOKEN: token,
127+
});
128+
assert(
129+
buildByNumberHumanOutput.stdout.includes(`Build #${buildNumber}`),
130+
"builds get prints the build number in human-readable mode",
131+
);
132+
assert(
133+
buildByNumberHumanOutput.stdout.includes("Snapshots:"),
134+
"builds get prints snapshot stats in human-readable mode",
135+
);
136+
assert(
137+
buildByNumberHumanOutput.stdout.includes(`URL: ${buildUrl}`),
138+
"builds get prints the build URL in human-readable mode",
139+
);
140+
assert(buildByNumberJson.id !== undefined, "builds get returns build id");
141+
assert(buildByNumberJson.url !== undefined, "builds get returns build url");
142+
assert(
143+
buildByNumberJson.number === Number(buildNumber),
144+
"builds get returns the requested build number",
145+
);
146+
147+
const buildByUrlJsonOutput = run(["builds", "get", "--json", buildUrl], {
148+
...baseEnv,
149+
ARGOS_TOKEN: token,
150+
});
151+
const buildByUrlJson = JSON.parse(buildByUrlJsonOutput.stdout);
152+
assert(
153+
buildByUrlJson.number === Number(buildNumber),
154+
"builds get accepts an Argos build URL",
155+
);
156+
157+
const snapshotsEmptyStateOutput = run(["builds", "snapshots", buildNumber], {
158+
...baseEnv,
159+
ARGOS_TOKEN: token,
160+
});
161+
assert(
162+
snapshotsEmptyStateOutput.stdout.includes("No snapshots found."),
163+
"builds snapshots prints an empty state when there are no snapshots",
164+
);
165+
166+
const snapshotsNeedsReviewJsonOutput = run(
167+
["builds", "snapshots", buildNumber, "--needs-review", "--json"],
168+
{
169+
...baseEnv,
170+
ARGOS_TOKEN: token,
171+
},
172+
);
173+
const snapshotsNeedingReview = JSON.parse(
174+
snapshotsNeedsReviewJsonOutput.stdout,
175+
);
176+
assert(
177+
Array.isArray(snapshotsNeedingReview),
178+
"builds snapshots returns an array in JSON mode",
179+
);
180+
assert(
181+
snapshotsNeedingReview.length === 0,
182+
"builds snapshots returns an empty array when there are no snapshots to review",
183+
);

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: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
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+
const buildUrlMatch = uploadResult.combined.match(
16+
/https?:\/\/\S+\/builds\/\d+/,
1417
);
18+
assert(buildUrlMatch, "upload returns a full build URL");

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)