@argos-ci/core 2.9.2 → 2.11.0

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.js ADDED
@@ -0,0 +1,1032 @@
1
+ // src/upload.ts
2
+ import {
3
+ createClient,
4
+ throwAPIError
5
+ } from "@argos-ci/api-client";
6
+
7
+ // src/config.ts
8
+ import convict from "convict";
9
+
10
+ // src/ci-environment/git.ts
11
+ import { execSync } from "node:child_process";
12
+ function checkIsGitRepository() {
13
+ try {
14
+ return execSync("git rev-parse --is-inside-work-tree").toString().trim() === "true";
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function head() {
20
+ try {
21
+ return execSync("git rev-parse HEAD").toString().trim();
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ function branch() {
27
+ try {
28
+ const headRef = execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
29
+ if (headRef === "HEAD") {
30
+ return null;
31
+ }
32
+ return headRef;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function getMergeBaseCommitShaWithDepth(input) {
38
+ try {
39
+ execSync(
40
+ `git fetch --update-head-ok --depth ${input.depth} origin ${input.head}:${input.head}`
41
+ );
42
+ execSync(
43
+ `git fetch --update-head-ok --depth ${input.depth} origin ${input.base}:${input.base}`
44
+ );
45
+ const mergeBase = execSync(`git merge-base ${input.head} ${input.base}`).toString().trim();
46
+ return mergeBase || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function getMergeBaseCommitSha(input) {
52
+ let depth = 200;
53
+ while (depth < 1e3) {
54
+ const mergeBase = getMergeBaseCommitShaWithDepth({
55
+ depth,
56
+ ...input
57
+ });
58
+ if (mergeBase) {
59
+ return mergeBase;
60
+ }
61
+ depth += 200;
62
+ }
63
+ return null;
64
+ }
65
+ function listParentCommits(input) {
66
+ try {
67
+ execSync(`git fetch --depth=200 origin ${input.sha}`);
68
+ const raw = execSync(`git log --format="%H" --max-count=200 ${input.sha}`);
69
+ const shas = raw.toString().trim().split("\n");
70
+ return shas;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ // src/ci-environment/services/bitrise.ts
77
+ var getPrNumber = ({ env }) => {
78
+ return env.BITRISE_PULL_REQUEST ? Number(env.BITRISE_PULL_REQUEST) : null;
79
+ };
80
+ var service = {
81
+ name: "Bitrise",
82
+ key: "bitrise",
83
+ detect: ({ env }) => Boolean(env.BITRISE_IO),
84
+ config: ({ env }) => {
85
+ return {
86
+ commit: env.BITRISE_GIT_COMMIT || null,
87
+ branch: env.BITRISE_GIT_BRANCH || null,
88
+ owner: env.BITRISEIO_GIT_REPOSITORY_OWNER || null,
89
+ repository: env.BITRISEIO_GIT_REPOSITORY_SLUG || null,
90
+ jobId: null,
91
+ runId: null,
92
+ runAttempt: null,
93
+ prNumber: getPrNumber({ env }),
94
+ prHeadCommit: null,
95
+ prBaseBranch: null,
96
+ nonce: env.BITRISEIO_PIPELINE_ID || null
97
+ };
98
+ },
99
+ getMergeBaseCommitSha,
100
+ listParentCommits
101
+ };
102
+ var bitrise_default = service;
103
+
104
+ // src/ci-environment/services/buildkite.ts
105
+ var service2 = {
106
+ name: "Buildkite",
107
+ key: "buildkite",
108
+ detect: ({ env }) => Boolean(env.BUILDKITE),
109
+ config: ({ env }) => {
110
+ return {
111
+ // Buildkite doesn't work well so we fallback to git to ensure we have commit and branch
112
+ commit: env.BUILDKITE_COMMIT || head() || null,
113
+ branch: env.BUILDKITE_BRANCH || branch() || null,
114
+ owner: env.BUILDKITE_ORGANIZATION_SLUG || null,
115
+ repository: env.BUILDKITE_PROJECT_SLUG || null,
116
+ jobId: null,
117
+ runId: null,
118
+ runAttempt: null,
119
+ prNumber: env.BUILDKITE_PULL_REQUEST ? Number(env.BUILDKITE_PULL_REQUEST) : null,
120
+ prHeadCommit: null,
121
+ prBaseBranch: null,
122
+ nonce: env.BUILDKITE_BUILD_ID || null
123
+ };
124
+ },
125
+ getMergeBaseCommitSha,
126
+ listParentCommits
127
+ };
128
+ var buildkite_default = service2;
129
+
130
+ // src/ci-environment/services/heroku.ts
131
+ var service3 = {
132
+ name: "Heroku",
133
+ key: "heroku",
134
+ detect: ({ env }) => Boolean(env.HEROKU_TEST_RUN_ID),
135
+ config: ({ env }) => ({
136
+ commit: env.HEROKU_TEST_RUN_COMMIT_VERSION || null,
137
+ branch: env.HEROKU_TEST_RUN_BRANCH || null,
138
+ owner: null,
139
+ repository: null,
140
+ jobId: null,
141
+ runId: null,
142
+ runAttempt: null,
143
+ prNumber: null,
144
+ prHeadCommit: null,
145
+ prBaseBranch: null,
146
+ nonce: env.HEROKU_TEST_RUN_ID || null
147
+ }),
148
+ getMergeBaseCommitSha,
149
+ listParentCommits
150
+ };
151
+ var heroku_default = service3;
152
+
153
+ // src/ci-environment/services/github-actions.ts
154
+ import { existsSync, readFileSync } from "node:fs";
155
+ import axios from "axios";
156
+
157
+ // src/debug.ts
158
+ import createDebug from "debug";
159
+ var KEY = "@argos-ci/core";
160
+ var debug = createDebug(KEY);
161
+ var debugTime = (arg) => {
162
+ const enabled = createDebug.enabled(KEY);
163
+ if (enabled) {
164
+ console.time(arg);
165
+ }
166
+ };
167
+ var debugTimeEnd = (arg) => {
168
+ const enabled = createDebug.enabled(KEY);
169
+ if (enabled) {
170
+ console.timeEnd(arg);
171
+ }
172
+ };
173
+
174
+ // src/ci-environment/services/github-actions.ts
175
+ async function getPullRequestFromHeadSha({ env }, sha) {
176
+ debug("Fetching pull request number from head sha", sha);
177
+ if (!env.GITHUB_REPOSITORY) {
178
+ throw new Error("GITHUB_REPOSITORY is missing");
179
+ }
180
+ if (!env.GITHUB_TOKEN) {
181
+ if (!env.DISABLE_GITHUB_TOKEN_WARNING) {
182
+ console.log(
183
+ `
184
+ Running argos from a "deployment_status" event requires a GITHUB_TOKEN.
185
+ Please add \`GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}\` as environment variable.
186
+
187
+ Read more at https://argos-ci.com/docs/run-on-preview-deployment
188
+
189
+ To disable this warning, add \`DISABLE_GITHUB_TOKEN_WARNING: true\` as environment variable.
190
+ `.trim()
191
+ );
192
+ }
193
+ return null;
194
+ }
195
+ try {
196
+ const result = await axios.get(
197
+ `https://api.github.com/repos/${env.GITHUB_REPOSITORY}/pulls`,
198
+ {
199
+ params: {
200
+ state: "open",
201
+ sort: "updated",
202
+ per_page: 30,
203
+ page: 1
204
+ },
205
+ headers: {
206
+ Accept: "application/vnd.github+json",
207
+ Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
208
+ "X-GitHub-Api-Version": "2022-11-28"
209
+ }
210
+ }
211
+ );
212
+ if (result.data.length === 0) {
213
+ debug("Aborting because no pull request found");
214
+ return null;
215
+ }
216
+ const matchingPr = result.data.find((pr) => pr.head.sha === sha);
217
+ if (matchingPr) {
218
+ debug("Pull request found", matchingPr);
219
+ return matchingPr;
220
+ }
221
+ debug("Aborting because no pull request found");
222
+ return null;
223
+ } catch (error) {
224
+ debug("Error while fetching pull request from head sha", error);
225
+ return null;
226
+ }
227
+ }
228
+ function getBranch(context, eventPayload) {
229
+ if (eventPayload?.pull_request?.head.ref) {
230
+ return eventPayload.pull_request.head.ref;
231
+ }
232
+ const { env } = context;
233
+ if (env.GITHUB_HEAD_REF) {
234
+ return env.GITHUB_HEAD_REF;
235
+ }
236
+ if (!env.GITHUB_REF) {
237
+ return null;
238
+ }
239
+ const branchRegex = /refs\/heads\/(.*)/;
240
+ const matches = branchRegex.exec(env.GITHUB_REF);
241
+ return matches?.[1] ?? null;
242
+ }
243
+ function getRepository({ env }) {
244
+ if (!env.GITHUB_REPOSITORY) return null;
245
+ return env.GITHUB_REPOSITORY.split("/")[1] || null;
246
+ }
247
+ function readEventPayload({ env }) {
248
+ if (!env.GITHUB_EVENT_PATH) return null;
249
+ if (!existsSync(env.GITHUB_EVENT_PATH)) return null;
250
+ return JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf-8"));
251
+ }
252
+ var service4 = {
253
+ name: "GitHub Actions",
254
+ key: "github-actions",
255
+ detect: (context) => Boolean(context.env.GITHUB_ACTIONS),
256
+ config: async (context) => {
257
+ const { env } = context;
258
+ const payload = readEventPayload(context);
259
+ const sha = process.env.GITHUB_SHA || null;
260
+ if (!sha) {
261
+ throw new Error(`GITHUB_SHA is missing`);
262
+ }
263
+ const commonConfig = {
264
+ commit: sha,
265
+ owner: env.GITHUB_REPOSITORY_OWNER || null,
266
+ repository: getRepository(context),
267
+ jobId: env.GITHUB_JOB || null,
268
+ runId: env.GITHUB_RUN_ID || null,
269
+ runAttempt: env.GITHUB_RUN_ATTEMPT ? Number(env.GITHUB_RUN_ATTEMPT) : null,
270
+ nonce: `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` || null
271
+ };
272
+ if (payload?.deployment) {
273
+ debug("Deployment event detected");
274
+ const pullRequest = await getPullRequestFromHeadSha(context, sha);
275
+ return {
276
+ ...commonConfig,
277
+ // If no pull request is found, we fallback to the deployment environment as branch name
278
+ // Branch name is required to create a build but has no real impact on the build.
279
+ branch: pullRequest?.head.ref || payload.deployment.environment || null,
280
+ prNumber: pullRequest?.number || null,
281
+ prHeadCommit: pullRequest?.head.sha || null,
282
+ prBaseBranch: null
283
+ };
284
+ }
285
+ return {
286
+ ...commonConfig,
287
+ branch: payload?.pull_request?.head.ref || getBranch(context, payload) || null,
288
+ prNumber: payload?.pull_request?.number || null,
289
+ prHeadCommit: payload?.pull_request?.head.sha ?? null,
290
+ prBaseBranch: payload?.pull_request?.base.ref ?? null
291
+ };
292
+ },
293
+ getMergeBaseCommitSha,
294
+ listParentCommits
295
+ };
296
+ var github_actions_default = service4;
297
+
298
+ // src/ci-environment/services/circleci.ts
299
+ var getPrNumber2 = ({ env }) => {
300
+ const branchRegex = /pull\/(\d+)/;
301
+ const matches = branchRegex.exec(env.CIRCLE_PULL_REQUEST || "");
302
+ if (matches) {
303
+ return Number(matches[1]);
304
+ }
305
+ return null;
306
+ };
307
+ var service5 = {
308
+ name: "CircleCI",
309
+ key: "circleci",
310
+ detect: ({ env }) => Boolean(env.CIRCLECI),
311
+ config: ({ env }) => {
312
+ return {
313
+ commit: env.CIRCLE_SHA1 || null,
314
+ branch: env.CIRCLE_BRANCH || null,
315
+ owner: env.CIRCLE_PROJECT_USERNAME || null,
316
+ repository: env.CIRCLE_PROJECT_REPONAME || null,
317
+ jobId: null,
318
+ runId: null,
319
+ runAttempt: null,
320
+ prNumber: getPrNumber2({ env }),
321
+ prHeadCommit: null,
322
+ prBaseBranch: null,
323
+ nonce: env.CIRCLE_WORKFLOW_ID || env.CIRCLE_BUILD_NUM || null
324
+ };
325
+ },
326
+ getMergeBaseCommitSha,
327
+ listParentCommits
328
+ };
329
+ var circleci_default = service5;
330
+
331
+ // src/ci-environment/services/travis.ts
332
+ var getOwner = ({ env }) => {
333
+ if (!env.TRAVIS_REPO_SLUG) return null;
334
+ return env.TRAVIS_REPO_SLUG.split("/")[0] || null;
335
+ };
336
+ var getRepository2 = ({ env }) => {
337
+ if (!env.TRAVIS_REPO_SLUG) return null;
338
+ return env.TRAVIS_REPO_SLUG.split("/")[1] || null;
339
+ };
340
+ var getPrNumber3 = ({ env }) => {
341
+ if (env.TRAVIS_PULL_REQUEST) return Number(env.TRAVIS_PULL_REQUEST);
342
+ return null;
343
+ };
344
+ var service6 = {
345
+ name: "Travis CI",
346
+ key: "travis",
347
+ detect: ({ env }) => Boolean(env.TRAVIS),
348
+ config: (ctx) => {
349
+ const { env } = ctx;
350
+ return {
351
+ commit: env.TRAVIS_COMMIT || null,
352
+ branch: env.TRAVIS_BRANCH || null,
353
+ owner: getOwner(ctx),
354
+ repository: getRepository2(ctx),
355
+ jobId: null,
356
+ runId: null,
357
+ runAttempt: null,
358
+ prNumber: getPrNumber3(ctx),
359
+ prHeadCommit: null,
360
+ prBaseBranch: null,
361
+ nonce: env.TRAVIS_BUILD_ID || null
362
+ };
363
+ },
364
+ getMergeBaseCommitSha,
365
+ listParentCommits
366
+ };
367
+ var travis_default = service6;
368
+
369
+ // src/ci-environment/services/gitlab.ts
370
+ var service7 = {
371
+ name: "GitLab",
372
+ key: "gitlab",
373
+ detect: ({ env }) => env.GITLAB_CI === "true",
374
+ config: ({ env }) => {
375
+ return {
376
+ commit: env.CI_COMMIT_SHA || null,
377
+ branch: env.CI_COMMIT_REF_NAME || null,
378
+ owner: null,
379
+ repository: null,
380
+ jobId: null,
381
+ runId: null,
382
+ runAttempt: null,
383
+ prNumber: null,
384
+ prHeadCommit: null,
385
+ prBaseBranch: null,
386
+ nonce: env.CI_PIPELINE_ID || null
387
+ };
388
+ },
389
+ getMergeBaseCommitSha,
390
+ listParentCommits
391
+ };
392
+ var gitlab_default = service7;
393
+
394
+ // src/ci-environment/services/git.ts
395
+ var service8 = {
396
+ name: "Git",
397
+ key: "git",
398
+ detect: () => checkIsGitRepository(),
399
+ config: () => {
400
+ return {
401
+ commit: head() || null,
402
+ branch: branch() || null,
403
+ owner: null,
404
+ repository: null,
405
+ jobId: null,
406
+ runId: null,
407
+ runAttempt: null,
408
+ prNumber: null,
409
+ prHeadCommit: null,
410
+ prBaseBranch: null,
411
+ nonce: null
412
+ };
413
+ },
414
+ getMergeBaseCommitSha,
415
+ listParentCommits
416
+ };
417
+ var git_default = service8;
418
+
419
+ // src/ci-environment/index.ts
420
+ var services = [
421
+ heroku_default,
422
+ github_actions_default,
423
+ circleci_default,
424
+ travis_default,
425
+ buildkite_default,
426
+ gitlab_default,
427
+ bitrise_default,
428
+ git_default
429
+ ];
430
+ function createContext() {
431
+ return { env: process.env };
432
+ }
433
+ function getCiService(context) {
434
+ return services.find((service9) => service9.detect(context));
435
+ }
436
+ function getMergeBaseCommitSha2(input) {
437
+ const context = createContext();
438
+ const service9 = getCiService(context);
439
+ if (!service9) {
440
+ return null;
441
+ }
442
+ return service9.getMergeBaseCommitSha(input, context);
443
+ }
444
+ function listParentCommits2(input) {
445
+ const context = createContext();
446
+ const service9 = getCiService(context);
447
+ if (!service9) {
448
+ return null;
449
+ }
450
+ return service9.listParentCommits(input, context);
451
+ }
452
+ async function getCiEnvironment() {
453
+ const context = createContext();
454
+ debug("Detecting CI environment", context);
455
+ const service9 = getCiService(context);
456
+ if (service9) {
457
+ debug("Internal service matched", service9.name);
458
+ const variables = await service9.config(context);
459
+ const ciEnvironment = {
460
+ name: service9.name,
461
+ key: service9.key,
462
+ ...variables
463
+ };
464
+ debug("CI environment", ciEnvironment);
465
+ return ciEnvironment;
466
+ }
467
+ return null;
468
+ }
469
+
470
+ // src/config.ts
471
+ var mustBeApiBaseUrl = (value) => {
472
+ const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
473
+ if (!URL_REGEX.test(value)) {
474
+ throw new Error("Invalid Argos API base URL");
475
+ }
476
+ };
477
+ var mustBeCommit = (value) => {
478
+ const SHA1_REGEX = /^[0-9a-f]{40}$/;
479
+ if (!SHA1_REGEX.test(value)) {
480
+ const SHA1_SHORT_REGEX = /^[0-9a-f]{7}$/;
481
+ if (SHA1_SHORT_REGEX.test(value)) {
482
+ throw new Error("Short SHA1 is not allowed");
483
+ }
484
+ throw new Error("Invalid commit");
485
+ }
486
+ };
487
+ var mustBeArgosToken = (value) => {
488
+ if (value && value.length !== 40) {
489
+ throw new Error("Invalid Argos repository token (must be 40 characters)");
490
+ }
491
+ };
492
+ convict.addFormat({
493
+ name: "float-percent",
494
+ validate: function(val) {
495
+ if (val !== 0 && (!val || val > 1 || val < 0)) {
496
+ throw new Error("Must be a float between 0 and 1, inclusive.");
497
+ }
498
+ },
499
+ coerce: function(val) {
500
+ return parseFloat(val);
501
+ }
502
+ });
503
+ var schema = {
504
+ apiBaseUrl: {
505
+ env: "ARGOS_API_BASE_URL",
506
+ default: "https://api.argos-ci.com/v2/",
507
+ format: mustBeApiBaseUrl
508
+ },
509
+ commit: {
510
+ env: "ARGOS_COMMIT",
511
+ default: null,
512
+ format: mustBeCommit
513
+ },
514
+ branch: {
515
+ env: "ARGOS_BRANCH",
516
+ default: null,
517
+ format: String
518
+ },
519
+ token: {
520
+ env: "ARGOS_TOKEN",
521
+ default: null,
522
+ format: mustBeArgosToken
523
+ },
524
+ buildName: {
525
+ env: "ARGOS_BUILD_NAME",
526
+ default: null,
527
+ format: String,
528
+ nullable: true
529
+ },
530
+ mode: {
531
+ env: "ARGOS_MODE",
532
+ format: ["ci", "monitoring"],
533
+ default: null,
534
+ nullable: true
535
+ },
536
+ prNumber: {
537
+ env: "ARGOS_PR_NUMBER",
538
+ format: Number,
539
+ default: null,
540
+ nullable: true
541
+ },
542
+ prHeadCommit: {
543
+ env: "ARGOS_PR_HEAD_COMMIT",
544
+ format: String,
545
+ default: null,
546
+ nullable: true
547
+ },
548
+ prBaseBranch: {
549
+ env: "ARGOS_PR_BASE_BRANCH",
550
+ format: String,
551
+ default: null,
552
+ nullable: true
553
+ },
554
+ parallel: {
555
+ env: "ARGOS_PARALLEL",
556
+ default: false,
557
+ format: Boolean
558
+ },
559
+ parallelNonce: {
560
+ env: "ARGOS_PARALLEL_NONCE",
561
+ format: String,
562
+ default: null,
563
+ nullable: true
564
+ },
565
+ parallelIndex: {
566
+ env: "ARGOS_PARALLEL_INDEX",
567
+ format: "nat",
568
+ default: null,
569
+ nullable: true
570
+ },
571
+ parallelTotal: {
572
+ env: "ARGOS_PARALLEL_TOTAL",
573
+ format: "int",
574
+ default: null,
575
+ nullable: true
576
+ },
577
+ referenceBranch: {
578
+ env: "ARGOS_REFERENCE_BRANCH",
579
+ format: String,
580
+ default: null,
581
+ nullable: true
582
+ },
583
+ referenceCommit: {
584
+ env: "ARGOS_REFERENCE_COMMIT",
585
+ format: String,
586
+ default: null,
587
+ nullable: true
588
+ },
589
+ jobId: {
590
+ format: String,
591
+ default: null,
592
+ nullable: true
593
+ },
594
+ runId: {
595
+ format: String,
596
+ default: null,
597
+ nullable: true
598
+ },
599
+ runAttempt: {
600
+ format: "nat",
601
+ default: null,
602
+ nullable: true
603
+ },
604
+ owner: {
605
+ format: String,
606
+ default: null,
607
+ nullable: true
608
+ },
609
+ repository: {
610
+ format: String,
611
+ default: null,
612
+ nullable: true
613
+ },
614
+ ciProvider: {
615
+ format: String,
616
+ default: null,
617
+ nullable: true
618
+ },
619
+ threshold: {
620
+ env: "ARGOS_THRESHOLD",
621
+ format: "float-percent",
622
+ default: null,
623
+ nullable: true
624
+ }
625
+ };
626
+ var createConfig = () => {
627
+ return convict(schema, {
628
+ args: []
629
+ });
630
+ };
631
+ async function readConfig(options = {}) {
632
+ const config = createConfig();
633
+ const ciEnv = await getCiEnvironment();
634
+ config.load({
635
+ apiBaseUrl: options.apiBaseUrl || config.get("apiBaseUrl"),
636
+ commit: options.commit || config.get("commit") || ciEnv?.commit || null,
637
+ branch: options.branch || config.get("branch") || ciEnv?.branch || null,
638
+ token: options.token || config.get("token") || null,
639
+ buildName: options.buildName || config.get("buildName") || null,
640
+ prNumber: options.prNumber || config.get("prNumber") || ciEnv?.prNumber || null,
641
+ prHeadCommit: config.get("prHeadCommit") || ciEnv?.prHeadCommit || null,
642
+ prBaseBranch: config.get("prBaseBranch") || ciEnv?.prBaseBranch || null,
643
+ referenceBranch: options.referenceBranch || config.get("referenceBranch") || null,
644
+ referenceCommit: options.referenceCommit || config.get("referenceCommit") || null,
645
+ owner: ciEnv?.owner || null,
646
+ repository: ciEnv?.repository || null,
647
+ jobId: ciEnv?.jobId || null,
648
+ runId: ciEnv?.runId || null,
649
+ runAttempt: ciEnv?.runAttempt || null,
650
+ parallel: options.parallel ?? config.get("parallel") ?? false,
651
+ parallelNonce: options.parallelNonce || config.get("parallelNonce") || ciEnv?.nonce || null,
652
+ parallelTotal: options.parallelTotal ?? config.get("parallelTotal") ?? null,
653
+ parallelIndex: options.parallelIndex ?? config.get("parallelIndex") ?? null,
654
+ mode: options.mode || config.get("mode") || null,
655
+ ciProvider: ciEnv?.key || null
656
+ });
657
+ config.validate();
658
+ return config.get();
659
+ }
660
+
661
+ // src/discovery.ts
662
+ import { resolve } from "node:path";
663
+ import glob from "fast-glob";
664
+ var discoverScreenshots = async (patterns, { root = process.cwd(), ignore } = {}) => {
665
+ debug(
666
+ `Discovering screenshots with patterns: ${Array.isArray(patterns) ? patterns.join(", ") : patterns} in ${root}`
667
+ );
668
+ const matches = await glob(patterns, { onlyFiles: true, ignore, cwd: root });
669
+ return matches.map((match) => {
670
+ debug(`Found screenshot: ${match}`);
671
+ const path = resolve(root, match);
672
+ return {
673
+ name: match,
674
+ path
675
+ };
676
+ });
677
+ };
678
+
679
+ // src/optimize.ts
680
+ import { promisify } from "node:util";
681
+ import sharp from "sharp";
682
+ import tmp from "tmp";
683
+ var tmpFile = promisify(tmp.file);
684
+ var optimizeScreenshot = async (filepath) => {
685
+ try {
686
+ const resultFilePath = await tmpFile();
687
+ await sharp(filepath).resize(2048, 64e3, {
688
+ fit: "inside",
689
+ withoutEnlargement: true
690
+ }).png({ force: true }).toFile(resultFilePath);
691
+ return resultFilePath;
692
+ } catch (error) {
693
+ const message = error instanceof Error ? error.message : "Unknown Error";
694
+ throw new Error(`Error while processing image (${filepath}): ${message}`, {
695
+ cause: error
696
+ });
697
+ }
698
+ };
699
+
700
+ // src/hashing.ts
701
+ import { createReadStream } from "node:fs";
702
+ import { createHash } from "node:crypto";
703
+ var hashFile = async (filepath) => {
704
+ const fileStream = createReadStream(filepath);
705
+ const hash = createHash("sha256");
706
+ await new Promise((resolve2, reject) => {
707
+ fileStream.on("error", reject);
708
+ hash.on("error", reject);
709
+ hash.on("finish", resolve2);
710
+ fileStream.pipe(hash);
711
+ });
712
+ return hash.digest("hex");
713
+ };
714
+
715
+ // src/auth.ts
716
+ var base64Encode = (obj) => Buffer.from(JSON.stringify(obj), "utf8").toString("base64");
717
+ function getAuthToken({
718
+ token,
719
+ ciProvider,
720
+ owner,
721
+ repository,
722
+ jobId,
723
+ runId,
724
+ prNumber
725
+ }) {
726
+ if (token) {
727
+ return token;
728
+ }
729
+ switch (ciProvider) {
730
+ case "github-actions": {
731
+ if (!owner || !repository || !jobId || !runId) {
732
+ throw new Error(
733
+ `Automatic GitHub Actions variables detection failed. Please add the 'ARGOS_TOKEN'`
734
+ );
735
+ }
736
+ return `tokenless-github-${base64Encode({
737
+ owner,
738
+ repository,
739
+ jobId,
740
+ runId,
741
+ prNumber
742
+ })}`;
743
+ }
744
+ default:
745
+ throw new Error("Missing Argos repository token 'ARGOS_TOKEN'");
746
+ }
747
+ }
748
+
749
+ // src/s3.ts
750
+ import { readFile } from "node:fs/promises";
751
+ import axios2 from "axios";
752
+ var upload = async (input) => {
753
+ const file = await readFile(input.path);
754
+ await axios2({
755
+ method: "PUT",
756
+ url: input.url,
757
+ data: file,
758
+ headers: {
759
+ "Content-Type": input.contentType
760
+ }
761
+ });
762
+ };
763
+
764
+ // src/util/chunk.ts
765
+ var chunk = (collection, size) => {
766
+ const result = [];
767
+ for (let x = 0; x < Math.ceil(collection.length / size); x++) {
768
+ let start = x * size;
769
+ let end = start + size;
770
+ result.push(collection.slice(start, end));
771
+ }
772
+ return result;
773
+ };
774
+
775
+ // src/upload.ts
776
+ import { getPlaywrightTracePath, readMetadata } from "@argos-ci/util";
777
+
778
+ // src/version.ts
779
+ import { readVersionFromPackage } from "@argos-ci/util";
780
+ import { createRequire } from "node:module";
781
+ var require2 = createRequire(import.meta.url);
782
+ async function getArgosCoreSDKIdentifier() {
783
+ const pkgPath = require2.resolve("@argos-ci/core/package.json");
784
+ const version = await readVersionFromPackage(pkgPath);
785
+ return `@argos-ci/core@${version}`;
786
+ }
787
+
788
+ // src/upload.ts
789
+ var CHUNK_SIZE = 10;
790
+ async function getConfigFromOptions({
791
+ parallel,
792
+ ...options
793
+ }) {
794
+ return readConfig({
795
+ ...options,
796
+ parallel: Boolean(parallel),
797
+ parallelNonce: parallel ? parallel.nonce : null,
798
+ parallelTotal: parallel ? parallel.total : null,
799
+ parallelIndex: parallel ? parallel.index : null
800
+ });
801
+ }
802
+ async function uploadFilesToS3(files) {
803
+ debug(`Split files in chunks of ${CHUNK_SIZE}`);
804
+ const chunks = chunk(files, CHUNK_SIZE);
805
+ debug(`Starting upload of ${chunks.length} chunks`);
806
+ for (let i = 0; i < chunks.length; i++) {
807
+ debug(`Uploading chunk ${i + 1}/${chunks.length}`);
808
+ const timeLabel = `Chunk ${i + 1}/${chunks.length}`;
809
+ debugTime(timeLabel);
810
+ const chunk2 = chunks[i];
811
+ if (!chunk2) {
812
+ throw new Error(`Invariant: chunk ${i} is empty`);
813
+ }
814
+ await Promise.all(
815
+ chunk2.map(async ({ url, path, contentType }) => {
816
+ await upload({
817
+ url,
818
+ path,
819
+ contentType
820
+ });
821
+ })
822
+ );
823
+ debugTimeEnd(timeLabel);
824
+ }
825
+ }
826
+ async function upload2(params) {
827
+ debug("Starting upload with params", params);
828
+ const [config, argosSdk] = await Promise.all([
829
+ getConfigFromOptions(params),
830
+ getArgosCoreSDKIdentifier()
831
+ ]);
832
+ const files = params.files ?? ["**/*.{png,jpg,jpeg}"];
833
+ debug("Using config and files", config, files);
834
+ const authToken = getAuthToken(config);
835
+ const apiClient = createClient({
836
+ baseUrl: config.apiBaseUrl,
837
+ authToken
838
+ });
839
+ const foundScreenshots = await discoverScreenshots(files, {
840
+ root: params.root,
841
+ ignore: params.ignore
842
+ });
843
+ debug("Found screenshots", foundScreenshots);
844
+ const screenshots = await Promise.all(
845
+ foundScreenshots.map(async (screenshot) => {
846
+ const [metadata, pwTracePath, optimizedPath] = await Promise.all([
847
+ readMetadata(screenshot.path),
848
+ getPlaywrightTracePath(screenshot.path),
849
+ optimizeScreenshot(screenshot.path)
850
+ ]);
851
+ const [hash, pwTraceHash] = await Promise.all([
852
+ hashFile(optimizedPath),
853
+ pwTracePath ? hashFile(pwTracePath) : null
854
+ ]);
855
+ const threshold = metadata?.transient?.threshold ?? null;
856
+ const baseName = metadata?.transient?.baseName ?? null;
857
+ if (metadata) {
858
+ delete metadata.transient;
859
+ }
860
+ return {
861
+ ...screenshot,
862
+ hash,
863
+ optimizedPath,
864
+ metadata,
865
+ threshold,
866
+ baseName,
867
+ pwTrace: pwTracePath && pwTraceHash ? { path: pwTracePath, hash: pwTraceHash } : null
868
+ };
869
+ })
870
+ );
871
+ debug("Fetch project");
872
+ const projectResponse = await apiClient.GET("/project");
873
+ if (projectResponse.error) {
874
+ throwAPIError(projectResponse.error);
875
+ }
876
+ debug("Project fetched", projectResponse.data);
877
+ const { defaultBaseBranch, hasRemoteContentAccess } = projectResponse.data;
878
+ const referenceCommit = (() => {
879
+ if (config.referenceCommit) {
880
+ debug("Found reference commit in config", config.referenceCommit);
881
+ return config.referenceCommit;
882
+ }
883
+ if (hasRemoteContentAccess) {
884
+ return null;
885
+ }
886
+ const base = config.referenceBranch || config.prBaseBranch || defaultBaseBranch;
887
+ const sha = getMergeBaseCommitSha2({ base, head: config.branch });
888
+ if (sha) {
889
+ debug("Found merge base", sha);
890
+ } else {
891
+ debug("No merge base found");
892
+ }
893
+ return sha;
894
+ })();
895
+ const parentCommits = (() => {
896
+ if (hasRemoteContentAccess) {
897
+ return null;
898
+ }
899
+ if (referenceCommit) {
900
+ const commits = listParentCommits2({ sha: referenceCommit });
901
+ if (commits) {
902
+ debug("Found parent commits", commits);
903
+ } else {
904
+ debug("No parent commits found");
905
+ }
906
+ return commits;
907
+ }
908
+ return null;
909
+ })();
910
+ debug("Creating build");
911
+ const [pwTraceKeys, screenshotKeys] = screenshots.reduce(
912
+ ([pwTraceKeys2, screenshotKeys2], screenshot) => {
913
+ if (screenshot.pwTrace && !pwTraceKeys2.includes(screenshot.pwTrace.hash)) {
914
+ pwTraceKeys2.push(screenshot.pwTrace.hash);
915
+ }
916
+ if (!screenshotKeys2.includes(screenshot.hash)) {
917
+ screenshotKeys2.push(screenshot.hash);
918
+ }
919
+ return [pwTraceKeys2, screenshotKeys2];
920
+ },
921
+ [[], []]
922
+ );
923
+ const createBuildResponse = await apiClient.POST("/builds", {
924
+ body: {
925
+ commit: config.commit,
926
+ branch: config.branch,
927
+ name: config.buildName,
928
+ mode: config.mode,
929
+ parallel: config.parallel,
930
+ parallelNonce: config.parallelNonce,
931
+ screenshotKeys,
932
+ pwTraceKeys,
933
+ prNumber: config.prNumber,
934
+ prHeadCommit: config.prHeadCommit,
935
+ referenceBranch: config.referenceBranch,
936
+ referenceCommit,
937
+ parentCommits,
938
+ argosSdk,
939
+ ciProvider: config.ciProvider,
940
+ runId: config.runId,
941
+ runAttempt: config.runAttempt
942
+ }
943
+ });
944
+ if (createBuildResponse.error) {
945
+ throwAPIError(createBuildResponse.error);
946
+ }
947
+ const result = createBuildResponse.data;
948
+ debug("Got uploads url", result);
949
+ const uploadFiles = [
950
+ ...result.screenshots.map(({ key, putUrl }) => {
951
+ const screenshot = screenshots.find((s) => s.hash === key);
952
+ if (!screenshot) {
953
+ throw new Error(`Invariant: screenshot with hash ${key} not found`);
954
+ }
955
+ return {
956
+ url: putUrl,
957
+ path: screenshot.optimizedPath,
958
+ contentType: "image/png"
959
+ };
960
+ }),
961
+ ...result.pwTraces?.map(({ key, putUrl }) => {
962
+ const screenshot = screenshots.find(
963
+ (s) => s.pwTrace && s.pwTrace.hash === key
964
+ );
965
+ if (!screenshot || !screenshot.pwTrace) {
966
+ throw new Error(`Invariant: trace with ${key} not found`);
967
+ }
968
+ return {
969
+ url: putUrl,
970
+ path: screenshot.pwTrace.path,
971
+ contentType: "application/json"
972
+ };
973
+ }) ?? []
974
+ ];
975
+ await uploadFilesToS3(uploadFiles);
976
+ debug("Updating build");
977
+ const uploadBuildResponse = await apiClient.PUT("/builds/{buildId}", {
978
+ params: {
979
+ path: {
980
+ buildId: result.build.id
981
+ }
982
+ },
983
+ body: {
984
+ screenshots: screenshots.map((screenshot) => ({
985
+ key: screenshot.hash,
986
+ name: screenshot.name,
987
+ metadata: screenshot.metadata,
988
+ pwTraceKey: screenshot.pwTrace?.hash ?? null,
989
+ threshold: screenshot.threshold ?? config?.threshold ?? null,
990
+ baseName: screenshot.baseName
991
+ })),
992
+ parallel: config.parallel,
993
+ parallelTotal: config.parallelTotal,
994
+ parallelIndex: config.parallelIndex,
995
+ metadata: params.metadata
996
+ }
997
+ });
998
+ if (uploadBuildResponse.error) {
999
+ throwAPIError(uploadBuildResponse.error);
1000
+ }
1001
+ return { build: uploadBuildResponse.data.build, screenshots };
1002
+ }
1003
+
1004
+ // src/finalize.ts
1005
+ import { createClient as createClient2, throwAPIError as throwAPIError2 } from "@argos-ci/api-client";
1006
+ async function finalize(params) {
1007
+ const config = await readConfig({
1008
+ parallelNonce: params.parallel?.nonce ?? null
1009
+ });
1010
+ const authToken = getAuthToken(config);
1011
+ const apiClient = createClient2({
1012
+ baseUrl: config.apiBaseUrl,
1013
+ authToken
1014
+ });
1015
+ if (!config.parallelNonce) {
1016
+ throw new Error("parallel.nonce is required to finalize the build");
1017
+ }
1018
+ const finalizeBuildsResult = await apiClient.POST("/builds/finalize", {
1019
+ body: {
1020
+ parallelNonce: config.parallelNonce
1021
+ }
1022
+ });
1023
+ if (finalizeBuildsResult.error) {
1024
+ throwAPIError2(finalizeBuildsResult.error);
1025
+ }
1026
+ return finalizeBuildsResult.data;
1027
+ }
1028
+ export {
1029
+ finalize,
1030
+ readConfig,
1031
+ upload2 as upload
1032
+ };