@argos-ci/cli 0.4.3 → 0.4.5

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 +1 -537
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -2,545 +2,9 @@ import { readFile } from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { resolve } from 'node:path';
4
4
  import { program } from 'commander';
5
- import convict from 'convict';
6
- import { execSync } from 'node:child_process';
7
- import { existsSync, readFileSync, createReadStream } from 'node:fs';
8
- import createDebug from 'debug';
9
- import envCi from 'env-ci';
10
- import glob from 'fast-glob';
11
- import { promisify } from 'node:util';
12
- import sharp from 'sharp';
13
- import tmp from 'tmp';
14
- import { createHash } from 'node:crypto';
15
- import axios from 'axios';
5
+ import { upload } from '@argos-ci/core';
16
6
  import ora from 'ora';
17
7
 
18
- const mustBeApiBaseUrl = (value)=>{
19
- const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
20
- if (!URL_REGEX.test(value)) {
21
- throw new Error("Invalid Argos API base URL");
22
- }
23
- };
24
- const mustBeCommit = (value)=>{
25
- const SHA1_REGEX = /^[0-9a-f]{40}$/;
26
- if (!SHA1_REGEX.test(value)) {
27
- const SHA1_SHORT_REGEX = /^[0-9a-f]{7}$/;
28
- if (SHA1_SHORT_REGEX.test(value)) {
29
- throw new Error("Short SHA1 is not allowed");
30
- }
31
- throw new Error("Invalid commit");
32
- }
33
- };
34
- const mustBeArgosToken = (value)=>{
35
- if (value && value.length !== 40) {
36
- throw new Error("Must be a valid Argos repository token");
37
- }
38
- };
39
- const schema = {
40
- apiBaseUrl: {
41
- env: "ARGOS_API_BASE_URL",
42
- default: "https://api.argos-ci.com/v2/",
43
- format: mustBeApiBaseUrl
44
- },
45
- commit: {
46
- env: "ARGOS_COMMIT",
47
- default: null,
48
- format: mustBeCommit
49
- },
50
- branch: {
51
- env: "ARGOS_BRANCH",
52
- default: null,
53
- format: String
54
- },
55
- token: {
56
- env: "ARGOS_TOKEN",
57
- default: null,
58
- format: mustBeArgosToken
59
- },
60
- buildName: {
61
- env: "ARGOS_BUILD_NAME",
62
- default: null,
63
- format: String,
64
- nullable: true
65
- },
66
- prNumber: {
67
- env: "ARGOS_PR_NUMBER",
68
- format: Number,
69
- default: null,
70
- nullable: true
71
- },
72
- parallel: {
73
- env: "ARGOS_PARALLEL",
74
- default: false,
75
- format: Boolean
76
- },
77
- parallelNonce: {
78
- env: "ARGOS_PARALLEL_NONCE",
79
- format: String,
80
- default: null,
81
- nullable: true
82
- },
83
- parallelTotal: {
84
- env: "ARGOS_PARALLEL_TOTAL",
85
- format: "nat",
86
- default: null,
87
- nullable: true
88
- },
89
- ciService: {
90
- format: String,
91
- default: null,
92
- nullable: true
93
- },
94
- jobId: {
95
- format: String,
96
- default: null,
97
- nullable: true
98
- },
99
- runId: {
100
- format: String,
101
- default: null,
102
- nullable: true
103
- },
104
- owner: {
105
- format: String,
106
- default: null,
107
- nullable: true
108
- },
109
- repository: {
110
- format: String,
111
- default: null,
112
- nullable: true
113
- }
114
- };
115
- const createConfig = ()=>{
116
- return convict(schema, {
117
- args: []
118
- });
119
- };
120
-
121
- /**
122
- * Returns the head commit.
123
- */ const head = ()=>{
124
- try {
125
- return execSync("git rev-parse HEAD").toString().trim();
126
- } catch {
127
- return null;
128
- }
129
- };
130
- /**
131
- * Returns the current branch.
132
- */ const branch = ()=>{
133
- try {
134
- const headRef = execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
135
- if (headRef === "HEAD") {
136
- return null;
137
- }
138
- return headRef;
139
- } catch {
140
- return null;
141
- }
142
- };
143
-
144
- const service$4 = {
145
- name: "Buildkite",
146
- detect: ({ env })=>Boolean(env.BUILDKITE),
147
- config: ({ env })=>{
148
- return {
149
- // Buildkite doesn't work well so we fallback to git to ensure we have commit and branch
150
- commit: env.BUILDKITE_COMMIT || head() || null,
151
- branch: env.BUILDKITE_BRANCH || branch() || null,
152
- owner: env.BUILDKITE_ORGANIZATION_SLUG || null,
153
- repository: env.BUILDKITE_PROJECT_SLUG || null,
154
- jobId: null,
155
- runId: null,
156
- prNumber: env.BUILDKITE_PULL_REQUEST ? Number(env.BUILDKITE_PULL_REQUEST) : null
157
- };
158
- }
159
- };
160
-
161
- const service$3 = {
162
- name: "Heroku",
163
- detect: ({ env })=>Boolean(env.HEROKU_TEST_RUN_ID),
164
- config: ({ env })=>({
165
- commit: env.HEROKU_TEST_RUN_COMMIT_VERSION || null,
166
- branch: env.HEROKU_TEST_RUN_BRANCH || null,
167
- owner: null,
168
- repository: null,
169
- jobId: null,
170
- runId: null,
171
- prNumber: null
172
- })
173
- };
174
-
175
- const getBranch = ({ env })=>{
176
- if (env.GITHUB_HEAD_REF) {
177
- return env.GITHUB_HEAD_REF;
178
- }
179
- const branchRegex = /refs\/heads\/(.*)/;
180
- const matches = branchRegex.exec(env.GITHUB_REF || "");
181
- if (matches) {
182
- return matches[1];
183
- }
184
- return null;
185
- };
186
- const getRepository$1 = ({ env })=>{
187
- if (!env.GITHUB_REPOSITORY) return null;
188
- return env.GITHUB_REPOSITORY.split("/")[1];
189
- };
190
- const readEventPayload = ({ env })=>{
191
- if (!env.GITHUB_EVENT_PATH) return null;
192
- if (!existsSync(env.GITHUB_EVENT_PATH)) return null;
193
- return JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf-8"));
194
- };
195
- const service$2 = {
196
- name: "GitHub Actions",
197
- detect: ({ env })=>Boolean(env.GITHUB_ACTIONS),
198
- config: ({ env })=>{
199
- const payload = readEventPayload({
200
- env
201
- });
202
- return {
203
- commit: payload?.pull_request?.head.sha || process.env.GITHUB_SHA || null,
204
- branch: payload?.pull_request?.head.ref || getBranch({
205
- env
206
- }) || null,
207
- owner: env.GITHUB_REPOSITORY_OWNER || null,
208
- repository: getRepository$1({
209
- env
210
- }),
211
- jobId: env.GITHUB_JOB || null,
212
- runId: env.GITHUB_RUN_ID || null,
213
- prNumber: payload?.pull_request?.number || null
214
- };
215
- }
216
- };
217
-
218
- const getPrNumber$1 = ({ env })=>{
219
- const branchRegex = /pull\/(\d+)/;
220
- const matches = branchRegex.exec(env.CIRCLE_PULL_REQUEST || "");
221
- if (matches) {
222
- return Number(matches[1]);
223
- }
224
- return null;
225
- };
226
- const service$1 = {
227
- name: "CircleCI",
228
- detect: ({ env })=>Boolean(env.CIRCLECI),
229
- config: ({ env })=>{
230
- return {
231
- commit: env.CIRCLE_SHA1 || null,
232
- branch: env.CIRCLE_BRANCH || null,
233
- owner: env.CIRCLE_PROJECT_USERNAME || null,
234
- repository: env.CIRCLE_PROJECT_REPONAME || null,
235
- jobId: null,
236
- runId: null,
237
- prNumber: getPrNumber$1({
238
- env
239
- })
240
- };
241
- }
242
- };
243
-
244
- const getOwner = ({ env })=>{
245
- if (!env.TRAVIS_REPO_SLUG) return null;
246
- return env.TRAVIS_REPO_SLUG.split("/")[0] || null;
247
- };
248
- const getRepository = ({ env })=>{
249
- if (!env.TRAVIS_REPO_SLUG) return null;
250
- return env.TRAVIS_REPO_SLUG.split("/")[1] || null;
251
- };
252
- const getPrNumber = ({ env })=>{
253
- if (env.TRAVIS_PULL_REQUEST) return Number(env.TRAVIS_PULL_REQUEST);
254
- return null;
255
- };
256
- const service = {
257
- name: "Travis CI",
258
- detect: ({ env })=>Boolean(env.TRAVIS),
259
- config: (ctx)=>{
260
- const { env } = ctx;
261
- return {
262
- commit: env.TRAVIS_COMMIT || null,
263
- branch: env.TRAVIS_BRANCH || null,
264
- owner: getOwner(ctx),
265
- repository: getRepository(ctx),
266
- jobId: null,
267
- runId: null,
268
- prNumber: getPrNumber(ctx)
269
- };
270
- }
271
- };
272
-
273
- const debug = createDebug("@argos-ci/core");
274
-
275
- const getCiEnvironmentFromEnvCi = (ctx)=>{
276
- const ciContext = envCi(ctx);
277
- const name = ciContext.isCi ? ciContext.name ?? null : ciContext.commit ? "Git" : null;
278
- const commit = ciContext.commit ?? null;
279
- const branch = (ciContext.branch || ciContext.prBranch) ?? null;
280
- const slug = ciContext.slug ? ciContext.slug.split("/") : null;
281
- const owner = slug ? slug[0] : null;
282
- const repository = slug ? slug[1] : null;
283
- const jobId = ciContext.job ?? null;
284
- const runId = null;
285
- const prNumber = null;
286
- return commit ? {
287
- name,
288
- commit,
289
- branch,
290
- owner,
291
- repository,
292
- jobId,
293
- runId,
294
- prNumber
295
- } : null;
296
- };
297
-
298
- const services = [
299
- service$3,
300
- service$2,
301
- service$1,
302
- service,
303
- service$4
304
- ];
305
- const getCiEnvironment = ({ env =process.env } = {})=>{
306
- const ctx = {
307
- env
308
- };
309
- debug("Detecting CI environment", {
310
- env
311
- });
312
- const service = services.find((service)=>service.detect(ctx));
313
- // Service matched
314
- if (service) {
315
- debug("Internal service matched", service.name);
316
- const variables = service.config(ctx);
317
- const ciEnvironment = {
318
- name: service.name,
319
- ...variables
320
- };
321
- debug("CI environment", ciEnvironment);
322
- return ciEnvironment;
323
- }
324
- // We fallback on "env-ci" library, not very good but it's better than nothing
325
- debug("Falling back on env-ci");
326
- const ciEnvironment1 = getCiEnvironmentFromEnvCi(ctx);
327
- debug("CI environment", ciEnvironment1);
328
- return ciEnvironment1;
329
- };
330
-
331
- const discoverScreenshots = async (patterns, { root =process.cwd() , ignore } = {})=>{
332
- const matches = await glob(patterns, {
333
- onlyFiles: true,
334
- ignore,
335
- cwd: root
336
- });
337
- return matches.map((match)=>({
338
- name: match,
339
- path: resolve(root, match)
340
- }));
341
- };
342
-
343
- const tmpFile = promisify(tmp.file);
344
- const optimizeScreenshot = async (filepath)=>{
345
- const resultFilePath = await tmpFile();
346
- await sharp(filepath).resize(2048, 20480, {
347
- fit: "inside",
348
- withoutEnlargement: true
349
- }).png({
350
- force: true
351
- }).toFile(resultFilePath);
352
- return resultFilePath;
353
- };
354
-
355
- const hashFile = async (filepath)=>{
356
- const fileStream = createReadStream(filepath);
357
- const hash = createHash("sha256");
358
- await new Promise((resolve, reject)=>{
359
- fileStream.on("error", reject);
360
- hash.on("error", reject);
361
- hash.on("finish", resolve);
362
- fileStream.pipe(hash);
363
- });
364
- return hash.digest("hex");
365
- };
366
-
367
- const base64Encode = (obj)=>Buffer.from(JSON.stringify(obj), "utf8").toString("base64");
368
- const getBearerToken = ({ token , ciService , owner , repository , jobId , runId , prNumber })=>{
369
- if (token) return `Bearer ${token}`;
370
- switch(ciService){
371
- case "GitHub Actions":
372
- {
373
- if (!owner || !repository || !jobId || !runId) {
374
- throw new Error(`Automatic ${ciService} variables detection failed. Please add the 'ARGOS_TOKEN'`);
375
- }
376
- return `Bearer tokenless-github-${base64Encode({
377
- owner,
378
- repository,
379
- jobId,
380
- runId,
381
- prNumber
382
- })}`;
383
- }
384
- default:
385
- throw new Error("Missing Argos repository token 'ARGOS_TOKEN'");
386
- }
387
- };
388
- const createArgosApiClient = (options)=>{
389
- const axiosInstance = axios.create({
390
- baseURL: options.baseUrl,
391
- headers: {
392
- Authorization: options.bearerToken,
393
- "Content-Type": "application/json",
394
- Accept: "application/json"
395
- }
396
- });
397
- const call = async (method, path, data)=>{
398
- try {
399
- debug("Sending request", {
400
- method,
401
- path,
402
- data
403
- });
404
- const response = await axiosInstance.request({
405
- method,
406
- url: path,
407
- data
408
- });
409
- debug("Getting response", {
410
- status: response.status,
411
- data: response.data
412
- });
413
- return response.data;
414
- } catch (error) {
415
- if (error?.response?.data?.error?.message) {
416
- // @ts-ignore
417
- throw new Error(error.response.data.error.message, {
418
- cause: error
419
- });
420
- }
421
- throw error;
422
- }
423
- };
424
- return {
425
- createBuild: async (input)=>{
426
- return call("POST", "/builds", input);
427
- },
428
- updateBuild: async (input)=>{
429
- const { buildId , ...body } = input;
430
- return call("PUT", `/builds/${buildId}`, body);
431
- }
432
- };
433
- };
434
-
435
- const upload$1 = async (input)=>{
436
- const file = await readFile(input.path);
437
- await axios({
438
- method: "PUT",
439
- url: input.url,
440
- data: file,
441
- headers: {
442
- "Content-Type": "image/png"
443
- }
444
- });
445
- };
446
-
447
- const getConfigFromOptions = (options)=>{
448
- const config = createConfig();
449
- const ciEnv = getCiEnvironment();
450
- config.load({
451
- apiBaseUrl: config.get("apiBaseUrl") ?? options.apiBaseUrl,
452
- commit: config.get("commit") ?? options.commit ?? ciEnv?.commit ?? null,
453
- branch: config.get("branch") ?? options.branch ?? ciEnv?.branch ?? null,
454
- token: config.get("token") ?? options.token ?? null,
455
- buildName: config.get("buildName") ?? options.buildName ?? null,
456
- prNumber: config.get("prNumber") ?? options.prNumber ?? ciEnv?.prNumber ?? null,
457
- ciService: ciEnv?.name ?? null,
458
- owner: ciEnv?.owner ?? null,
459
- repository: ciEnv?.repository ?? null,
460
- jobId: ciEnv?.jobId ?? null,
461
- runId: ciEnv?.runId ?? null
462
- });
463
- if (options.parallel) {
464
- config.load({
465
- parallel: Boolean(options.parallel),
466
- parallelNonce: options.parallel ? options.parallel.nonce : null,
467
- parallelTotal: options.parallel ? options.parallel.total : null
468
- });
469
- }
470
- config.validate();
471
- return config.get();
472
- };
473
- /**
474
- * Upload screenshots to argos-ci.com.
475
- */ const upload = async (params)=>{
476
- debug("Starting upload with params", params);
477
- // Read config
478
- const config = getConfigFromOptions(params);
479
- const files = params.files ?? [
480
- "**/*.{png,jpg,jpeg}"
481
- ];
482
- debug("Using config and files", config, files);
483
- const apiClient = createArgosApiClient({
484
- baseUrl: config.apiBaseUrl,
485
- bearerToken: getBearerToken(config)
486
- });
487
- // Collect screenshots
488
- const foundScreenshots = await discoverScreenshots(files, {
489
- root: params.root,
490
- ignore: params.ignore
491
- });
492
- debug("Found screenshots", foundScreenshots);
493
- // Optimize & compute hashes
494
- const screenshots = await Promise.all(foundScreenshots.map(async (screenshot)=>{
495
- const optimizedPath = await optimizeScreenshot(screenshot.path);
496
- const hash = await hashFile(optimizedPath);
497
- return {
498
- ...screenshot,
499
- optimizedPath,
500
- hash
501
- };
502
- }));
503
- // Create build
504
- debug("Creating build");
505
- const result = await apiClient.createBuild({
506
- commit: config.commit,
507
- branch: config.branch,
508
- name: config.buildName,
509
- parallel: config.parallel,
510
- parallelNonce: config.parallelNonce,
511
- screenshotKeys: Array.from(new Set(screenshots.map((screenshot)=>screenshot.hash))),
512
- prNumber: config.prNumber
513
- });
514
- debug("Got screenshots", result);
515
- // Upload screenshots
516
- debug("Uploading screenshots");
517
- await Promise.all(result.screenshots.map(async ({ key , putUrl })=>{
518
- const screenshot = screenshots.find((s)=>s.hash === key);
519
- if (!screenshot) {
520
- throw new Error(`Invariant: screenshot with hash ${key} not found`);
521
- }
522
- await upload$1({
523
- url: putUrl,
524
- path: screenshot.optimizedPath
525
- });
526
- }));
527
- // Update build
528
- debug("Updating build");
529
- await apiClient.updateBuild({
530
- buildId: result.build.id,
531
- screenshots: screenshots.map((screenshot)=>({
532
- key: screenshot.hash,
533
- name: screenshot.name
534
- })),
535
- parallel: config.parallel,
536
- parallelTotal: config.parallelTotal
537
- });
538
- return {
539
- build: result.build,
540
- screenshots
541
- };
542
- };
543
-
544
8
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
545
9
  const rawPkg = await readFile(resolve(__dirname, "..", "package.json"), "utf8");
546
10
  const pkg = JSON.parse(rawPkg);
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.3",
4
+ "version": "0.4.5",
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.3",
43
+ "@argos-ci/core": "^0.8.1",
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": "56d7e2259161c49265d8983ce0f0ecfe71910d2b"
52
+ "gitHead": "12cffdc1330d8e8c89db91cb87ddcc6775da34be"
53
53
  }