@argos-ci/cli 4.1.3 → 4.1.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +192 -0
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { Option, program } from "commander";
|
|
5
5
|
import { finalize, skip, upload } from "@argos-ci/core";
|
|
6
6
|
import ora from "ora";
|
|
7
|
+
import { createClient, throwAPIError } from "@argos-ci/api-client";
|
|
7
8
|
//#region src/options.ts
|
|
8
9
|
const parallelNonceOption = new Option("--parallel-nonce <string>", "A unique ID for this parallel build").env("ARGOS_PARALLEL_NONCE");
|
|
9
10
|
const tokenOption = new Option("--token <token>", "Repository token").env("ARGOS_TOKEN");
|
|
@@ -91,6 +92,196 @@ function skipCommand(program) {
|
|
|
91
92
|
});
|
|
92
93
|
}
|
|
93
94
|
//#endregion
|
|
95
|
+
//#region src/commands/build.ts
|
|
96
|
+
function getToken(options) {
|
|
97
|
+
const token = options.token ?? process.env["ARGOS_TOKEN"];
|
|
98
|
+
if (!token) {
|
|
99
|
+
console.error("Error: No Argos token found. Use --token or set ARGOS_TOKEN.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
return token;
|
|
103
|
+
}
|
|
104
|
+
function getAPIBaseURL() {
|
|
105
|
+
return process.env["ARGOS_API_BASE_URL"];
|
|
106
|
+
}
|
|
107
|
+
function createBuildsClient(options) {
|
|
108
|
+
return createClient({
|
|
109
|
+
authToken: getToken(options),
|
|
110
|
+
baseUrl: getAPIBaseURL()
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function isBuildPending(build) {
|
|
114
|
+
return build.status === "pending" || build.status === "progress";
|
|
115
|
+
}
|
|
116
|
+
function parseBuildReferenceOrExit(buildReference) {
|
|
117
|
+
const parsedBuildNumber = Number(buildReference);
|
|
118
|
+
if (Number.isFinite(parsedBuildNumber) && Number.isInteger(parsedBuildNumber)) return parsedBuildNumber;
|
|
119
|
+
const urlMatch = buildReference.match(/^https:\/\/app\.argos-ci\.(?:com|dev(?::\d+)?)\/.+\/builds\/(\d+)(?:\/?$|[?#])/);
|
|
120
|
+
if (urlMatch) return Number(urlMatch[1]);
|
|
121
|
+
console.error(`Error: Build reference must be a valid build number or Argos build URL (https://app.argos-ci.com/.../builds/<number>), got "${buildReference}".`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
function formatValue(value) {
|
|
125
|
+
if (value === null || value === void 0 || value === "") return "-";
|
|
126
|
+
return String(value);
|
|
127
|
+
}
|
|
128
|
+
function formatStats(stats) {
|
|
129
|
+
if (!stats) return "-";
|
|
130
|
+
return [
|
|
131
|
+
`total ${stats.total}`,
|
|
132
|
+
`changed ${stats.changed}`,
|
|
133
|
+
`added ${stats.added}`,
|
|
134
|
+
`removed ${stats.removed}`,
|
|
135
|
+
`unchanged ${stats.unchanged}`
|
|
136
|
+
].join(", ");
|
|
137
|
+
}
|
|
138
|
+
function formatSnapshotSummary(diffs) {
|
|
139
|
+
const counts = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const diff of diffs) counts.set(diff.status, (counts.get(diff.status) ?? 0) + 1);
|
|
141
|
+
return [
|
|
142
|
+
"changed",
|
|
143
|
+
"added",
|
|
144
|
+
"removed",
|
|
145
|
+
"unchanged",
|
|
146
|
+
"ignored",
|
|
147
|
+
"pending",
|
|
148
|
+
"failure",
|
|
149
|
+
"retryFailure"
|
|
150
|
+
].map((status) => {
|
|
151
|
+
const count = counts.get(status);
|
|
152
|
+
return count ? `${status} ${count}` : null;
|
|
153
|
+
}).filter((part) => Boolean(part)).join(", ");
|
|
154
|
+
}
|
|
155
|
+
function printBuild(build) {
|
|
156
|
+
const lines = [
|
|
157
|
+
`Build #${build.number}`,
|
|
158
|
+
`Status: ${build.status}`,
|
|
159
|
+
`Snapshots: ${formatStats(build.stats)}`,
|
|
160
|
+
`Conclusion: ${formatValue(build.conclusion)}`,
|
|
161
|
+
`Branch: ${formatValue(build.head?.branch)}`,
|
|
162
|
+
`Commit: ${formatValue(build.head?.sha)}`,
|
|
163
|
+
`Base branch: ${formatValue(build.base?.branch)}`,
|
|
164
|
+
`Base commit: ${formatValue(build.base?.sha)}`,
|
|
165
|
+
`URL: ${build.url}`
|
|
166
|
+
];
|
|
167
|
+
console.log(lines.join("\n"));
|
|
168
|
+
}
|
|
169
|
+
function printSnapshots(diffs, build) {
|
|
170
|
+
if (diffs.length === 0) {
|
|
171
|
+
console.log("No snapshots found.");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const lines = [
|
|
175
|
+
`Snapshots for build #${build.number}`,
|
|
176
|
+
`Count: ${diffs.length}`,
|
|
177
|
+
`Summary: ${formatSnapshotSummary(diffs)}`,
|
|
178
|
+
"",
|
|
179
|
+
...diffs.flatMap((diff) => {
|
|
180
|
+
return [
|
|
181
|
+
`${diff.name} [${diff.status}]`,
|
|
182
|
+
` Review: ${build.url}/${diff.id}`,
|
|
183
|
+
` Mask: ${formatValue(diff.url)}`,
|
|
184
|
+
` Base file: ${formatValue(diff.base?.url)}`,
|
|
185
|
+
` Head file: ${formatValue(diff.head?.url)}`,
|
|
186
|
+
` Score: ${formatValue(diff.score)}`,
|
|
187
|
+
` Group: ${formatValue(diff.group)}`,
|
|
188
|
+
""
|
|
189
|
+
];
|
|
190
|
+
})
|
|
191
|
+
];
|
|
192
|
+
console.log(lines.slice(0, -1).join("\n"));
|
|
193
|
+
}
|
|
194
|
+
async function fetchAllDiffs(client, project, buildNumber, options) {
|
|
195
|
+
const results = [];
|
|
196
|
+
let page = 1;
|
|
197
|
+
const perPage = 100;
|
|
198
|
+
while (true) {
|
|
199
|
+
const query = {
|
|
200
|
+
page: String(page),
|
|
201
|
+
perPage: String(perPage),
|
|
202
|
+
...options?.needsReview ? { needsReview: true } : {}
|
|
203
|
+
};
|
|
204
|
+
const { data, error } = await client.GET("/projects/{owner}/{project}/builds/{buildNumber}/diffs", { params: {
|
|
205
|
+
path: {
|
|
206
|
+
owner: project.account.slug,
|
|
207
|
+
project: project.name,
|
|
208
|
+
buildNumber
|
|
209
|
+
},
|
|
210
|
+
query
|
|
211
|
+
} });
|
|
212
|
+
if (error || !data) {
|
|
213
|
+
if (error) throwAPIError(error);
|
|
214
|
+
throw new Error("Unexpected empty response from API.");
|
|
215
|
+
}
|
|
216
|
+
results.push(...data.results);
|
|
217
|
+
if (results.length >= data.pageInfo.total) break;
|
|
218
|
+
page++;
|
|
219
|
+
}
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
async function fetchProject(client) {
|
|
223
|
+
const { data, error } = await client.GET("/project");
|
|
224
|
+
if (error) throwAPIError(error);
|
|
225
|
+
if (!data) {
|
|
226
|
+
console.error("Error: Unexpected empty response from API.");
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
return data;
|
|
230
|
+
}
|
|
231
|
+
async function fetchBuildByNumber(client, project, buildNumber, errorLabel) {
|
|
232
|
+
const { data, error, response } = await client.GET("/projects/{owner}/{project}/builds/{buildNumber}", { params: { path: {
|
|
233
|
+
owner: project.account.slug,
|
|
234
|
+
project: project.name,
|
|
235
|
+
buildNumber
|
|
236
|
+
} } });
|
|
237
|
+
if (error) {
|
|
238
|
+
if (response.status === 404) {
|
|
239
|
+
console.error(`Error: Build number ${errorLabel} not found.`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
throwAPIError(error);
|
|
243
|
+
}
|
|
244
|
+
if (!data) {
|
|
245
|
+
console.error("Error: Unexpected empty response from API.");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
function buildCommand(program) {
|
|
251
|
+
const build = program.command("build").description("Manage Argos builds");
|
|
252
|
+
const createJsonOption = () => new Option("--json", "Output machine-readable JSON instead of human-readable text");
|
|
253
|
+
build.command("get").description("Fetch build metadata").argument("<buildReference>", "Build number or Argos build URL").addOption(tokenOption).addOption(createJsonOption()).action(async (buildReference, options) => {
|
|
254
|
+
const buildNumber = parseBuildReferenceOrExit(buildReference);
|
|
255
|
+
const client = createBuildsClient(options);
|
|
256
|
+
const build = await fetchBuildByNumber(client, await fetchProject(client), buildNumber, buildReference);
|
|
257
|
+
if (options.json) {
|
|
258
|
+
console.log(JSON.stringify(build, null, 2));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
printBuild(build);
|
|
262
|
+
});
|
|
263
|
+
build.command("snapshots").description("Fetch snapshot diffs for a build").argument("<buildReference>", "Build number or Argos build URL").option("--needs-review", "Only include snapshot diffs that require review").addOption(tokenOption).addOption(createJsonOption()).action(async (buildReference, options) => {
|
|
264
|
+
const buildNumber = parseBuildReferenceOrExit(buildReference);
|
|
265
|
+
const client = createBuildsClient(options);
|
|
266
|
+
const project = await fetchProject(client);
|
|
267
|
+
const build = await fetchBuildByNumber(client, project, buildNumber, buildReference);
|
|
268
|
+
if (isBuildPending(build)) {
|
|
269
|
+
if (options.json) {
|
|
270
|
+
console.error(`Error: Build #${build.number} is still processing (${build.status}). Try again in a moment.`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
console.log(`Build #${build.number} is still processing (${build.status}). Try again in a moment.`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const diffs = await fetchAllDiffs(client, project, build.number, { needsReview: Boolean(options.needsReview) });
|
|
277
|
+
if (options.json) {
|
|
278
|
+
console.log(JSON.stringify(diffs, null, 2));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
printSnapshots(diffs, build);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
94
285
|
//#region src/index.ts
|
|
95
286
|
const rawPkg = await readFile(resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json"), "utf8");
|
|
96
287
|
const pkg = JSON.parse(rawPkg);
|
|
@@ -98,6 +289,7 @@ program.name(pkg.name).description("Interact with and upload screenshots to Argo
|
|
|
98
289
|
uploadCommand(program);
|
|
99
290
|
skipCommand(program);
|
|
100
291
|
finalizeCommand(program);
|
|
292
|
+
buildCommand(program);
|
|
101
293
|
if (!process.argv.slice(2).length) program.outputHelp();
|
|
102
294
|
else program.parse(process.argv);
|
|
103
295
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@argos-ci/cli",
|
|
3
3
|
"description": "Command-line (CLI) for visual testing with Argos.",
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.5-alpha.2+3431e7d",
|
|
5
5
|
"bin": {
|
|
6
6
|
"argos": "./bin/argos-cli.js"
|
|
7
7
|
},
|
|
@@ -34,17 +34,18 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@argos-ci/
|
|
37
|
+
"@argos-ci/api-client": "0.17.1-alpha.2+3431e7d",
|
|
38
|
+
"@argos-ci/core": "5.2.1-alpha.2+3431e7d",
|
|
38
39
|
"commander": "^14.0.3",
|
|
39
40
|
"ora": "^9.3.0",
|
|
40
41
|
"update-notifier": "^7.3.1"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "tsdown",
|
|
44
|
-
"e2e": "node e2e/upload.js && node e2e/skip.js",
|
|
45
|
+
"e2e": "node e2e/upload.js && node e2e/skip.js && node e2e/build.js",
|
|
45
46
|
"check-types": "tsc",
|
|
46
47
|
"check-format": "prettier --check --ignore-unknown --ignore-path=../../.gitignore --ignore-path=../../.prettierignore .",
|
|
47
48
|
"lint": "eslint ."
|
|
48
49
|
},
|
|
49
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "3431e7d43acf6b1f043b55e53dec32318cabcd4c"
|
|
50
51
|
}
|