@argos-ci/cli 0.4.0 → 0.4.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 +144 -103
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -3,8 +3,10 @@ import { fileURLToPath } from 'node:url';
3
3
  import { resolve } from 'node:path';
4
4
  import { program } from 'commander';
5
5
  import convict from 'convict';
6
+ import { execSync } from 'node:child_process';
7
+ import { execSync as execSync$1 } from 'child_process';
8
+ import createDebug from 'debug';
6
9
  import envCi from 'env-ci';
7
- import { execSync } from 'child_process';
8
10
  import glob from 'fast-glob';
9
11
  import { promisify } from 'node:util';
10
12
  import sharp from 'sharp';
@@ -12,7 +14,6 @@ import tmp from 'tmp';
12
14
  import { createReadStream } from 'node:fs';
13
15
  import { createHash } from 'node:crypto';
14
16
  import axios from 'axios';
15
- import createDebug from 'debug';
16
17
  import ora from 'ora';
17
18
 
18
19
  const mustBeApiBaseUrl = (value)=>{
@@ -44,14 +45,13 @@ const schema = {
44
45
  },
45
46
  commit: {
46
47
  env: "ARGOS_COMMIT",
47
- default: "",
48
+ default: null,
48
49
  format: mustBeCommit
49
50
  },
50
51
  branch: {
51
52
  env: "ARGOS_BRANCH",
52
53
  default: null,
53
- format: String,
54
- nullable: true
54
+ format: String
55
55
  },
56
56
  token: {
57
57
  env: "ARGOS_TOKEN",
@@ -64,6 +64,12 @@ const schema = {
64
64
  format: String,
65
65
  nullable: true
66
66
  },
67
+ prNumber: {
68
+ env: "ARGOS_PR_NUMBER",
69
+ format: Number,
70
+ default: null,
71
+ nullable: true
72
+ },
67
73
  parallel: {
68
74
  env: "ARGOS_PARALLEL",
69
75
  default: false,
@@ -96,11 +102,6 @@ const schema = {
96
102
  default: null,
97
103
  nullable: true
98
104
  },
99
- prNumber: {
100
- format: Number,
101
- default: null,
102
- nullable: true
103
- },
104
105
  owner: {
105
106
  format: String,
106
107
  default: null,
@@ -119,45 +120,54 @@ const createConfig = ()=>{
119
120
  };
120
121
 
121
122
  /**
122
- * Omit undefined properties from an object.
123
- */ const omitUndefined = (obj)=>{
124
- const result = {};
125
- Object.keys(obj).forEach((key)=>{
126
- if (obj[key] !== undefined) {
127
- result[key] = obj[key];
123
+ * Returns the head commit.
124
+ */ const head = ()=>{
125
+ try {
126
+ return execSync("git rev-parse HEAD").toString().trim();
127
+ } catch {
128
+ return null;
129
+ }
130
+ };
131
+ /**
132
+ * Returns the current branch.
133
+ */ const branch = ()=>{
134
+ try {
135
+ const headRef = execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
136
+ if (headRef === "HEAD") {
137
+ return null;
128
138
  }
129
- });
130
- return result;
139
+ return headRef;
140
+ } catch {
141
+ return null;
142
+ }
131
143
  };
132
144
 
133
145
  const service$4 = {
146
+ name: "Buildkite",
134
147
  detect: ({ env })=>Boolean(env.BUILDKITE),
135
148
  config: ({ env })=>{
136
- const ciProps = envCiDetection({
137
- env
138
- });
139
149
  return {
140
- name: "Buildkite",
141
- commit: ciProps?.commit || null,
142
- branch: env.BUILDKITE_BRANCH || null,
150
+ // Buildkite doesn't work well so we fallback to git to ensure we have commit and branch
151
+ commit: env.BUILDKITE_COMMIT || head() || null,
152
+ branch: env.BUILDKITE_BRANCH || branch() || null,
143
153
  owner: env.BUILDKITE_ORGANIZATION_SLUG || null,
144
154
  repository: env.BUILDKITE_PROJECT_SLUG || null,
145
- jobId: env.BUILDKITE_JOB_ID || null,
146
- runId: ciProps?.runId || null,
155
+ jobId: null,
156
+ runId: null,
147
157
  prNumber: env.BUILDKITE_PULL_REQUEST ? Number(env.BUILDKITE_PULL_REQUEST) : null
148
158
  };
149
159
  }
150
160
  };
151
161
 
152
162
  const service$3 = {
163
+ name: "Heroku",
153
164
  detect: ({ env })=>Boolean(env.HEROKU_TEST_RUN_ID),
154
165
  config: ({ env })=>({
155
- name: "Heroku",
156
166
  commit: env.HEROKU_TEST_RUN_COMMIT_VERSION || null,
157
167
  branch: env.HEROKU_TEST_RUN_BRANCH || null,
158
168
  owner: null,
159
169
  repository: null,
160
- jobId: env.HEROKU_TEST_RUN_ID || null,
170
+ jobId: null,
161
171
  runId: null,
162
172
  prNumber: null
163
173
  })
@@ -167,7 +177,7 @@ const getSha = ({ env })=>{
167
177
  const isPr = env.GITHUB_EVENT_NAME === "pull_request" || env.GITHUB_EVENT_NAME === "pull_request_target";
168
178
  if (isPr) {
169
179
  const mergeCommitRegex = /^[a-z0-9]{40} [a-z0-9]{40}$/;
170
- const mergeCommitMessage = execSync("git show --no-patch --format=%P").toString().trim();
180
+ const mergeCommitMessage = execSync$1("git show --no-patch --format=%P").toString().trim();
171
181
  // console.log(
172
182
  // `Handling PR with parent hash(es) '${mergeCommitMessage}' of current commit.`
173
183
  // );
@@ -196,22 +206,22 @@ Please run "actions/checkout" with "fetch-depth: 2". Example:
196
206
  }
197
207
  return process.env.GITHUB_SHA ?? null;
198
208
  };
199
- function getBranch({ env }) {
209
+ const getBranch = ({ env })=>{
200
210
  if (env.GITHUB_HEAD_REF) {
201
211
  return env.GITHUB_HEAD_REF;
202
212
  }
203
213
  const branchRegex = /refs\/heads\/(.*)/;
204
- const branchMatches = branchRegex.exec(env.GITHUB_REF || "");
205
- if (branchMatches) {
206
- return branchMatches[1];
214
+ const matches = branchRegex.exec(env.GITHUB_REF || "");
215
+ if (matches) {
216
+ return matches[1];
207
217
  }
208
218
  return null;
209
- }
210
- function getRepository({ env }) {
219
+ };
220
+ const getRepository$1 = ({ env })=>{
211
221
  if (!env.GITHUB_REPOSITORY) return null;
212
222
  return env.GITHUB_REPOSITORY.split("/")[1];
213
- }
214
- const getPrNumber$1 = ({ env })=>{
223
+ };
224
+ const getPrNumber$2 = ({ env })=>{
215
225
  const branchRegex = /refs\/pull\/(\d+)/;
216
226
  const branchMatches = branchRegex.exec(env.GITHUB_REF || "");
217
227
  if (branchMatches) {
@@ -220,9 +230,9 @@ const getPrNumber$1 = ({ env })=>{
220
230
  return null;
221
231
  };
222
232
  const service$2 = {
233
+ name: "GitHub Actions",
223
234
  detect: ({ env })=>Boolean(env.GITHUB_ACTIONS),
224
235
  config: ({ env })=>({
225
- name: "GitHub Actions",
226
236
  commit: getSha({
227
237
  env
228
238
  }),
@@ -230,73 +240,75 @@ const service$2 = {
230
240
  env
231
241
  }),
232
242
  owner: env.GITHUB_REPOSITORY_OWNER || null,
233
- repository: getRepository({
243
+ repository: getRepository$1({
234
244
  env
235
245
  }),
236
246
  jobId: env.GITHUB_JOB || null,
237
247
  runId: env.GITHUB_RUN_ID || null,
238
- prNumber: getPrNumber$1({
248
+ prNumber: getPrNumber$2({
239
249
  env
240
250
  })
241
251
  })
242
252
  };
243
253
 
244
- const getPrNumber = ({ env })=>{
254
+ const getPrNumber$1 = ({ env })=>{
245
255
  const branchRegex = /pull\/(\d+)/;
246
- const branchMatches = branchRegex.exec(env.CIRCLE_PULL_REQUEST || "");
247
- if (branchMatches) {
248
- return Number(branchMatches[1]);
256
+ const matches = branchRegex.exec(env.CIRCLE_PULL_REQUEST || "");
257
+ if (matches) {
258
+ return Number(matches[1]);
249
259
  }
250
260
  return null;
251
261
  };
252
262
  const service$1 = {
263
+ name: "CircleCI",
253
264
  detect: ({ env })=>Boolean(env.CIRCLECI),
254
265
  config: ({ env })=>{
255
- const ciProps = envCiDetection({
256
- env
257
- });
258
266
  return {
259
- name: "CircleCI",
260
- commit: ciProps?.commit || null,
261
- branch: ciProps?.branch || null,
262
- owner: ciProps?.owner || null,
263
- repository: ciProps?.repository || null,
264
- jobId: ciProps?.jobId || null,
265
- runId: ciProps?.runId || null,
266
- prNumber: getPrNumber({
267
+ commit: env.CIRCLE_SHA1 || null,
268
+ branch: env.CIRCLE_BRANCH || null,
269
+ owner: env.CIRCLE_PROJECT_USERNAME || null,
270
+ repository: env.CIRCLE_PROJECT_REPONAME || null,
271
+ jobId: null,
272
+ runId: null,
273
+ prNumber: getPrNumber$1({
267
274
  env
268
275
  })
269
276
  };
270
277
  }
271
278
  };
272
279
 
280
+ const getOwner = ({ env })=>{
281
+ if (!env.TRAVIS_REPO_SLUG) return null;
282
+ return env.TRAVIS_REPO_SLUG.split("/")[0] || null;
283
+ };
284
+ const getRepository = ({ env })=>{
285
+ if (!env.TRAVIS_REPO_SLUG) return null;
286
+ return env.TRAVIS_REPO_SLUG.split("/")[1] || null;
287
+ };
288
+ const getPrNumber = ({ env })=>{
289
+ if (env.TRAVIS_PULL_REQUEST) return Number(env.TRAVIS_PULL_REQUEST);
290
+ return null;
291
+ };
273
292
  const service = {
293
+ name: "Travis CI",
274
294
  detect: ({ env })=>Boolean(env.TRAVIS),
275
- config: ({ env })=>{
276
- const ciProps = envCiDetection({
277
- env
278
- });
295
+ config: (ctx)=>{
296
+ const { env } = ctx;
279
297
  return {
280
- name: "Travis CI",
281
- commit: ciProps?.commit || null,
282
- branch: ciProps?.branch || null,
283
- owner: ciProps?.owner || null,
284
- repository: ciProps?.repository || null,
285
- jobId: ciProps?.jobId || null,
286
- runId: ciProps?.runId || null,
287
- prNumber: env.TRAVIS_PULL_REQUEST ? Number(env.TRAVIS_PULL_REQUEST) : null
298
+ commit: env.TRAVIS_COMMIT || null,
299
+ branch: env.TRAVIS_BRANCH || null,
300
+ owner: getOwner(ctx),
301
+ repository: getRepository(ctx),
302
+ jobId: null,
303
+ runId: null,
304
+ prNumber: getPrNumber(ctx)
288
305
  };
289
306
  }
290
307
  };
291
308
 
292
- const services = [
293
- service$3,
294
- service$2,
295
- service$1,
296
- service,
297
- service$4
298
- ];
299
- const envCiDetection = (ctx)=>{
309
+ const debug = createDebug("@argos-ci/core");
310
+
311
+ const getCiEnvironmentFromEnvCi = (ctx)=>{
300
312
  const ciContext = envCi(ctx);
301
313
  const name = ciContext.isCi ? ciContext.name ?? null : ciContext.commit ? "Git" : null;
302
314
  const commit = ciContext.commit ?? null;
@@ -318,16 +330,38 @@ const envCiDetection = (ctx)=>{
318
330
  prNumber
319
331
  } : null;
320
332
  };
333
+
334
+ const services = [
335
+ service$3,
336
+ service$2,
337
+ service$1,
338
+ service,
339
+ service$4
340
+ ];
321
341
  const getCiEnvironment = ({ env =process.env } = {})=>{
322
342
  const ctx = {
323
343
  env
324
344
  };
345
+ debug("Detecting CI environment", {
346
+ env
347
+ });
325
348
  const service = services.find((service)=>service.detect(ctx));
326
- // Internal service matched
349
+ // Service matched
327
350
  if (service) {
328
- return service.config(ctx);
351
+ debug("Internal service matched", service.name);
352
+ const variables = service.config(ctx);
353
+ const ciEnvironment = {
354
+ name: service.name,
355
+ ...variables
356
+ };
357
+ debug("CI environment", ciEnvironment);
358
+ return ciEnvironment;
329
359
  }
330
- return envCiDetection(ctx);
360
+ // We fallback on "env-ci" library, not very good but it's better than nothing
361
+ debug("Falling back on env-ci");
362
+ const ciEnvironment1 = getCiEnvironmentFromEnvCi(ctx);
363
+ debug("CI environment", ciEnvironment1);
364
+ return ciEnvironment1;
331
365
  };
332
366
 
333
367
  const discoverScreenshots = async (patterns, { root =process.cwd() , ignore } = {})=>{
@@ -398,11 +432,20 @@ const createArgosApiClient = (options)=>{
398
432
  });
399
433
  const call = async (method, path, data)=>{
400
434
  try {
435
+ debug("Sending request", {
436
+ method,
437
+ path,
438
+ data
439
+ });
401
440
  const response = await axiosInstance.request({
402
441
  method,
403
442
  url: path,
404
443
  data
405
444
  });
445
+ debug("Getting response", {
446
+ status: response.status,
447
+ data: response.data
448
+ });
406
449
  return response.data;
407
450
  } catch (error) {
408
451
  if (error?.response?.data?.error?.message) {
@@ -437,45 +480,42 @@ const upload$1 = async (input)=>{
437
480
  });
438
481
  };
439
482
 
440
- const debug = createDebug("@argos-ci/core");
441
-
442
483
  const getConfigFromOptions = (options)=>{
443
484
  const config = createConfig();
444
485
  const ciEnv = getCiEnvironment();
445
- if (ciEnv) {
446
- config.load(omitUndefined({
447
- commit: ciEnv.commit,
448
- branch: ciEnv.branch,
449
- ciService: ciEnv.name,
450
- owner: ciEnv.owner,
451
- repository: ciEnv.repository,
452
- jobId: ciEnv.jobId,
453
- runId: ciEnv.runId,
454
- prNumber: ciEnv.prNumber
455
- }));
486
+ config.load({
487
+ apiBaseUrl: config.get("apiBaseUrl") ?? options.apiBaseUrl,
488
+ commit: config.get("commit") ?? options.commit ?? ciEnv?.commit ?? null,
489
+ branch: config.get("branch") ?? options.branch ?? ciEnv?.branch ?? null,
490
+ token: config.get("token") ?? options.token ?? null,
491
+ buildName: config.get("buildName") ?? options.buildName ?? null,
492
+ prNumber: config.get("prNumber") ?? options.prNumber ?? ciEnv?.prNumber ?? null,
493
+ ciService: ciEnv?.name ?? null,
494
+ owner: ciEnv?.owner ?? null,
495
+ repository: ciEnv?.repository ?? null,
496
+ jobId: ciEnv?.jobId ?? null,
497
+ runId: ciEnv?.runId ?? null
498
+ });
499
+ if (options.parallel) {
500
+ config.load({
501
+ parallel: Boolean(options.parallel),
502
+ parallelNonce: options.parallel ? options.parallel.nonce : null,
503
+ parallelTotal: options.parallel ? options.parallel.total : null
504
+ });
456
505
  }
457
- config.load(omitUndefined({
458
- apiBaseUrl: options.apiBaseUrl,
459
- commit: options.commit,
460
- branch: options.branch,
461
- token: options.token,
462
- prNumber: options.prNumber,
463
- buildName: options.buildName,
464
- parallel: Boolean(options.parallel),
465
- parallelNonce: options.parallel ? options.parallel.nonce : null,
466
- parallelTotal: options.parallel ? options.parallel.total : null
467
- }));
468
506
  config.validate();
469
507
  return config.get();
470
508
  };
471
509
  /**
472
510
  * Upload screenshots to argos-ci.com.
473
511
  */ const upload = async (params)=>{
512
+ debug("Starting upload with params", params);
474
513
  // Read config
475
514
  const config = getConfigFromOptions(params);
476
515
  const files = params.files ?? [
477
516
  "**/*.{png,jpg,jpeg}"
478
517
  ];
518
+ debug("Using config and files", config, files);
479
519
  const apiClient = createArgosApiClient({
480
520
  baseUrl: config.apiBaseUrl,
481
521
  bearerToken: getBearerToken(config)
@@ -485,6 +525,7 @@ const getConfigFromOptions = (options)=>{
485
525
  root: params.root,
486
526
  ignore: params.ignore
487
527
  });
528
+ debug("Found screenshots", foundScreenshots);
488
529
  // Optimize & compute hashes
489
530
  const screenshots = await Promise.all(foundScreenshots.map(async (screenshot)=>{
490
531
  const optimizedPath = await optimizeScreenshot(screenshot.path);
@@ -540,7 +581,7 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
540
581
  const rawPkg = await readFile(resolve(__dirname, "..", "package.json"), "utf8");
541
582
  const pkg = JSON.parse(rawPkg);
542
583
  program.name(pkg.name).description("Interact with and upload screenshots to argos-ci.com via command line.").version(pkg.version);
543
- program.command("upload").argument("<directory>", "Directory to upload").description("Upload screenshots to argos-ci.com").option("-f, --files <patterns...>", "One or more globs matching image file paths to upload", "**/*.{png,jpg,jpeg}").option("-i, --ignore <patterns...>", 'One or more globs matching image file paths to ignore (ex: "**/*.png **/diff.jpg")').option("--token <token>", "Repository token").option("--pull-request <number>", "Pull-request number").option("--build-name <string>", "Name of the build, in case you want to run multiple Argos builds in a single CI job").option("--parallel", "Enable parallel mode. Run multiple Argos builds and combine them at the end").option("--parallel-total <number>", "The number of parallel nodes being ran").option("--parallel-nonce <string>", "A unique ID for this parallel build").action(async (directory, options)=>{
584
+ program.command("upload").argument("<directory>", "Directory to upload").description("Upload screenshots to argos-ci.com").option("-f, --files <patterns...>", "One or more globs matching image file paths to upload", "**/*.{png,jpg,jpeg}").option("-i, --ignore <patterns...>", 'One or more globs matching image file paths to ignore (ex: "**/*.png **/diff.jpg")').option("--token <token>", "Repository token").option("--build-name <string>", "Name of the build, in case you want to run multiple Argos builds in a single CI job").option("--parallel", "Enable parallel mode. Run multiple Argos builds and combine them at the end").option("--parallel-total <number>", "The number of parallel nodes being ran").option("--parallel-nonce <string>", "A unique ID for this parallel build").action(async (directory, options)=>{
544
585
  const spinner = ora("Uploading screenshots").start();
545
586
  try {
546
587
  const result = await upload({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@argos-ci/cli",
3
3
  "description": "Visual testing solution to avoid visual regression. Argos CLI is used to interact with and upload screenshots to argos-ci.com via command line.",
4
- "version": "0.4.0",
4
+ "version": "0.4.2",
5
5
  "bin": {
6
6
  "argos": "./bin/argos-cli.js"
7
7
  },
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@argos-ci/core": "^0.7.0",
43
+ "@argos-ci/core": "^0.7.2",
44
44
  "commander": "^9.4.1",
45
45
  "ora": "^6.1.2",
46
46
  "update-notifier": "^6.0.2"
@@ -49,5 +49,5 @@
49
49
  "rollup": "^2.79.1",
50
50
  "rollup-plugin-swc3": "^0.6.0"
51
51
  },
52
- "gitHead": "a7603beaf95ec4be8be9405450a445bfe94a6a12"
52
+ "gitHead": "7adf2cb62ceb13887c7ddf675f4d19e2bd2572b9"
53
53
  }