Skip to content

Commit cb50324

Browse files
committed
feat(cli): add builds command
1 parent 82add14 commit cb50324

9 files changed

Lines changed: 536 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ jobs:
8989
run: pnpm exec -- turbo run e2e --filter=@argos-ci/core --filter=@argos-ci/cli
9090
env:
9191
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
92+
ARGOS_BUILD_NUMBER: ${{ vars.ARGOS_BUILD_NUMBER }}
9293
NODE_VERSION: ${{ matrix.node-version }}
9394
OS: ${{ matrix.os }}
9495

.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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* E2E tests for `argos builds` commands.
3+
* Requires ARGOS_TOKEN and ARGOS_BUILD_NUMBER env vars.
4+
* Optional: ARGOS_API_BASE_URL env var.
5+
*
6+
* Usage:
7+
* ARGOS_TOKEN=xxx ARGOS_BUILD_NUMBER=<buildNumber> node e2e/builds.js
8+
* ARGOS_TOKEN=xxx ARGOS_BUILD_NUMBER=<buildNumber> 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 { execFileSync } from "node:child_process";
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;
16+
const cliPath = "bin/argos-cli.js";
17+
18+
if (!token || !buildNumber) {
19+
console.error(
20+
"Usage: ARGOS_TOKEN=xxx ARGOS_BUILD_NUMBER=<number> [ARGOS_API_BASE_URL=<url>] node e2e/builds.js",
21+
);
22+
process.exit(1);
23+
}
24+
25+
function envWith(overrides = {}) {
26+
return { ...process.env, ...overrides };
27+
}
28+
29+
const baseEnv = apiBaseURL
30+
? envWith({ ARGOS_API_BASE_URL: apiBaseURL })
31+
: process.env;
32+
33+
function run(args, env = process.env) {
34+
return execFileSync("node", [cliPath, ...args], {
35+
encoding: "utf8",
36+
env,
37+
});
38+
}
39+
40+
function assert(condition, message) {
41+
if (!condition) {
42+
console.error(`FAIL: ${message}`);
43+
process.exit(1);
44+
}
45+
console.log(`PASS: ${message}`);
46+
}
47+
48+
// --- Authentication tests ---
49+
50+
// No token → error + exit 1
51+
try {
52+
run(["builds", "get", buildNumber], { ...baseEnv, ARGOS_TOKEN: "" });
53+
assert(false, "Missing token should exit with code 1");
54+
} catch (err) {
55+
assert(err.status === 1, "Exit code 1 when no token");
56+
assert(
57+
err.stderr.includes("No Argos token found"),
58+
"Error message includes 'No Argos token found'",
59+
);
60+
}
61+
62+
// ARGOS_TOKEN env var
63+
const getOutputEnv = run(["builds", "get", buildNumber], {
64+
...baseEnv,
65+
ARGOS_TOKEN: token,
66+
});
67+
assert(
68+
getOutputEnv.includes("Build number"),
69+
"ARGOS_TOKEN env var authenticates",
70+
);
71+
72+
// --token flag (takes precedence)
73+
const getOutputFlag = run(["builds", "get", buildNumber, "--token", token], {
74+
...baseEnv,
75+
ARGOS_TOKEN: "invalid",
76+
});
77+
assert(
78+
getOutputFlag.includes("Build number"),
79+
"--token flag takes precedence over env var",
80+
);
81+
82+
// --- builds get tests ---
83+
84+
// Formatted output
85+
assert(getOutputEnv.includes("Status:"), "builds get: formatted Status line");
86+
assert(getOutputEnv.includes("Branch:"), "builds get: formatted Branch line");
87+
assert(getOutputEnv.includes("Commit:"), "builds get: formatted Commit line");
88+
assert(getOutputEnv.includes("URL:"), "builds get: formatted URL line");
89+
assert(
90+
getOutputEnv.includes("Snapshots:"),
91+
"builds get: formatted Snapshots line",
92+
);
93+
94+
// --json flag
95+
const getJsonOutput = run(["builds", "get", buildNumber, "--json"], {
96+
...baseEnv,
97+
ARGOS_TOKEN: token,
98+
});
99+
const buildJson = JSON.parse(getJsonOutput);
100+
assert(buildJson.id !== undefined, "--json: has 'id' field");
101+
assert(buildJson.status !== undefined, "--json: has 'status' field");
102+
assert(
103+
buildJson.branch !== undefined || buildJson.branch === null,
104+
"--json: has 'branch' field",
105+
);
106+
assert(
107+
buildJson.commit !== undefined || buildJson.commit === null,
108+
"--json: has 'commit' field",
109+
);
110+
assert(buildJson.url !== undefined, "--json: has 'url' field");
111+
112+
// status reflects actual result
113+
assert(
114+
["failure", "success", "pending"].includes(buildJson.status),
115+
`--json: status is one of failure|success|pending (got: ${buildJson.status})`,
116+
);
117+
118+
// Unknown build number
119+
try {
120+
run(["builds", "get", "999999"], {
121+
...baseEnv,
122+
ARGOS_TOKEN: token,
123+
});
124+
assert(false, "Unknown build number should exit with code 1");
125+
} catch (err) {
126+
assert(err.status === 1, "Unknown build number: exit code 1");
127+
assert(
128+
err.stderr.includes("Error:"),
129+
"Unknown build number: human-readable error message",
130+
);
131+
}
132+
133+
// --- builds snapshots tests ---
134+
135+
// List all (no filter)
136+
const snapshotsAll = run(["builds", "snapshots", buildNumber], {
137+
...baseEnv,
138+
ARGOS_TOKEN: token,
139+
});
140+
assert(typeof snapshotsAll === "string", "builds snapshots: returns output");
141+
142+
// --status failure filter
143+
const snapshotsFailure = run(
144+
["builds", "snapshots", buildNumber, "--status", "failure"],
145+
{ ...baseEnv, ARGOS_TOKEN: token },
146+
);
147+
const failureLines = snapshotsFailure.trim().split("\n").filter(Boolean);
148+
assert(
149+
failureLines.every((line) => line.includes("[failure]")),
150+
"--status failure: all lines have [failure] status",
151+
);
152+
153+
// --json output
154+
const snapshotsJson = run(["builds", "snapshots", buildNumber, "--json"], {
155+
...baseEnv,
156+
ARGOS_TOKEN: token,
157+
});
158+
const snapshotItems = JSON.parse(snapshotsJson);
159+
assert(Array.isArray(snapshotItems), "--json: returns an array");
160+
if (snapshotItems.length > 0) {
161+
const first = snapshotItems[0];
162+
assert(first.name !== undefined, "--json: each item has 'name' field");
163+
assert(first.status !== undefined, "--json: each item has 'status' field");
164+
assert(first.url !== undefined, "--json: each item has 'url' field");
165+
}
166+
167+
// Integration test: --status failure --json → every item has "status": "failure"
168+
const snapshotsFailureJson = run(
169+
["builds", "snapshots", buildNumber, "--status", "failure", "--json"],
170+
{ ...baseEnv, ARGOS_TOKEN: token },
171+
);
172+
const failureItems = JSON.parse(snapshotsFailureJson);
173+
assert(
174+
Array.isArray(failureItems),
175+
"integration: --status failure --json returns array",
176+
);
177+
assert(
178+
failureItems.every((item) => item.status === "failure"),
179+
'integration: every item in --status failure --json has "status": "failure"',
180+
);
181+
182+
console.log("\nAll e2e tests passed!");

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)