@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.
Files changed (2) hide show
  1. package/dist/index.mjs +192 -0
  2. 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.3",
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/core": "5.1.3",
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": "b49d186d5eb6e1338982a4cd5c3cb29e6cb0b7af"
50
+ "gitHead": "3431e7d43acf6b1f043b55e53dec32318cabcd4c"
50
51
  }