@secureci/cli 1.0.0 → 1.1.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 +36 -15
- package/dist/advisories.js +33 -6
- package/dist/cli.js +103 -4
- package/dist/config.js +12 -1
- package/dist/inventory.js +52 -0
- package/dist/org-scan-script.js +6 -0
- package/dist/org-scan.js +88 -0
- package/dist/parser/pipeline-contract-validator.js +56 -25
- package/dist/runner/action-fetcher.js +6 -1
- package/dist/runner/actions.js +7 -7
- package/dist/runner/checkout.js +124 -8
- package/dist/runner/container-policy.js +32 -0
- package/dist/runner/docker-action.js +1 -1
- package/dist/runner/executor.js +28 -6
- package/dist/runner/expression.js +125 -3
- package/dist/runner/job-container.js +57 -11
- package/dist/runner/job-runtime.js +4 -3
- package/dist/runner/node-action.js +1 -1
- package/dist/runner/runner-env.js +20 -0
- package/dist/runner/steps.js +2 -2
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ const repository_1 = require("../utils/repository");
|
|
|
43
43
|
const action_fetcher_1 = require("../runner/action-fetcher");
|
|
44
44
|
const github_lockfile_1 = require("../github-lockfile");
|
|
45
45
|
const permissions_1 = require("./permissions");
|
|
46
|
+
const expression_1 = require("../runner/expression");
|
|
46
47
|
/** Maximum nesting of composite actions / reusable workflows. */
|
|
47
48
|
const MAX_DEPTH = 10;
|
|
48
49
|
/**
|
|
@@ -81,8 +82,17 @@ async function validatePipelineContract(parsed, spec, options = {}) {
|
|
|
81
82
|
(spec.required ?? []).forEach((entry, idx) => {
|
|
82
83
|
const path = `required[${idx}]`;
|
|
83
84
|
const name = normalizeActionName(entry.uses);
|
|
84
|
-
const
|
|
85
|
-
|
|
85
|
+
const found = sequence.filter((u) => u.name === name);
|
|
86
|
+
const candidates = found.filter((u) => u.runs !== 'never');
|
|
87
|
+
if (found.length > 0 && candidates.length === 0) {
|
|
88
|
+
errors.push({
|
|
89
|
+
path,
|
|
90
|
+
message: `Required action "${entry.uses}" never runs: its if: conditions (on its step or job) can't be true for this workflow's triggers (${(graph.events ?? []).join(', ')})`,
|
|
91
|
+
severity: 'error'
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (candidates.length > 0 && candidates.every((c) => c.runs === 'maybe')) {
|
|
86
96
|
errors.push({
|
|
87
97
|
path,
|
|
88
98
|
message: `Required action "${entry.uses}" only runs under an if: condition (on its step or job), so it may be skipped`,
|
|
@@ -275,6 +285,20 @@ function needsTransitively(nodes, from, to) {
|
|
|
275
285
|
async function buildGraph(parsed, recurse, offline) {
|
|
276
286
|
const graph = { sequence: [], nodes: new Map(), errors: [] };
|
|
277
287
|
const root = (0, repository_1.repositoryRoot)(parsed.path);
|
|
288
|
+
// `if:` conditions are decided, where they can be, for each trigger of
|
|
289
|
+
// the workflow (called workflows run with their caller's event). With
|
|
290
|
+
// workflow_call, the event is the caller's, so it isn't known.
|
|
291
|
+
graph.events = triggerEvents(parsed.file.on);
|
|
292
|
+
const known = graph.events
|
|
293
|
+
? graph.events.map((event) => ({ github: { event_name: event } }))
|
|
294
|
+
: [{}];
|
|
295
|
+
/** `runs`, narrowed by a job or step's own `if:`. */
|
|
296
|
+
const within = (runs, condition) => runs.map((outer, i) => {
|
|
297
|
+
const own = (0, expression_1.staticCondition)(condition, known[i]);
|
|
298
|
+
if (outer === false || own === false)
|
|
299
|
+
return false;
|
|
300
|
+
return outer === true && own === true ? true : undefined;
|
|
301
|
+
});
|
|
278
302
|
const isLocal = (uses) => uses.startsWith('./') || uses.startsWith('../');
|
|
279
303
|
/**
|
|
280
304
|
* The file behind a composite action / reusable workflow reference, or
|
|
@@ -355,31 +379,35 @@ async function buildGraph(parsed, recurse, offline) {
|
|
|
355
379
|
}
|
|
356
380
|
return true;
|
|
357
381
|
};
|
|
358
|
-
const addUsage = (node, index, at, uses,
|
|
382
|
+
const addUsage = (node, index, at, uses, runs) => {
|
|
359
383
|
graph.sequence.push({
|
|
360
384
|
node,
|
|
361
385
|
index,
|
|
362
386
|
path: at,
|
|
363
|
-
|
|
387
|
+
runs: runs.every((r) => r === false)
|
|
388
|
+
? 'never'
|
|
389
|
+
: runs.every((r) => r === true)
|
|
390
|
+
? 'always'
|
|
391
|
+
: 'maybe',
|
|
364
392
|
...splitActionRef(uses)
|
|
365
393
|
});
|
|
366
394
|
};
|
|
367
395
|
/** Flatten a job's steps (and composite action steps) into `node`. */
|
|
368
|
-
const addSteps = async (steps, node, at, counter, stack, workflowRoot,
|
|
396
|
+
const addSteps = async (steps, node, at, counter, stack, workflowRoot, runs) => {
|
|
369
397
|
for (const [i, step] of steps.entries()) {
|
|
370
398
|
const stepPath = `${at}steps[${i}]`;
|
|
371
399
|
if (!step.uses) {
|
|
372
400
|
counter.next++;
|
|
373
401
|
continue;
|
|
374
402
|
}
|
|
375
|
-
const
|
|
376
|
-
addUsage(node, counter.next++, stepPath, step.uses,
|
|
403
|
+
const stepRuns = within(runs, step.if);
|
|
404
|
+
addUsage(node, counter.next++, stepPath, step.uses, stepRuns);
|
|
377
405
|
const file = await resolveRef(step.uses, 'action', workflowRoot, stepPath);
|
|
378
406
|
if (!file || !canEnter(file, stack, stepPath))
|
|
379
407
|
continue;
|
|
380
408
|
const action = parse(file, stepPath);
|
|
381
409
|
if (action?.type === 'action' && action.file.runs.using === 'composite') {
|
|
382
|
-
await addSteps(action.file.runs.steps ?? [], node, `${stepPath} > ${step.uses} > `, counter, [...stack, file], workflowRoot,
|
|
410
|
+
await addSteps(action.file.runs.steps ?? [], node, `${stepPath} > ${step.uses} > `, counter, [...stack, file], workflowRoot, stepRuns);
|
|
383
411
|
}
|
|
384
412
|
}
|
|
385
413
|
};
|
|
@@ -388,7 +416,7 @@ async function buildGraph(parsed, recurse, offline) {
|
|
|
388
416
|
* reusable-workflow call itself needs, plus the call). Returns the ids
|
|
389
417
|
* of the added jobs.
|
|
390
418
|
*/
|
|
391
|
-
const addWorkflow = async (workflow, workflowRoot, prefix, labelPrefix, at, after, stack,
|
|
419
|
+
const addWorkflow = async (workflow, workflowRoot, prefix, labelPrefix, at, after, stack, runs) => {
|
|
392
420
|
const added = [];
|
|
393
421
|
// A job id -> the graph nodes standing for it (a reusable-workflow call
|
|
394
422
|
// stands for the call and all of its called jobs).
|
|
@@ -408,17 +436,17 @@ async function buildGraph(parsed, recurse, offline) {
|
|
|
408
436
|
});
|
|
409
437
|
added.push(node);
|
|
410
438
|
const jobPath = `${at}jobs.${jobId}`;
|
|
411
|
-
const
|
|
439
|
+
const jobRuns = within(runs, job.if);
|
|
412
440
|
if (job.uses) {
|
|
413
441
|
// The call itself is a one-step "job" its called jobs run after.
|
|
414
|
-
addUsage(node, 0, jobPath, job.uses,
|
|
442
|
+
addUsage(node, 0, jobPath, job.uses, jobRuns);
|
|
415
443
|
graph.nodes.get(node).length = 1;
|
|
416
444
|
const ids = [node];
|
|
417
445
|
const file = await resolveRef(job.uses, 'workflow', workflowRoot, jobPath);
|
|
418
446
|
if (file && canEnter(file, stack, jobPath)) {
|
|
419
447
|
const called = parse(file, jobPath);
|
|
420
448
|
if (called?.type === 'workflow') {
|
|
421
|
-
ids.push(...(await addWorkflow(called.file, (0, repository_1.repositoryRoot)(file), `${node}>`, `${labelPrefix}${jobId} > ${job.uses} > `, `${jobPath} > ${job.uses} > `, [...needs, node], [...stack, file],
|
|
449
|
+
ids.push(...(await addWorkflow(called.file, (0, repository_1.repositoryRoot)(file), `${node}>`, `${labelPrefix}${jobId} > ${job.uses} > `, `${jobPath} > ${job.uses} > `, [...needs, node], [...stack, file], jobRuns)));
|
|
422
450
|
}
|
|
423
451
|
}
|
|
424
452
|
alias.set(jobId, ids);
|
|
@@ -427,27 +455,30 @@ async function buildGraph(parsed, recurse, offline) {
|
|
|
427
455
|
}
|
|
428
456
|
alias.set(jobId, [node]);
|
|
429
457
|
const counter = { next: 0 };
|
|
430
|
-
await addSteps(job.steps ?? [], node, `${jobPath}.`, counter, stack, workflowRoot,
|
|
458
|
+
await addSteps(job.steps ?? [], node, `${jobPath}.`, counter, stack, workflowRoot, jobRuns);
|
|
431
459
|
graph.nodes.get(node).length = counter.next;
|
|
432
460
|
}
|
|
433
461
|
return added;
|
|
434
462
|
};
|
|
435
|
-
await addWorkflow(parsed.file, root, '', '', '', [], [path.resolve(parsed.path)],
|
|
463
|
+
await addWorkflow(parsed.file, root, '', '', '', [], [path.resolve(parsed.path)], known.map(() => true));
|
|
436
464
|
return graph;
|
|
437
465
|
}
|
|
438
466
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
467
|
+
* The events that trigger a workflow (`on:` as a string, list or map), or
|
|
468
|
+
* undefined when the event isn't known: none are listed, or the workflow
|
|
469
|
+
* is reusable (it runs with its caller's event).
|
|
441
470
|
*/
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return
|
|
471
|
+
function triggerEvents(on) {
|
|
472
|
+
const events = typeof on === 'string'
|
|
473
|
+
? [on]
|
|
474
|
+
: Array.isArray(on)
|
|
475
|
+
? on.map(String)
|
|
476
|
+
: on && typeof on === 'object'
|
|
477
|
+
? Object.keys(on)
|
|
478
|
+
: [];
|
|
479
|
+
return events.length === 0 || events.includes('workflow_call')
|
|
480
|
+
? undefined
|
|
481
|
+
: events;
|
|
451
482
|
}
|
|
452
483
|
function splitActionRef(uses) {
|
|
453
484
|
const atIndex = uses.lastIndexOf('@');
|
|
@@ -43,6 +43,7 @@ exports.lsRemoteSha = lsRemoteSha;
|
|
|
43
43
|
exports.parseLsRemoteTags = parseLsRemoteTags;
|
|
44
44
|
exports.lsRemoteTags = lsRemoteTags;
|
|
45
45
|
exports.getText = getText;
|
|
46
|
+
exports.getResponse = getResponse;
|
|
46
47
|
exports.followRedirect = followRedirect;
|
|
47
48
|
exports.fetchAction = fetchAction;
|
|
48
49
|
exports.fetchRepoFile = fetchRepoFile;
|
|
@@ -222,6 +223,10 @@ async function lsRemoteTags(url) {
|
|
|
222
223
|
* with an error carrying `status`.
|
|
223
224
|
*/
|
|
224
225
|
function getText(url, headers, timeoutMs) {
|
|
226
|
+
return getResponse(url, headers, timeoutMs).then((r) => r.body);
|
|
227
|
+
}
|
|
228
|
+
/** getText, with the response headers (e.g. `link` for pagination). */
|
|
229
|
+
function getResponse(url, headers, timeoutMs) {
|
|
225
230
|
return new Promise((resolve, reject) => {
|
|
226
231
|
const client = url.startsWith('http://') ? http : https;
|
|
227
232
|
const request = client.get(url, { headers }, (response) => {
|
|
@@ -237,7 +242,7 @@ function getText(url, headers, timeoutMs) {
|
|
|
237
242
|
body += chunk;
|
|
238
243
|
});
|
|
239
244
|
response.on('end', () => response.statusCode === 200
|
|
240
|
-
? resolve(body)
|
|
245
|
+
? resolve({ body, headers: response.headers })
|
|
241
246
|
: reject(Object.assign(new Error(`HTTP ${response.statusCode}`), {
|
|
242
247
|
status: response.statusCode
|
|
243
248
|
})));
|
package/dist/runner/actions.js
CHANGED
|
@@ -93,7 +93,7 @@ function warnUnsandboxedActions(steps, ctx, options) {
|
|
|
93
93
|
async function stepFileSystem(runtime) {
|
|
94
94
|
const container = runtime.container;
|
|
95
95
|
if (!container)
|
|
96
|
-
return (0, builtin_actions_1.hostFileSystem)(
|
|
96
|
+
return (0, builtin_actions_1.hostFileSystem)(runtime.workspace);
|
|
97
97
|
if (container.home === undefined) {
|
|
98
98
|
const probe = await container.runtime.exec(container.id, {
|
|
99
99
|
command: 'printf %s "$HOME"'
|
|
@@ -102,7 +102,7 @@ async function stepFileSystem(runtime) {
|
|
|
102
102
|
}
|
|
103
103
|
return (0, builtin_actions_1.containerFileSystem)({
|
|
104
104
|
workspace: container_runtime_1.CONTAINER_WORKDIR,
|
|
105
|
-
hostWorkspace:
|
|
105
|
+
hostWorkspace: runtime.workspace,
|
|
106
106
|
home: container.home,
|
|
107
107
|
copyFrom: (from, to) => container.runtime.copyFrom(container.id, from, to),
|
|
108
108
|
copyTo: (from, to) => container.runtime.copyTo(container.id, from, to),
|
|
@@ -259,8 +259,8 @@ function isLocalActionRef(uses) {
|
|
|
259
259
|
* Resolve a local action (`uses: ./path/to/action`) relative to the
|
|
260
260
|
* workspace, like the runner does, to its action.yml / action.yaml.
|
|
261
261
|
*/
|
|
262
|
-
function resolveLocalAction(uses) {
|
|
263
|
-
const dir = path.resolve(
|
|
262
|
+
function resolveLocalAction(uses, workspace = process.cwd()) {
|
|
263
|
+
const dir = path.resolve(workspace, uses);
|
|
264
264
|
for (const name of ['action.yml', 'action.yaml']) {
|
|
265
265
|
const candidate = path.join(dir, name);
|
|
266
266
|
if (fs.existsSync(candidate))
|
|
@@ -297,7 +297,7 @@ async function executeAction(step, options, startTime, runtime, ctx) {
|
|
|
297
297
|
args: withValues.args ? (0, shell_1.splitArgs)(withValues.args) : []
|
|
298
298
|
}
|
|
299
299
|
};
|
|
300
|
-
return await (0, docker_action_1.executeDockerAction)(dockerAction, path.join(
|
|
300
|
+
return await (0, docker_action_1.executeDockerAction)(dockerAction, path.join(runtime.workspace, 'action.yml'), step, {}, callerCtx.env, options, runtime, startTime);
|
|
301
301
|
}
|
|
302
302
|
if (options.localCheckout !== false &&
|
|
303
303
|
(0, checkout_1.isLocalCheckout)(step.uses, step.with, callerCtx)) {
|
|
@@ -308,11 +308,11 @@ async function executeAction(step, options, startTime, runtime, ctx) {
|
|
|
308
308
|
return await runBuiltinAction(builtin, step, callerCtx, options, runtime, startTime);
|
|
309
309
|
}
|
|
310
310
|
const actionPath = isLocalActionRef(step.uses)
|
|
311
|
-
? resolveLocalAction(step.uses)
|
|
311
|
+
? resolveLocalAction(step.uses, runtime.workspace)
|
|
312
312
|
: await fetchActionOnce(step.uses, options, runtime);
|
|
313
313
|
if (!actionPath) {
|
|
314
314
|
const error = `Could not resolve action "${step.uses}"${isLocalActionRef(step.uses)
|
|
315
|
-
? ` (no action.yml or action.yaml in ${path.resolve(
|
|
315
|
+
? ` (no action.yml or action.yaml in ${path.resolve(runtime.workspace, step.uses)})`
|
|
316
316
|
: ''}`;
|
|
317
317
|
logger_1.logger.error(error);
|
|
318
318
|
return {
|
package/dist/runner/checkout.js
CHANGED
|
@@ -34,10 +34,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.copyWorkingCopy = copyWorkingCopy;
|
|
37
|
+
exports.createIsolatedWorkspace = createIsolatedWorkspace;
|
|
38
|
+
exports.removeIsolatedWorkspace = removeIsolatedWorkspace;
|
|
37
39
|
exports.isLocalCheckout = isLocalCheckout;
|
|
38
40
|
exports.localCheckout = localCheckout;
|
|
39
41
|
const fs = __importStar(require("fs"));
|
|
40
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
41
44
|
const logger_1 = require("../utils/logger");
|
|
42
45
|
const expression_1 = require("./expression");
|
|
43
46
|
/** Marks a `path:` copy made by localCheckout, so it may be replaced. */
|
|
@@ -64,10 +67,34 @@ function copyWorkingCopy(srcDir, dstDir, exclude) {
|
|
|
64
67
|
fs.cpSync(src, dst, {
|
|
65
68
|
recursive: true,
|
|
66
69
|
verbatimSymlinks: true,
|
|
70
|
+
// Copy-on-write clones where the file system supports them.
|
|
71
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
67
72
|
filter: (file) => !fs.existsSync(path.join(file, CHECKOUT_MARKER))
|
|
68
73
|
});
|
|
69
74
|
}
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* A copy of the working copy (uncommitted changes and .git included) for
|
|
78
|
+
* one job, in `parentDir`, for --isolate-workspace. Remove it with
|
|
79
|
+
* removeIsolatedWorkspace.
|
|
80
|
+
*/
|
|
81
|
+
function createIsolatedWorkspace(workingCopy, parentDir) {
|
|
82
|
+
const dir = fs.mkdtempSync(path.join(parentDir, 'workspace-'));
|
|
83
|
+
copyWorkingCopy(workingCopy, dir, dir);
|
|
84
|
+
return dir;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove a job's isolated workspace. Best effort: files a container step
|
|
88
|
+
* created as another user may not be removable, which is only warned about.
|
|
89
|
+
*/
|
|
90
|
+
function removeIsolatedWorkspace(dir) {
|
|
91
|
+
try {
|
|
92
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
logger_1.logger.warn(`Couldn't remove the job's workspace copy ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
71
98
|
/**
|
|
72
99
|
* Whether a step is `actions/checkout` of the repository being run (no
|
|
73
100
|
* `repository:` input, or the current one in any case), which is served
|
|
@@ -101,6 +128,54 @@ function escapesViaSymlink(workspace, dest) {
|
|
|
101
128
|
const real = fs.realpathSync(existing);
|
|
102
129
|
return real !== root && !real.startsWith(root + path.sep);
|
|
103
130
|
}
|
|
131
|
+
/** Run git in `cwd` on the host; undefined when it fails. */
|
|
132
|
+
function gitOutput(cwd, args) {
|
|
133
|
+
try {
|
|
134
|
+
return (0, child_process_1.execFileSync)('git', args, {
|
|
135
|
+
cwd,
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
138
|
+
}).trim();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The commit `ref` names in the local repository (as given, else as a
|
|
146
|
+
* branch of `origin`), or undefined. A ref that looks like an option is
|
|
147
|
+
* never passed to git.
|
|
148
|
+
*/
|
|
149
|
+
function resolveLocalRef(workspace, ref) {
|
|
150
|
+
if (ref.startsWith('-'))
|
|
151
|
+
return undefined;
|
|
152
|
+
for (const candidate of [ref, `refs/remotes/origin/${ref}`]) {
|
|
153
|
+
const sha = gitOutput(workspace, [
|
|
154
|
+
'rev-parse',
|
|
155
|
+
'--verify',
|
|
156
|
+
'--quiet',
|
|
157
|
+
'--end-of-options',
|
|
158
|
+
`${candidate}^{commit}`
|
|
159
|
+
]);
|
|
160
|
+
if (sha)
|
|
161
|
+
return sha;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Submodules of the working copy that aren't initialized (`git submodule
|
|
167
|
+
* status` marks them with "-"), by path.
|
|
168
|
+
*/
|
|
169
|
+
function uninitializedSubmodules(workspace) {
|
|
170
|
+
if (!fs.existsSync(path.join(workspace, '.gitmodules')))
|
|
171
|
+
return [];
|
|
172
|
+
const status = gitOutput(workspace, ['submodule', 'status', '--recursive']);
|
|
173
|
+
return (status ?? '')
|
|
174
|
+
.split('\n')
|
|
175
|
+
.filter((line) => line.startsWith('-'))
|
|
176
|
+
.map((line) => line.slice(1).split(' ')[1])
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
}
|
|
104
179
|
function localCheckout(step, ctx, runtime, startTime) {
|
|
105
180
|
const withValues = (0, expression_1.substituteRecord)(step.with, ctx);
|
|
106
181
|
const outputs = {
|
|
@@ -111,12 +186,7 @@ function localCheckout(step, ctx, runtime, startTime) {
|
|
|
111
186
|
logger_1.logger.error(error);
|
|
112
187
|
return { step, success: false, error, duration: Date.now() - startTime };
|
|
113
188
|
};
|
|
114
|
-
const
|
|
115
|
-
if (ref &&
|
|
116
|
-
![ctx.github.ref, ctx.github.ref_name, ctx.github.sha].includes(ref)) {
|
|
117
|
-
logger_1.logger.warn(`actions/checkout: ignoring ref "${ref}"; using the local working copy (pass --no-local-checkout to run the real action)`);
|
|
118
|
-
}
|
|
119
|
-
const workspace = process.cwd();
|
|
189
|
+
const workspace = runtime.workspace;
|
|
120
190
|
const dest = path.resolve(workspace, withValues.path || '.');
|
|
121
191
|
const relative = path.relative(workspace, dest);
|
|
122
192
|
if (relative.startsWith('..') ||
|
|
@@ -124,6 +194,28 @@ function localCheckout(step, ctx, runtime, startTime) {
|
|
|
124
194
|
escapesViaSymlink(workspace, dest)) {
|
|
125
195
|
return fail(`actions/checkout: path "${withValues.path}" is outside the workspace`);
|
|
126
196
|
}
|
|
197
|
+
// A ref other than the current one: check out that commit, when the local
|
|
198
|
+
// repository has it, into path: (never over the user's working copy).
|
|
199
|
+
const ref = withValues.ref;
|
|
200
|
+
let commit;
|
|
201
|
+
if (ref &&
|
|
202
|
+
![ctx.github.ref, ctx.github.ref_name, ctx.github.sha].includes(ref)) {
|
|
203
|
+
commit = resolveLocalRef(workspace, ref);
|
|
204
|
+
if (!commit) {
|
|
205
|
+
logger_1.logger.warn(`actions/checkout: ref "${ref}" isn't in the local repository; using the local working copy (fetch it, or pass --no-local-checkout to run the real action)`);
|
|
206
|
+
}
|
|
207
|
+
else if (relative === '') {
|
|
208
|
+
logger_1.logger.warn(`actions/checkout: not checking out ref "${ref}" over your working copy; add a path: to check it out there, or pass --no-local-checkout to run the real action`);
|
|
209
|
+
commit = undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const submodules = String(withValues.submodules ?? '').toLowerCase();
|
|
213
|
+
if (submodules === 'true' || submodules === 'recursive') {
|
|
214
|
+
const missing = uninitializedSubmodules(workspace);
|
|
215
|
+
if (missing.length > 0) {
|
|
216
|
+
logger_1.logger.warn(`actions/checkout: submodules aren't initialized in your working copy (${missing.join(', ')}); run \`git submodule update --init --recursive\` first`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
127
219
|
let output = 'Using local working copy';
|
|
128
220
|
if (relative !== '') {
|
|
129
221
|
if (fs.existsSync(dest)) {
|
|
@@ -134,9 +226,33 @@ function localCheckout(step, ctx, runtime, startTime) {
|
|
|
134
226
|
return fail(`actions/checkout: path "${withValues.path}" already exists in the working copy; remove it or pass --no-local-checkout`);
|
|
135
227
|
}
|
|
136
228
|
}
|
|
137
|
-
|
|
229
|
+
if (commit) {
|
|
230
|
+
// A local clone, sharing the repository's objects: nothing is fetched
|
|
231
|
+
// and the working copy's .git is only read.
|
|
232
|
+
const cloned = gitOutput(workspace, [
|
|
233
|
+
'clone',
|
|
234
|
+
'--quiet',
|
|
235
|
+
'--shared',
|
|
236
|
+
'--no-checkout',
|
|
237
|
+
'--',
|
|
238
|
+
workspace,
|
|
239
|
+
dest
|
|
240
|
+
]) !== undefined &&
|
|
241
|
+
gitOutput(dest, ['checkout', '--quiet', '--detach', commit]) !==
|
|
242
|
+
undefined;
|
|
243
|
+
if (!cloned) {
|
|
244
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
245
|
+
return fail(`actions/checkout: couldn't check out ref "${ref}" to "${withValues.path}"`);
|
|
246
|
+
}
|
|
247
|
+
outputs.ref = String(ref);
|
|
248
|
+
outputs.commit = commit;
|
|
249
|
+
output = `Checked out ${ref} (${commit.slice(0, 7)}) to ${relative}`;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
copyWorkingCopy(workspace, dest, dest);
|
|
253
|
+
output = `Using local working copy (copied to ${relative})`;
|
|
254
|
+
}
|
|
138
255
|
fs.writeFileSync(path.join(dest, CHECKOUT_MARKER), '');
|
|
139
|
-
output = `Using local working copy (copied to ${relative})`;
|
|
140
256
|
const postStep = { name: `Post ${step.name || step.uses}` };
|
|
141
257
|
runtime.postSteps.push({
|
|
142
258
|
step: postStep,
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.invalidImageReason = invalidImageReason;
|
|
37
37
|
exports.unsafeServiceOptions = unsafeServiceOptions;
|
|
38
|
+
exports.splitReservedOptions = splitReservedOptions;
|
|
38
39
|
exports.unsafeServiceVolumeReason = unsafeServiceVolumeReason;
|
|
39
40
|
const fs = __importStar(require("fs"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
@@ -125,6 +126,37 @@ function unsafeServiceOptions(args) {
|
|
|
125
126
|
}
|
|
126
127
|
return found;
|
|
127
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Split `args` into the options to pass on and the `reserved` ones (by
|
|
131
|
+
* flag name, in order), dropping a reserved option's value too. Values of
|
|
132
|
+
* other options are kept as they are, so `--health-cmd --entrypoint` stays.
|
|
133
|
+
*/
|
|
134
|
+
function splitReservedOptions(args, reserved) {
|
|
135
|
+
const kept = [];
|
|
136
|
+
const dropped = [];
|
|
137
|
+
for (let i = 0; i < args.length; i++) {
|
|
138
|
+
const arg = args[i];
|
|
139
|
+
let name;
|
|
140
|
+
let takesNext = false;
|
|
141
|
+
if (arg.startsWith('--')) {
|
|
142
|
+
const [flag, value] = arg.split(/=(.*)/s);
|
|
143
|
+
name = flag;
|
|
144
|
+
takesNext = value === undefined && VALUE_OPTIONS.has(flag);
|
|
145
|
+
}
|
|
146
|
+
else if (arg.startsWith('-') && arg.length > 1) {
|
|
147
|
+
name = arg.slice(0, 2);
|
|
148
|
+
takesNext = arg.length === 2 && SHORT_VALUE_OPTIONS.has(name);
|
|
149
|
+
}
|
|
150
|
+
const span = takesNext ? args.slice(i, i + 2) : [arg];
|
|
151
|
+
if (name !== undefined && reserved.has(name))
|
|
152
|
+
dropped.push(name);
|
|
153
|
+
else
|
|
154
|
+
kept.push(...span);
|
|
155
|
+
if (takesNext)
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
return { kept, dropped };
|
|
159
|
+
}
|
|
128
160
|
const VOLUME_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
129
161
|
function realpathOrSelf(p) {
|
|
130
162
|
try {
|
|
@@ -131,7 +131,7 @@ async function executeDockerAction(action, actionPath, step, actionInputs, calle
|
|
|
131
131
|
image,
|
|
132
132
|
args,
|
|
133
133
|
entrypoint: action.runs.entrypoint,
|
|
134
|
-
workingDir:
|
|
134
|
+
workingDir: runtime.workspace,
|
|
135
135
|
readOnlyPaths: (0, job_container_1.protectedWorkspacePaths)(options),
|
|
136
136
|
exchangeDir,
|
|
137
137
|
env: {
|
package/dist/runner/executor.js
CHANGED
|
@@ -49,6 +49,7 @@ const job_plan_1 = require("./job-plan");
|
|
|
49
49
|
const job_runtime_1 = require("./job-runtime");
|
|
50
50
|
const job_container_1 = require("./job-container");
|
|
51
51
|
const steps_1 = require("./steps");
|
|
52
|
+
const checkout_1 = require("./checkout");
|
|
52
53
|
const actions_1 = require("./actions");
|
|
53
54
|
const scheduler_1 = require("./scheduler");
|
|
54
55
|
// Public API, kept here so `executor` stays the entry point.
|
|
@@ -161,6 +162,15 @@ async function executeWorkflow(filePath, options = {}, internal = {}) {
|
|
|
161
162
|
return { success: false, jobs: [], duration: Date.now() - startTime };
|
|
162
163
|
}
|
|
163
164
|
logger_1.logger.info(`Executing workflow: ${parsed.file.name}`);
|
|
165
|
+
const concurrencyAt = [
|
|
166
|
+
...(parsed.file.concurrency ? ['workflow'] : []),
|
|
167
|
+
...Object.entries(parsed.file.jobs)
|
|
168
|
+
.filter(([, job]) => job.concurrency)
|
|
169
|
+
.map(([id]) => `jobs.${id}`)
|
|
170
|
+
];
|
|
171
|
+
if (concurrencyAt.length > 0) {
|
|
172
|
+
logger_1.logger.info(`concurrency: isn't applied in a local run (${concurrencyAt.join(', ')}): it doesn't queue behind or cancel other runs, and jobs run as needs: and --parallel allow`);
|
|
173
|
+
}
|
|
164
174
|
options = {
|
|
165
175
|
...options,
|
|
166
176
|
inputs: {
|
|
@@ -291,11 +301,20 @@ async function executeWorkflow(filePath, options = {}, internal = {}) {
|
|
|
291
301
|
// its running step killed and its containers removed.
|
|
292
302
|
const runCombination = async (jobIndex, matrixValues, signal) => {
|
|
293
303
|
const jobLabel = labelOf(matrixValues);
|
|
294
|
-
|
|
295
|
-
|
|
304
|
+
// With --isolate-workspace, the job (and github.workspace) gets
|
|
305
|
+
// its own copy of the working copy.
|
|
306
|
+
const workspace = options.isolateWorkspace
|
|
307
|
+
? (0, checkout_1.createIsolatedWorkspace)(process.cwd(), runDir)
|
|
308
|
+
: process.cwd();
|
|
309
|
+
const comboGithub = options.isolateWorkspace
|
|
310
|
+
? { ...github, workspace }
|
|
311
|
+
: github;
|
|
312
|
+
const runtime = (0, job_runtime_1.createJobRuntime)(comboGithub, runDir, {
|
|
313
|
+
shell: job.defaults?.run?.shell ??
|
|
314
|
+
parsed.file.defaults?.run?.shell,
|
|
296
315
|
workingDirectory: job.defaults?.run?.['working-directory'] ??
|
|
297
316
|
parsed.file.defaults?.run?.['working-directory']
|
|
298
|
-
});
|
|
317
|
+
}, workspace);
|
|
299
318
|
runtime.signal = signal;
|
|
300
319
|
// Workflow-level env can't reference `env` itself; job-level
|
|
301
320
|
// env sees the workflow env plus matrix (matching GitHub's
|
|
@@ -303,17 +322,17 @@ async function executeWorkflow(filePath, options = {}, internal = {}) {
|
|
|
303
322
|
const workflowEnv = {
|
|
304
323
|
...options.env,
|
|
305
324
|
...(0, expression_1.substituteRecord)(parsed.file.env, {
|
|
306
|
-
...(0, expression_1.createContext)({ ...options.env }, inputs, options.secrets,
|
|
325
|
+
...(0, expression_1.createContext)({ ...options.env }, inputs, options.secrets, comboGithub),
|
|
307
326
|
vars
|
|
308
327
|
})
|
|
309
328
|
};
|
|
310
329
|
const jobEnv = (0, expression_1.substituteRecord)(job.env, {
|
|
311
|
-
...(0, expression_1.createContext)(workflowEnv, inputs, options.secrets,
|
|
330
|
+
...(0, expression_1.createContext)(workflowEnv, inputs, options.secrets, comboGithub, matrixValues),
|
|
312
331
|
needs,
|
|
313
332
|
vars
|
|
314
333
|
});
|
|
315
334
|
const jobCtx = {
|
|
316
|
-
...(0, expression_1.createContext)({ ...workflowEnv, ...jobEnv }, inputs, options.secrets,
|
|
335
|
+
...(0, expression_1.createContext)({ ...workflowEnv, ...jobEnv }, inputs, options.secrets, comboGithub, matrixValues),
|
|
317
336
|
needs,
|
|
318
337
|
vars,
|
|
319
338
|
job: { status: 'success' },
|
|
@@ -526,6 +545,9 @@ async function executeWorkflow(filePath, options = {}, internal = {}) {
|
|
|
526
545
|
}
|
|
527
546
|
finally {
|
|
528
547
|
await stopping;
|
|
548
|
+
if (options.isolateWorkspace) {
|
|
549
|
+
(0, checkout_1.removeIsolatedWorkspace)(workspace);
|
|
550
|
+
}
|
|
529
551
|
}
|
|
530
552
|
const jobSuccess = stepResults.every((s) => s.success || s.step['continue-on-error']);
|
|
531
553
|
// Job-level continue-on-error (often
|