@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.
- package/README.md +74 -15
- package/dist/advisories.js +603 -0
- package/dist/cli.js +263 -38
- package/dist/config.js +23 -1
- package/dist/init.js +12 -5
- package/dist/parser/allow-comments.js +113 -0
- package/dist/parser/dangerous-triggers.js +163 -0
- package/dist/parser/injection.js +281 -0
- package/dist/parser/permissions.js +145 -0
- package/dist/parser/pipeline-contract-parser.js +70 -0
- package/dist/parser/pipeline-contract-validator.js +18 -4
- package/dist/parser/validator.js +61 -1
- package/dist/pin.js +311 -0
- package/dist/report.js +9 -2
- package/dist/runner/action-fetcher.js +100 -4
- package/dist/runner/builtin-actions.js +13 -3
- package/dist/runner/dry-run.js +57 -12
- package/dist/runner/executor.js +368 -254
- package/dist/runner/expression.js +106 -8
- package/dist/runner/job-plan.js +58 -1
- package/dist/runner/job-runtime.js +3 -3
- package/dist/runner/node-action.js +11 -3
- package/dist/runner/node-binary.js +108 -0
- package/dist/runner/scheduler.js +207 -0
- package/dist/runner/steps.js +54 -2
- package/dist/utils/env-file.js +185 -0
- package/dist/utils/logger.js +36 -7
- package/dist/version.js +56 -0
- package/package.json +3 -1
|
@@ -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
|
-
/**
|
|
346
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
495
|
+
value = this.access(value, STAR);
|
|
437
496
|
}
|
|
438
497
|
else if (part.type === 'ident') {
|
|
439
|
-
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 =
|
|
508
|
+
value = this.access(value, STAR);
|
|
450
509
|
}
|
|
451
510
|
else {
|
|
452
511
|
const key = unwrap(this.parseOr());
|
|
453
|
-
value =
|
|
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
|
package/dist/runner/job-plan.js
CHANGED
|
@@ -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}: ${
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|