@secureci/cli 0.5.1 → 0.7.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.
@@ -8,6 +8,7 @@ exports.withImplicitSuccess = withImplicitSuccess;
8
8
  exports.evaluateCondition = evaluateCondition;
9
9
  exports.ifEvaluationError = ifEvaluationError;
10
10
  exports.validateExpression = validateExpression;
11
+ exports.expressionDataFlow = expressionDataFlow;
11
12
  const logger_1 = require("../utils/logger");
12
13
  const hash_files_1 = require("./hash-files");
13
14
  function createContext(env = {}, inputs = {}, secrets = {}, github = {}, matrix = {}) {
@@ -205,6 +206,50 @@ function star(target) {
205
206
  }
206
207
  return new FilteredArray([]);
207
208
  }
209
+ /**
210
+ * Data-flow analysis value (see expressionDataFlow): which context data
211
+ * may end up in a value, instead of the value itself.
212
+ */
213
+ class Flow {
214
+ constructor(
215
+ /** Context paths whose value may be (part of) this value. */
216
+ values = [],
217
+ /** Context paths whose whole subtree may be (part of) it. */
218
+ trees = []) {
219
+ this.values = values;
220
+ this.trees = trees;
221
+ }
222
+ /** Everything any of the items carry; non-Flow items carry nothing. */
223
+ static union(...items) {
224
+ const flows = items.filter((i) => i instanceof Flow);
225
+ return new Flow(flows.flatMap((f) => f.values), flows.flatMap((f) => f.trees));
226
+ }
227
+ member(key) {
228
+ return new Flow(this.values.map((p) => [...p, key]), this.trees);
229
+ }
230
+ }
231
+ /** Functions whose result never carries their arguments' text. */
232
+ const OPAQUE_FUNCTIONS = new Set([
233
+ 'success',
234
+ 'failure',
235
+ 'always',
236
+ 'cancelled',
237
+ 'contains',
238
+ 'startswith',
239
+ 'endswith',
240
+ 'hashfiles'
241
+ ]);
242
+ /** The data flow of a function call's result, given its arguments' flows. */
243
+ function flowCall(name, args) {
244
+ const lower = name.toLowerCase();
245
+ if (OPAQUE_FUNCTIONS.has(lower))
246
+ return false;
247
+ const all = Flow.union(...args);
248
+ if (lower === 'tojson')
249
+ return new Flow([], [...all.values, ...all.trees]);
250
+ // format, join, fromJSON - and, conservatively, anything else.
251
+ return all;
252
+ }
208
253
  function stepsContext(ctx) {
209
254
  const steps = {};
210
255
  for (const [id, info] of Object.entries(ctx.steps)) {
@@ -341,12 +386,18 @@ function callFunction(name, rawArgs, ctx) {
341
386
  throw new Error(`Unrecognized function: '${name}'`);
342
387
  }
343
388
  }
389
+ /** The `*` filter in `x.*` / `x[*]`, as opposed to a `'*'` key. */
390
+ const STAR = Symbol('*');
344
391
  class Parser {
345
- /** With a scope, only check names and syntax; nothing is evaluated. */
346
- constructor(tokens, ctx, scope) {
392
+ /**
393
+ * With a scope, only check names and syntax; with `flow`, compute data
394
+ * flow (Flow values) instead. Either way nothing is evaluated.
395
+ */
396
+ constructor(tokens, ctx, scope, flow = false) {
347
397
  this.tokens = tokens;
348
398
  this.ctx = ctx;
349
399
  this.scope = scope;
400
+ this.flow = flow;
350
401
  this.pos = 0;
351
402
  }
352
403
  peek() {
@@ -378,7 +429,11 @@ class Parser {
378
429
  while (this.isOp('||')) {
379
430
  this.next();
380
431
  const right = this.parseAnd();
381
- left = toBoolean(unwrap(left)) ? left : right;
432
+ left = this.flow
433
+ ? Flow.union(left, right)
434
+ : toBoolean(unwrap(left))
435
+ ? left
436
+ : right;
382
437
  }
383
438
  return left;
384
439
  }
@@ -387,7 +442,11 @@ class Parser {
387
442
  while (this.isOp('&&')) {
388
443
  this.next();
389
444
  const right = this.parseEquality();
390
- left = toBoolean(unwrap(left)) ? right : left;
445
+ left = this.flow
446
+ ? Flow.union(left, right)
447
+ : toBoolean(unwrap(left))
448
+ ? right
449
+ : left;
391
450
  }
392
451
  return left;
393
452
  }
@@ -433,10 +492,10 @@ class Parser {
433
492
  this.next();
434
493
  const part = this.next();
435
494
  if (part.type === 'star') {
436
- value = star(value);
495
+ value = this.access(value, STAR);
437
496
  }
438
497
  else if (part.type === 'ident') {
439
- value = member(value, part.value);
498
+ value = this.access(value, part.value);
440
499
  }
441
500
  else {
442
501
  throw new Error('Expected property name after .');
@@ -446,11 +505,11 @@ class Parser {
446
505
  this.next();
447
506
  if (this.peek().type === 'star') {
448
507
  this.next();
449
- value = star(value);
508
+ value = this.access(value, STAR);
450
509
  }
451
510
  else {
452
511
  const key = unwrap(this.parseOr());
453
- value = member(value, typeof key === 'number' ? key : toText(key));
512
+ value = this.access(value, typeof key === 'number' || key instanceof Flow ? key : toText(key));
454
513
  }
455
514
  this.expect('rbracket');
456
515
  }
@@ -459,6 +518,18 @@ class Parser {
459
518
  }
460
519
  }
461
520
  }
521
+ /** `value.key`, `value[key]`, or `value.*` / `value[*]` for STAR. */
522
+ access(value, key) {
523
+ if (this.flow) {
524
+ if (!(value instanceof Flow))
525
+ return undefined;
526
+ // `.*` and a computed key (`x[matrix.field]`) may be any key.
527
+ return value.member(key === STAR || key instanceof Flow ? '*' : String(key).toLowerCase());
528
+ }
529
+ if (key === STAR)
530
+ return star(value);
531
+ return member(value, key);
532
+ }
462
533
  parsePrimary() {
463
534
  const tok = this.next();
464
535
  switch (tok.type) {
@@ -493,6 +564,8 @@ class Parser {
493
564
  checkCall(tok.value, args.length, this.scope.functions);
494
565
  return undefined;
495
566
  }
567
+ if (this.flow)
568
+ return flowCall(tok.value, args);
496
569
  return callFunction(tok.value, args, this.ctx);
497
570
  }
498
571
  if (this.scope) {
@@ -501,6 +574,8 @@ class Parser {
501
574
  }
502
575
  return undefined;
503
576
  }
577
+ if (this.flow)
578
+ return new Flow([[lower]]);
504
579
  return rootContext(lower, this.ctx);
505
580
  }
506
581
  default:
@@ -673,4 +748,27 @@ function validateExpression(expr, location) {
673
748
  return error instanceof Error ? error.message : String(error);
674
749
  }
675
750
  }
751
+ /**
752
+ * Static data-flow analysis of one expression (with or without its `${{ }}`
753
+ * wrapper), without evaluating it: which context data can reach the
754
+ * result. Comparisons, `!` and functions returning a boolean or hash
755
+ * (contains, startsWith, endsWith, status functions, hashFiles) carry no
756
+ * data; `||`/`&&` carry both operands (they return one of them); format,
757
+ * join and fromJSON carry their arguments; toJSON carries its argument's
758
+ * whole subtree. Returns undefined for a syntax error.
759
+ */
760
+ function expressionDataFlow(expr) {
761
+ let trimmed = expr.trim();
762
+ const wrapped = trimmed.match(/^\$\{\{([\s\S]*)\}\}$/);
763
+ if (wrapped)
764
+ trimmed = wrapped[1].trim();
765
+ try {
766
+ const result = new Parser(tokenize(trimmed), createContext(), undefined, true).parse();
767
+ const flow = Flow.union(result);
768
+ return { values: flow.values, trees: flow.trees };
769
+ }
770
+ catch {
771
+ return undefined;
772
+ }
773
+ }
676
774
  //# sourceMappingURL=expression.js.map
@@ -6,6 +6,11 @@ exports.isTruthySetting = isTruthySetting;
6
6
  exports.numberSetting = numberSetting;
7
7
  exports.matrixValue = matrixValue;
8
8
  exports.expandMatrix = expandMatrix;
9
+ exports.parseMatrixFilter = parseMatrixFilter;
10
+ exports.matrixValueText = matrixValueText;
11
+ exports.filterMatrix = filterMatrix;
12
+ exports.noMatrixMatchMessage = noMatrixMatchMessage;
13
+ exports.unknownMatrixKeys = unknownMatrixKeys;
9
14
  exports.topologicalJobOrder = topologicalJobOrder;
10
15
  const logger_1 = require("../utils/logger");
11
16
  const expression_1 = require("./expression");
@@ -25,7 +30,7 @@ function sameValue(a, b) {
25
30
  /** `key: value, ...` for a matrix combination's job label. */
26
31
  function matrixLabel(values) {
27
32
  return Object.entries(values)
28
- .map(([k, v]) => `${k}: ${v && typeof v === 'object' ? JSON.stringify(v) : v}`)
33
+ .map(([k, v]) => `${k}: ${matrixValueText(v)}`)
29
34
  .join(', ');
30
35
  }
31
36
  /** A boolean setting that may be an expression (`continue-on-error`). */
@@ -97,6 +102,58 @@ function expandMatrix(rawMatrix, ctx) {
97
102
  combinations.push(...added);
98
103
  return combinations.length ? combinations : [{}];
99
104
  }
105
+ /**
106
+ * Parse repeatable `--matrix key:value` arguments. Splits on the first `:`
107
+ * only, so values may contain colons (`image:node:20-alpine`).
108
+ */
109
+ function parseMatrixFilter(pairs) {
110
+ const filter = {};
111
+ for (const pair of pairs === undefined ? [] : [pairs].flat()) {
112
+ const separator = pair.indexOf(':');
113
+ if (separator <= 0) {
114
+ throw new Error(`Invalid --matrix argument "${pair}": expected key:value`);
115
+ }
116
+ const key = pair.slice(0, separator);
117
+ const value = pair.slice(separator + 1);
118
+ const values = (filter[key] ?? (filter[key] = []));
119
+ if (!values.includes(value))
120
+ values.push(value);
121
+ }
122
+ return filter;
123
+ }
124
+ /** A matrix value as text, the way it appears in the job name. */
125
+ function matrixValueText(value) {
126
+ return value && typeof value === 'object'
127
+ ? JSON.stringify(value)
128
+ : String(value);
129
+ }
130
+ /**
131
+ * Keep the combinations whose value for every filter key the matrix
132
+ * defines is one of the accepted values (compared as text). Keys the
133
+ * matrix doesn't define don't apply, so jobs without a matrix are
134
+ * unaffected; a combination lacking an applied key doesn't match.
135
+ */
136
+ function filterMatrix(combinations, filter) {
137
+ const keys = Object.keys(filter).filter((key) => combinations.some((combo) => key in combo));
138
+ const matches = combinations
139
+ .map((values, index) => ({ index, values }))
140
+ .filter(({ values }) => keys.every((key) => key in values && filter[key].includes(matrixValueText(values[key]))));
141
+ return { keys, matches };
142
+ }
143
+ /** The warning for a matrix job none of whose combinations match. */
144
+ function noMatrixMatchMessage(jobId, filter, combinations) {
145
+ const wanted = Object.entries(filter)
146
+ .flatMap(([key, values]) => values.map((value) => `${key}:${value}`))
147
+ .join(', ');
148
+ return [
149
+ `No matrix combination of job "${jobId}" matches --matrix ${wanted}; skipping it. Available combinations:`,
150
+ ...combinations.map((combo) => ` ${matrixLabel(combo)}`)
151
+ ].join('\n');
152
+ }
153
+ /** Filter keys that no expanded matrix defined. */
154
+ function unknownMatrixKeys(filter, definedKeys) {
155
+ return Object.keys(filter).filter((key) => !definedKeys.has(key));
156
+ }
100
157
  /**
101
158
  * Order job IDs so dependencies (via `needs`) run before dependents, using
102
159
  * Kahn's algorithm. Falls back to declaration order (with a warning) if a
@@ -95,12 +95,11 @@ function containerPathPrefix(runtime) {
95
95
  }
96
96
  /**
97
97
  * Create the per-run scratch directory holding the event payload
98
- * (`$GITHUB_EVENT_PATH`) and `$RUNNER_TEMP`.
98
+ * (`$GITHUB_EVENT_PATH`) and each job's `$RUNNER_TEMP`.
99
99
  */
100
100
  function createRunDir() {
101
101
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'secureci-run-'));
102
102
  fs.writeFileSync(path.join(dir, 'event.json'), '{}');
103
- fs.mkdirSync(path.join(dir, 'tmp'));
104
103
  return dir;
105
104
  }
106
105
  /** The `runner` context, derived from the runner env. */
@@ -134,7 +133,8 @@ function createJobRuntime(github, runDir, defaults = {}) {
134
133
  runnerEnv: (0, runner_env_1.buildRunnerEnv)(github, {
135
134
  workspace: process.cwd(),
136
135
  eventPath,
137
- tempDir: path.join(runDir, 'tmp'),
136
+ // Per job, so concurrent jobs don't share $RUNNER_TEMP.
137
+ tempDir: fs.mkdtempSync(path.join(runDir, 'tmp-')),
138
138
  toolCache: path.join(homeDir, '.secureci', 'toolcache')
139
139
  }),
140
140
  eventPath,
@@ -45,11 +45,14 @@ const container_runtime_1 = require("./container-runtime");
45
45
  const runner_env_1 = require("./runner-env");
46
46
  const job_runtime_1 = require("./job-runtime");
47
47
  const job_container_1 = require("./job-container");
48
+ const node_binary_1 = require("./node-binary");
48
49
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
49
50
  /**
50
51
  * Execute a JavaScript action (`runs.using: node*`): `pre` (if any) and
51
52
  * `main` now, with `post` queued on the job runtime to run at job end.
52
- * Scripts run with the CLI's own Node binary, in the workspace, with the
53
+ * Scripts run with the CLI's own Node binary (in the standalone binary:
54
+ * `node` from the step's PATH; in a job container: the image's `node`),
55
+ * in the workspace, with the
53
56
  * runner's default env, the caller's env, `INPUT_*`, `STATE_*` and the
54
57
  * file-command variables. Note: GitHub runs every `pre` at job start;
55
58
  * here it runs immediately before its own `main`.
@@ -134,8 +137,13 @@ async function executeNodeAction(action, actionPath, step, actionInputs, callerC
134
137
  : `node exited with code ${result.exitCode}`
135
138
  };
136
139
  }
140
+ const hostPath = (0, runner_env_1.prependPath)(runtime.path, callerCtx.env.PATH ?? process.env.PATH);
141
+ const node = (0, node_binary_1.hostNodeBinary)({ sea: (0, node_binary_1.isSea)(), pathEnv: hostPath });
142
+ if ('error' in node) {
143
+ return { success: false, stdout: '', stderr: '', error: node.error };
144
+ }
137
145
  try {
138
- const { stdout, stderr } = await execFileAsync(process.execPath, [path.join(actionDir, script)], {
146
+ const { stdout, stderr } = await execFileAsync(node.path, [path.join(actionDir, script)], {
139
147
  cwd: process.cwd(),
140
148
  maxBuffer: 64 * 1024 * 1024,
141
149
  timeout: limit.ms,
@@ -148,7 +156,7 @@ async function executeNodeAction(action, actionPath, step, actionInputs, callerC
148
156
  ...inputEnv,
149
157
  ...stateEnv,
150
158
  GITHUB_ACTION_PATH: actionDir,
151
- PATH: (0, runner_env_1.prependPath)(runtime.path, callerCtx.env.PATH ?? process.env.PATH),
159
+ PATH: hostPath,
152
160
  ...(0, job_runtime_1.fileCommandEnv)(files)
153
161
  }
154
162
  });
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NODE_ON_PATH_REQUIRED = void 0;
37
+ exports.isSea = isSea;
38
+ exports.findOnPath = findOnPath;
39
+ exports.hostNodeBinary = hostNodeBinary;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ exports.NODE_ON_PATH_REQUIRED = 'JavaScript actions need Node.js on PATH when using the standalone binary. Install Node.js (or add a setup-node step), or install the CLI with npm.';
43
+ /**
44
+ * Whether the CLI is running as a Node.js single executable application
45
+ * (the standalone binary). `node:sea` only exists on Node 20.12+/21.7+;
46
+ * older versions can't be a SEA, so a missing module means "no".
47
+ */
48
+ function isSea(
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ load = () => require('node:sea')) {
51
+ try {
52
+ return load().isSea();
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * The first executable `name` in `pathEnv` (like `command -v`). On Windows
60
+ * the separator is `;` and each PATHEXT extension is tried too. `exclude`
61
+ * skips one file (the standalone binary itself).
62
+ */
63
+ function findOnPath(name, pathEnv, { platform = process.platform, pathExt = process.env.PATHEXT, exclude } = {}) {
64
+ if (!pathEnv)
65
+ return undefined;
66
+ const windows = platform === 'win32';
67
+ const exts = windows
68
+ ? ['', ...(pathExt || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
69
+ : [''];
70
+ for (const dir of pathEnv.split(windows ? ';' : ':')) {
71
+ if (!dir)
72
+ continue;
73
+ for (const ext of exts) {
74
+ const candidate = path.join(dir, name + ext);
75
+ if (exclude && path.resolve(candidate) === path.resolve(exclude)) {
76
+ continue;
77
+ }
78
+ if (isExecutableFile(candidate, windows))
79
+ return candidate;
80
+ }
81
+ }
82
+ return undefined;
83
+ }
84
+ function isExecutableFile(file, windows) {
85
+ try {
86
+ if (!fs.statSync(file).isFile())
87
+ return false;
88
+ if (!windows)
89
+ fs.accessSync(file, fs.constants.X_OK);
90
+ return true;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * The `node` that runs JavaScript actions on the host. Normally the CLI's
98
+ * own Node binary; inside the standalone binary `process.execPath` is the
99
+ * CLI itself, so use `node` from the step's PATH instead (which also picks
100
+ * up a Node.js that setup-node added to the job's PATH).
101
+ */
102
+ function hostNodeBinary({ sea = isSea(), execPath = process.execPath, pathEnv, platform = process.platform }) {
103
+ if (!sea)
104
+ return { path: execPath };
105
+ const node = findOnPath('node', pathEnv, { platform, exclude: execPath });
106
+ return node ? { path: node } : { error: exports.NODE_ON_PATH_REQUIRED };
107
+ }
108
+ //# sourceMappingURL=node-binary.js.map
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Slots = void 0;
37
+ exports.defaultParallelism = defaultParallelism;
38
+ exports.parseParallel = parseParallel;
39
+ exports.runMatrix = runMatrix;
40
+ exports.runJobGraph = runJobGraph;
41
+ const os = __importStar(require("os"));
42
+ /** A FIFO counting semaphore: at most `limit` holders at a time. */
43
+ class Slots {
44
+ constructor(limit) {
45
+ this.active = 0;
46
+ this.waiting = [];
47
+ this.limit = Math.max(1, Math.floor(limit) || 1);
48
+ }
49
+ /** Wait for a free slot; call the returned function to free it. */
50
+ async acquire() {
51
+ if (this.active < this.limit) {
52
+ this.active++;
53
+ }
54
+ else {
55
+ // The releasing holder hands its slot straight to us.
56
+ await new Promise((resolve) => this.waiting.push(resolve));
57
+ }
58
+ let released = false;
59
+ return () => {
60
+ if (released)
61
+ return;
62
+ released = true;
63
+ const next = this.waiting.shift();
64
+ if (next)
65
+ next();
66
+ else
67
+ this.active--;
68
+ };
69
+ }
70
+ }
71
+ exports.Slots = Slots;
72
+ /** The machine's CPU count (the default for `--parallel`), at least 1. */
73
+ function defaultParallelism(osApi = os) {
74
+ const n = osApi.availableParallelism
75
+ ? osApi.availableParallelism()
76
+ : (osApi.cpus?.().length ?? 1);
77
+ return Math.max(1, n);
78
+ }
79
+ /** Parse `--parallel` / config `parallel`. Throws unless a positive integer. */
80
+ function parseParallel(raw) {
81
+ const n = typeof raw === 'number' ? raw : raw.trim() ? Number(raw) : NaN;
82
+ if (!Number.isInteger(n) || n < 1) {
83
+ throw new Error(`Invalid value "${raw}" for parallel. Must be a positive integer.`);
84
+ }
85
+ return n;
86
+ }
87
+ /**
88
+ * Run `count` matrix combinations, at most `maxParallel` at once and each
89
+ * holding one of the global `slots` while it runs. With fail-fast, a
90
+ * failure (not allowed by continue-on-error) aborts the signals of the
91
+ * combinations in progress and starts no more, as GitHub cancels the rest
92
+ * of the matrix. Returns each combination's outcome by index, or
93
+ * undefined for one that never started.
94
+ */
95
+ async function runMatrix(count, run, options) {
96
+ const outcomes = new Array(count).fill(undefined);
97
+ const local = new Slots(options.maxParallel ?? count);
98
+ const controllers = new Map();
99
+ let stopped = options.signal?.aborted ?? false;
100
+ let finished = 0;
101
+ const onParentAbort = () => {
102
+ stopped = true;
103
+ for (const controller of controllers.values())
104
+ controller.abort();
105
+ };
106
+ options.signal?.addEventListener('abort', onParentAbort);
107
+ const runOne = async (index) => {
108
+ const releaseLocal = await local.acquire();
109
+ try {
110
+ if (stopped)
111
+ return;
112
+ const releaseGlobal = options.slots
113
+ ? await options.slots.acquire()
114
+ : undefined;
115
+ try {
116
+ if (stopped)
117
+ return;
118
+ const controller = new AbortController();
119
+ controllers.set(index, controller);
120
+ try {
121
+ const outcome = await run(index, controller.signal);
122
+ outcomes[index] = outcome;
123
+ finished++;
124
+ if (options.failFast &&
125
+ !stopped &&
126
+ outcome.result === 'failure' &&
127
+ !outcome.allowedFailure) {
128
+ stopped = true;
129
+ if (finished < count)
130
+ options.onFailFast?.();
131
+ for (const other of controllers.values()) {
132
+ if (other !== controller)
133
+ other.abort();
134
+ }
135
+ }
136
+ }
137
+ finally {
138
+ controllers.delete(index);
139
+ }
140
+ }
141
+ finally {
142
+ releaseGlobal?.();
143
+ }
144
+ }
145
+ finally {
146
+ releaseLocal();
147
+ }
148
+ };
149
+ try {
150
+ const settled = await Promise.allSettled(Array.from({ length: count }, (_, i) => runOne(i)));
151
+ const failed = settled.find((s) => s.status === 'rejected');
152
+ if (failed)
153
+ throw failed.reason;
154
+ return outcomes;
155
+ }
156
+ finally {
157
+ options.signal?.removeEventListener('abort', onParentAbort);
158
+ }
159
+ }
160
+ /** A job not started because another job threw. */
161
+ class SkippedJob extends Error {
162
+ }
163
+ /**
164
+ * Run the jobs of a workflow. `order` is a topological order of the job
165
+ * ids; a job waits only for its `needs:` that come earlier in it (so a
166
+ * cycle, or an unknown job, doesn't deadlock) and gets their results.
167
+ * Serially, jobs run one after another in `order`; otherwise each starts
168
+ * as soon as its dependencies are done. If a job throws, no further jobs
169
+ * start, running ones finish, and the first error is rethrown.
170
+ */
171
+ async function runJobGraph(order, needsOf, runJob, concurrent) {
172
+ const results = new Map();
173
+ const position = new Map(order.map((id, i) => [id, i]));
174
+ const depsOf = (id) => needsOf(id).filter((dep) => (position.get(dep) ?? Infinity) < position.get(id));
175
+ const needsFor = (id) => Object.fromEntries(depsOf(id).map((dep) => [dep, results.get(dep)]));
176
+ if (!concurrent) {
177
+ for (const id of order) {
178
+ results.set(id, await runJob(id, needsFor(id)));
179
+ }
180
+ return results;
181
+ }
182
+ let broken = false;
183
+ const done = new Map();
184
+ for (const id of order) {
185
+ const deps = depsOf(id).map((dep) => done.get(dep));
186
+ done.set(id, Promise.all(deps).then(async () => {
187
+ if (broken)
188
+ throw new SkippedJob();
189
+ try {
190
+ results.set(id, await runJob(id, needsFor(id)));
191
+ }
192
+ catch (error) {
193
+ broken = true;
194
+ throw error;
195
+ }
196
+ }, () => {
197
+ throw new SkippedJob();
198
+ }));
199
+ }
200
+ const settled = await Promise.allSettled(done.values());
201
+ // The first job that failed itself, not one that never started.
202
+ const firstError = settled.find((s) => s.status === 'rejected' && !(s.reason instanceof SkippedJob));
203
+ if (firstError)
204
+ throw firstError.reason;
205
+ return results;
206
+ }
207
+ //# sourceMappingURL=scheduler.js.map