@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
package/dist/runner/executor.js
CHANGED
|
@@ -50,6 +50,7 @@ const job_runtime_1 = require("./job-runtime");
|
|
|
50
50
|
const job_container_1 = require("./job-container");
|
|
51
51
|
const steps_1 = require("./steps");
|
|
52
52
|
const actions_1 = require("./actions");
|
|
53
|
+
const scheduler_1 = require("./scheduler");
|
|
53
54
|
// Public API, kept here so `executor` stays the entry point.
|
|
54
55
|
var github_context_2 = require("./github-context");
|
|
55
56
|
Object.defineProperty(exports, "parseRepositoryFromRemote", { enumerable: true, get: function () { return github_context_2.parseRepositoryFromRemote; } });
|
|
@@ -59,6 +60,8 @@ Object.defineProperty(exports, "topologicalJobOrder", { enumerable: true, get: f
|
|
|
59
60
|
var job_container_2 = require("./job-container");
|
|
60
61
|
Object.defineProperty(exports, "DEFAULT_CONTAINER_IMAGE", { enumerable: true, get: function () { return job_container_2.DEFAULT_CONTAINER_IMAGE; } });
|
|
61
62
|
Object.defineProperty(exports, "DEFAULT_RUNNER_IMAGES", { enumerable: true, get: function () { return job_container_2.DEFAULT_RUNNER_IMAGES; } });
|
|
63
|
+
/** The error of a step killed because its job was cancelled (GitHub's). */
|
|
64
|
+
const CANCELLED_MESSAGE = 'The operation was canceled.';
|
|
62
65
|
/**
|
|
63
66
|
* Execute a reusable workflow called via `jobs.<id>.uses:`: a local path
|
|
64
67
|
* (`./.github/workflows/x.yml`, relative to the repository root) or a
|
|
@@ -67,7 +70,7 @@ Object.defineProperty(exports, "DEFAULT_RUNNER_IMAGES", { enumerable: true, get:
|
|
|
67
70
|
* (all of them with `secrets: inherit`), and its `on.workflow_call.outputs`
|
|
68
71
|
* become the calling job's outputs.
|
|
69
72
|
*/
|
|
70
|
-
async function executeReusableWorkflowJob(jobId, job, callerPath, options, ctx) {
|
|
73
|
+
async function executeReusableWorkflowJob(jobId, job, callerPath, options, ctx, internal) {
|
|
71
74
|
const startTime = Date.now();
|
|
72
75
|
const uses = job.uses;
|
|
73
76
|
const failed = (error) => {
|
|
@@ -111,8 +114,10 @@ async function executeReusableWorkflowJob(jobId, job, callerPath, options, ctx)
|
|
|
111
114
|
...options,
|
|
112
115
|
inputs,
|
|
113
116
|
secrets,
|
|
114
|
-
job
|
|
115
|
-
|
|
117
|
+
// --job and --matrix select the caller's jobs, not the called ones.
|
|
118
|
+
job: undefined,
|
|
119
|
+
matrix: undefined
|
|
120
|
+
}, internal);
|
|
116
121
|
return {
|
|
117
122
|
job: jobId,
|
|
118
123
|
success: subResult.success,
|
|
@@ -122,9 +127,11 @@ async function executeReusableWorkflowJob(jobId, job, callerPath, options, ctx)
|
|
|
122
127
|
};
|
|
123
128
|
}
|
|
124
129
|
/**
|
|
125
|
-
* Execute a workflow or action file
|
|
130
|
+
* Execute a workflow or action file. Jobs whose `needs:` are done, and
|
|
131
|
+
* matrix combinations, run concurrently up to `options.parallel` (default
|
|
132
|
+
* 1: one after another).
|
|
126
133
|
*/
|
|
127
|
-
async function executeWorkflow(filePath, options = {}) {
|
|
134
|
+
async function executeWorkflow(filePath, options = {}, internal = {}) {
|
|
128
135
|
const startTime = Date.now();
|
|
129
136
|
logger_1.logger.registerSecrets(options.secrets);
|
|
130
137
|
// One run id for every job and called workflow of this run.
|
|
@@ -164,32 +171,55 @@ async function executeWorkflow(filePath, options = {}) {
|
|
|
164
171
|
const event = (0, github_context_1.loadEvent)(options, options.inputs ?? {}, parsed.file.on);
|
|
165
172
|
const inputs = (0, github_context_1.typedInputs)(parsed.file.on, options.inputs ?? {});
|
|
166
173
|
fs.writeFileSync(path.join(runDir, 'event.json'), JSON.stringify(event.payload));
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
const vars = options.vars ?? {};
|
|
175
|
+
const parallel = Math.max(1, Math.floor(options.parallel ?? 1));
|
|
176
|
+
const concurrent = parallel > 1;
|
|
177
|
+
const slots = internal.slots ?? new scheduler_1.Slots(parallel);
|
|
178
|
+
const matrixFilter = options.matrix ?? {};
|
|
179
|
+
const definedMatrixKeys = new Set();
|
|
180
|
+
let matrixUnmatched = false;
|
|
181
|
+
const order = (0, job_plan_1.topologicalJobOrder)(parsed.file.jobs).filter(
|
|
182
|
+
// With --job, only that job runs; its dependencies count as done.
|
|
183
|
+
(jobId) => !options.job || jobId === options.job);
|
|
184
|
+
// Label log lines with their job only when jobs can interleave.
|
|
185
|
+
const prefixJobs = concurrent &&
|
|
186
|
+
(order.length > 1 ||
|
|
187
|
+
order.some((jobId) => parsed.file.jobs[jobId].strategy?.matrix));
|
|
188
|
+
const tagged = (label, fn, replace = false) => (prefixJobs ? logger_1.logger.withJob(label, fn, replace) : fn());
|
|
189
|
+
const needsOf = (jobId) => {
|
|
190
|
+
const needs = parsed.file.jobs[jobId].needs;
|
|
191
|
+
return needs ? (Array.isArray(needs) ? needs : [needs]) : [];
|
|
192
|
+
};
|
|
193
|
+
const selectCombinations = (jobId, combinations) => {
|
|
194
|
+
for (const combo of combinations) {
|
|
195
|
+
Object.keys(combo).forEach((key) => definedMatrixKeys.add(key));
|
|
196
|
+
}
|
|
197
|
+
const { keys, matches } = (0, job_plan_1.filterMatrix)(combinations, matrixFilter);
|
|
198
|
+
if (keys.length > 0 && matches.length === 0) {
|
|
199
|
+
logger_1.logger.warn((0, job_plan_1.noMatrixMatchMessage)(jobId, matrixFilter, combinations));
|
|
200
|
+
matrixUnmatched = true;
|
|
175
201
|
}
|
|
202
|
+
return matches;
|
|
203
|
+
};
|
|
204
|
+
// Each job's results, in matrix order; collected in `order` at the end
|
|
205
|
+
// so the report doesn't depend on which job finished first.
|
|
206
|
+
const resultsByJob = new Map();
|
|
207
|
+
const runJob = (jobId, depResults) => tagged(jobId, async () => {
|
|
208
|
+
const job = parsed.file.jobs[jobId];
|
|
209
|
+
const jobs = [];
|
|
210
|
+
resultsByJob.set(jobId, jobs);
|
|
176
211
|
const github = (0, github_context_1.buildGithubContext)(parsed.file.name, jobId, process.cwd(), event, options.secrets?.GITHUB_TOKEN, options.runId);
|
|
177
212
|
// `needs` results feed the job's `if:` (implicitly `success()`,
|
|
178
213
|
// which requires every dependency to have succeeded). With --job,
|
|
179
214
|
// dependencies aren't run and count as successful.
|
|
180
|
-
const needsIds =
|
|
181
|
-
? Array.isArray(job.needs)
|
|
182
|
-
? job.needs
|
|
183
|
-
: [job.needs]
|
|
184
|
-
: [];
|
|
215
|
+
const needsIds = needsOf(jobId);
|
|
185
216
|
const needs = {};
|
|
186
217
|
for (const dep of needsIds) {
|
|
187
|
-
needs[dep] =
|
|
218
|
+
needs[dep] = depResults[dep] ?? {
|
|
188
219
|
result: 'success',
|
|
189
220
|
outputs: {}
|
|
190
221
|
};
|
|
191
222
|
}
|
|
192
|
-
const vars = options.vars ?? {};
|
|
193
223
|
const jobIfCtx = {
|
|
194
224
|
...(0, expression_1.createContext)({}, inputs, options.secrets, github),
|
|
195
225
|
needs,
|
|
@@ -199,276 +229,360 @@ async function executeWorkflow(filePath, options = {}) {
|
|
|
199
229
|
anyFailed: Object.values(needs).some((n) => n.result === 'failure')
|
|
200
230
|
}
|
|
201
231
|
};
|
|
202
|
-
let
|
|
232
|
+
let runThisJob;
|
|
203
233
|
try {
|
|
204
|
-
|
|
234
|
+
runThisJob = (0, expression_1.evaluateCondition)((0, expression_1.withImplicitSuccess)(job.if), jobIfCtx);
|
|
205
235
|
}
|
|
206
236
|
catch (error) {
|
|
207
237
|
// An if: that can't be evaluated fails the job, as on GitHub.
|
|
208
238
|
logger_1.logger.error((0, expression_1.ifEvaluationError)(`job '${jobId}'`, error));
|
|
209
239
|
jobs.push({ job: jobId, success: false, steps: [], duration: 0 });
|
|
210
|
-
|
|
211
|
-
continue;
|
|
240
|
+
return { result: 'failure', outputs: {} };
|
|
212
241
|
}
|
|
213
|
-
if (!
|
|
242
|
+
if (!runThisJob) {
|
|
214
243
|
const notOk = needsIds.find((dep) => needs[dep].result !== 'success');
|
|
215
244
|
logger_1.logger.info(notOk
|
|
216
245
|
? `Skipping job: ${jobId} (dependency "${notOk}" ${needs[notOk].result === 'skipped' ? 'was skipped' : 'did not succeed'})`
|
|
217
246
|
: `Skipping job: ${jobId} (condition not met)`);
|
|
218
|
-
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
// Handle reusable workflow calls (once per matrix combination)
|
|
222
|
-
if (job.uses) {
|
|
223
|
-
const combinations = (0, job_plan_1.expandMatrix)(job.strategy?.matrix, jobIfCtx);
|
|
224
|
-
const callOutputs = {};
|
|
225
|
-
let callFailed = false;
|
|
226
|
-
for (const matrixValues of combinations) {
|
|
227
|
-
if (callFailed && job.strategy?.['fail-fast'] !== false)
|
|
228
|
-
break;
|
|
229
|
-
const label = combinations.length > 1
|
|
230
|
-
? `${jobId} (${(0, job_plan_1.matrixLabel)(matrixValues)})`
|
|
231
|
-
: jobId;
|
|
232
|
-
const reusableResult = await executeReusableWorkflowJob(label, job, filePath, options, { ...jobIfCtx, matrix: matrixValues });
|
|
233
|
-
jobs.push(reusableResult);
|
|
234
|
-
Object.assign(callOutputs, reusableResult.outputs);
|
|
235
|
-
if (!reusableResult.success)
|
|
236
|
-
callFailed = true;
|
|
237
|
-
}
|
|
238
|
-
jobResults.set(jobId, {
|
|
239
|
-
result: callFailed ? 'failure' : 'success',
|
|
240
|
-
outputs: callOutputs
|
|
241
|
-
});
|
|
242
|
-
if (!callFailed) {
|
|
243
|
-
logger_1.logger.success(`Job ${jobId} completed successfully`);
|
|
244
|
-
}
|
|
245
|
-
continue;
|
|
247
|
+
return { result: 'skipped', outputs: {} };
|
|
246
248
|
}
|
|
247
249
|
const matrixCombinations = (0, job_plan_1.expandMatrix)(job.strategy?.matrix, jobIfCtx);
|
|
250
|
+
const selected = selectCombinations(jobId, matrixCombinations);
|
|
251
|
+
if (selected.length === 0)
|
|
252
|
+
return { result: 'skipped', outputs: {} };
|
|
248
253
|
const failFast = job.strategy?.['fail-fast'] !== false;
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
254
|
+
const maxParallel = (0, job_plan_1.numberSetting)(job.strategy?.['max-parallel'], jobIfCtx);
|
|
255
|
+
const labelOf = (matrixValues) => matrixCombinations.length > 1
|
|
256
|
+
? `${jobId} (${(0, job_plan_1.matrixLabel)(matrixValues)})`
|
|
257
|
+
: jobId;
|
|
258
|
+
// Each combination's result and outputs, by combination index.
|
|
259
|
+
const unitResults = [];
|
|
260
|
+
const unitOutputs = [];
|
|
261
|
+
let outcomes;
|
|
262
|
+
if (job.uses) {
|
|
263
|
+
// A reusable workflow call (once per matrix combination). The
|
|
264
|
+
// call itself holds no slot: the called workflow's jobs do.
|
|
265
|
+
outcomes = await (0, scheduler_1.runMatrix)(selected.length, (index, signal) => {
|
|
266
|
+
const { index: combinationIndex, values: matrixValues } = selected[index];
|
|
267
|
+
const label = labelOf(matrixValues);
|
|
268
|
+
return tagged(label, async () => {
|
|
269
|
+
const reusableResult = await executeReusableWorkflowJob(label, job, filePath, options, { ...jobIfCtx, matrix: matrixValues }, { slots, signal });
|
|
270
|
+
const cancelled = !reusableResult.success && signal.aborted;
|
|
271
|
+
unitResults[combinationIndex] = cancelled
|
|
272
|
+
? { ...reusableResult, cancelled: true }
|
|
273
|
+
: reusableResult;
|
|
274
|
+
unitOutputs[combinationIndex] =
|
|
275
|
+
reusableResult.outputs ?? {};
|
|
276
|
+
return {
|
|
277
|
+
result: reusableResult.success
|
|
278
|
+
? 'success'
|
|
279
|
+
: cancelled
|
|
280
|
+
? 'cancelled'
|
|
281
|
+
: 'failure'
|
|
282
|
+
};
|
|
283
|
+
}, true);
|
|
284
|
+
}, { failFast, maxParallel, signal: internal.signal });
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Outputs so far, as each combination's result reports them.
|
|
288
|
+
const jobOutputs = {};
|
|
289
|
+
// One matrix combination (or the job itself): its container,
|
|
290
|
+
// services and steps. A cancelled combination (fail-fast) has
|
|
291
|
+
// its running step killed and its containers removed.
|
|
292
|
+
const runCombination = async (jobIndex, matrixValues, signal) => {
|
|
293
|
+
const jobLabel = labelOf(matrixValues);
|
|
294
|
+
const runtime = (0, job_runtime_1.createJobRuntime)(github, runDir, {
|
|
295
|
+
shell: job.defaults?.run?.shell ?? parsed.file.defaults?.run?.shell,
|
|
296
|
+
workingDirectory: job.defaults?.run?.['working-directory'] ??
|
|
297
|
+
parsed.file.defaults?.run?.['working-directory']
|
|
298
|
+
});
|
|
299
|
+
runtime.signal = signal;
|
|
300
|
+
// Workflow-level env can't reference `env` itself; job-level
|
|
301
|
+
// env sees the workflow env plus matrix (matching GitHub's
|
|
302
|
+
// contexts).
|
|
303
|
+
const workflowEnv = {
|
|
304
|
+
...options.env,
|
|
305
|
+
...(0, expression_1.substituteRecord)(parsed.file.env, {
|
|
306
|
+
...(0, expression_1.createContext)({ ...options.env }, inputs, options.secrets, github),
|
|
307
|
+
vars
|
|
308
|
+
})
|
|
309
|
+
};
|
|
310
|
+
const jobEnv = (0, expression_1.substituteRecord)(job.env, {
|
|
311
|
+
...(0, expression_1.createContext)(workflowEnv, inputs, options.secrets, github, matrixValues),
|
|
312
|
+
needs,
|
|
271
313
|
vars
|
|
272
|
-
})
|
|
273
|
-
};
|
|
274
|
-
const jobEnv = (0, expression_1.substituteRecord)(job.env, {
|
|
275
|
-
...(0, expression_1.createContext)(workflowEnv, inputs, options.secrets, github, matrixValues),
|
|
276
|
-
needs,
|
|
277
|
-
vars
|
|
278
|
-
});
|
|
279
|
-
const jobCtx = {
|
|
280
|
-
...(0, expression_1.createContext)({ ...workflowEnv, ...jobEnv }, inputs, options.secrets, github, matrixValues),
|
|
281
|
-
needs,
|
|
282
|
-
vars,
|
|
283
|
-
job: { status: 'success' },
|
|
284
|
-
runner: (0, job_runtime_1.runnerContext)(runtime),
|
|
285
|
-
strategy: {
|
|
286
|
-
'fail-fast': failFast,
|
|
287
|
-
'job-index': jobIndex,
|
|
288
|
-
'job-total': matrixCombinations.length,
|
|
289
|
-
'max-parallel': job.strategy?.['max-parallel'] ?? matrixCombinations.length
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
logger_1.logger.step(`Executing job: ${jobLabel}`);
|
|
293
|
-
const jobStartTime = Date.now();
|
|
294
|
-
const jobTimeoutMinutes = (0, job_plan_1.numberSetting)(job['timeout-minutes'], jobCtx);
|
|
295
|
-
const jobDeadline = jobTimeoutMinutes
|
|
296
|
-
? jobStartTime + jobTimeoutMinutes * 60000
|
|
297
|
-
: undefined;
|
|
298
|
-
const jobTimeoutMessage = `The job running on runner secureci has exceeded the maximum execution time of ${jobTimeoutMinutes} minutes.`;
|
|
299
|
-
const stepResults = [];
|
|
300
|
-
if (!(await (0, job_container_1.startJobServices)(job, jobCtx, options, runtime)) ||
|
|
301
|
-
!(await (0, job_container_1.startJobContainer)(job, jobCtx, options, runtime, runDir))) {
|
|
302
|
-
await (0, job_container_1.stopJobContainer)(runtime);
|
|
303
|
-
jobs.push({
|
|
304
|
-
job: jobLabel,
|
|
305
|
-
success: false,
|
|
306
|
-
steps: [],
|
|
307
|
-
duration: Date.now() - jobStartTime
|
|
308
314
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
...jobCtx.job,
|
|
321
|
-
status: jobCtx.jobFailed ? 'failure' : 'success'
|
|
322
|
-
};
|
|
323
|
-
// `$GITHUB_ENV` additions from earlier steps sit between the
|
|
324
|
-
// job env and this step's own `env:`.
|
|
325
|
-
const envCtx = {
|
|
326
|
-
...jobCtx,
|
|
327
|
-
env: { ...jobCtx.env, ...runtime.env }
|
|
328
|
-
};
|
|
329
|
-
runtime.actionEnv = (0, job_runtime_1.stepActionEnv)(runtime, step);
|
|
330
|
-
const stepCtx = {
|
|
331
|
-
...envCtx,
|
|
332
|
-
github: {
|
|
333
|
-
...envCtx.github,
|
|
334
|
-
action: runtime.actionEnv.GITHUB_ACTION
|
|
335
|
-
},
|
|
336
|
-
env: { ...envCtx.env, ...(0, expression_1.substituteRecord)(step.env, envCtx) }
|
|
337
|
-
};
|
|
338
|
-
// After a failure, only steps whose `if:` uses a status
|
|
339
|
-
// function (always(), failure(), ...) still run.
|
|
340
|
-
let runStep;
|
|
341
|
-
try {
|
|
342
|
-
runStep = (0, expression_1.evaluateCondition)((0, expression_1.withImplicitSuccess)(step.if), stepCtx);
|
|
315
|
+
const jobCtx = {
|
|
316
|
+
...(0, expression_1.createContext)({ ...workflowEnv, ...jobEnv }, inputs, options.secrets, github, matrixValues),
|
|
317
|
+
needs,
|
|
318
|
+
vars,
|
|
319
|
+
job: { status: 'success' },
|
|
320
|
+
runner: (0, job_runtime_1.runnerContext)(runtime),
|
|
321
|
+
strategy: {
|
|
322
|
+
'fail-fast': failFast,
|
|
323
|
+
'job-index': jobIndex,
|
|
324
|
+
'job-total': matrixCombinations.length,
|
|
325
|
+
'max-parallel': job.strategy?.['max-parallel'] ?? matrixCombinations.length
|
|
343
326
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
327
|
+
};
|
|
328
|
+
logger_1.logger.step(`Executing job: ${jobLabel}`);
|
|
329
|
+
const jobStartTime = Date.now();
|
|
330
|
+
const jobTimeoutMinutes = (0, job_plan_1.numberSetting)(job['timeout-minutes'], jobCtx);
|
|
331
|
+
const jobDeadline = jobTimeoutMinutes
|
|
332
|
+
? jobStartTime + jobTimeoutMinutes * 60000
|
|
333
|
+
: undefined;
|
|
334
|
+
const jobTimeoutMessage = `The job running on runner secureci has exceeded the maximum execution time of ${jobTimeoutMinutes} minutes.`;
|
|
335
|
+
const stepResults = [];
|
|
336
|
+
const cancelledResult = () => {
|
|
337
|
+
logger_1.logger.warn(`Cancelled job: ${jobLabel}`);
|
|
338
|
+
unitResults[jobIndex] = {
|
|
339
|
+
job: jobLabel,
|
|
340
|
+
success: false,
|
|
341
|
+
cancelled: true,
|
|
342
|
+
steps: stepResults,
|
|
343
|
+
duration: Date.now() - jobStartTime
|
|
344
|
+
};
|
|
345
|
+
return { result: 'cancelled' };
|
|
346
|
+
};
|
|
347
|
+
// Once the containers are up, cancelling removes them, which
|
|
348
|
+
// ends the step running in them.
|
|
349
|
+
let stopping;
|
|
350
|
+
const onAbort = () => {
|
|
351
|
+
stopping = (0, job_container_1.stopJobContainer)(runtime);
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
if (!(await (0, job_container_1.startJobServices)(job, jobCtx, options, runtime)) ||
|
|
355
|
+
!(await (0, job_container_1.startJobContainer)(job, jobCtx, options, runtime, runDir))) {
|
|
356
|
+
await (0, job_container_1.stopJobContainer)(runtime);
|
|
357
|
+
if (signal.aborted)
|
|
358
|
+
return cancelledResult();
|
|
359
|
+
unitResults[jobIndex] = {
|
|
360
|
+
job: jobLabel,
|
|
350
361
|
success: false,
|
|
351
|
-
|
|
352
|
-
duration:
|
|
353
|
-
});
|
|
354
|
-
if (step.id) {
|
|
355
|
-
jobCtx.steps[step.id] = {
|
|
356
|
-
outcome: 'failure',
|
|
357
|
-
conclusion: 'failure',
|
|
358
|
-
outputs: {}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
jobCtx.jobFailed = true;
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
if (!runStep) {
|
|
365
|
-
logger_1.logger.info(`Skipping step: ${(0, job_runtime_1.stepDisplayName)(step, stepCtx)} (condition not met)`);
|
|
366
|
-
const skippedResult = {
|
|
367
|
-
step,
|
|
368
|
-
success: true,
|
|
369
|
-
outcome: 'skipped',
|
|
370
|
-
output: 'Skipped: condition not met',
|
|
371
|
-
duration: 0
|
|
362
|
+
steps: [],
|
|
363
|
+
duration: Date.now() - jobStartTime
|
|
372
364
|
};
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
365
|
+
return { result: 'failure' };
|
|
366
|
+
}
|
|
367
|
+
if (signal.aborted) {
|
|
368
|
+
await (0, job_container_1.stopJobContainer)(runtime);
|
|
369
|
+
return cancelledResult();
|
|
370
|
+
}
|
|
371
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
372
|
+
if (!runtime.container) {
|
|
373
|
+
(0, actions_1.warnUnsandboxedActions)(job.steps ?? [], jobCtx, options);
|
|
382
374
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
375
|
+
try {
|
|
376
|
+
await (0, actions_1.runPreSteps)(job.steps ?? [], options, runtime, jobCtx, stepResults);
|
|
377
|
+
// Execute each step
|
|
378
|
+
for (const step of job.steps || []) {
|
|
379
|
+
if (signal.aborted)
|
|
380
|
+
break;
|
|
381
|
+
jobCtx.job = {
|
|
382
|
+
...jobCtx.job,
|
|
383
|
+
status: jobCtx.jobFailed ? 'failure' : 'success'
|
|
384
|
+
};
|
|
385
|
+
// `$GITHUB_ENV` additions from earlier steps sit between
|
|
386
|
+
// the job env and this step's own `env:`.
|
|
387
|
+
const envCtx = {
|
|
388
|
+
...jobCtx,
|
|
389
|
+
env: { ...jobCtx.env, ...runtime.env }
|
|
390
|
+
};
|
|
391
|
+
runtime.actionEnv = (0, job_runtime_1.stepActionEnv)(runtime, step);
|
|
392
|
+
const stepCtx = {
|
|
393
|
+
...envCtx,
|
|
394
|
+
github: {
|
|
395
|
+
...envCtx.github,
|
|
396
|
+
action: runtime.actionEnv.GITHUB_ACTION
|
|
397
|
+
},
|
|
398
|
+
env: {
|
|
399
|
+
...envCtx.env,
|
|
400
|
+
...(0, expression_1.substituteRecord)(step.env, envCtx)
|
|
397
401
|
}
|
|
402
|
+
};
|
|
403
|
+
// After a failure, only steps whose `if:` uses a status
|
|
404
|
+
// function (always(), failure(), ...) still run.
|
|
405
|
+
let runStep;
|
|
406
|
+
try {
|
|
407
|
+
runStep = (0, expression_1.evaluateCondition)((0, expression_1.withImplicitSuccess)(step.if), stepCtx);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
// An if: that can't be evaluated fails the step.
|
|
411
|
+
const message = (0, expression_1.ifEvaluationError)(`step '${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}'`, error);
|
|
412
|
+
logger_1.logger.error(message);
|
|
413
|
+
stepResults.push({
|
|
414
|
+
step,
|
|
415
|
+
success: false,
|
|
416
|
+
error: message,
|
|
417
|
+
duration: 0
|
|
418
|
+
});
|
|
419
|
+
if (step.id) {
|
|
420
|
+
jobCtx.steps[step.id] = {
|
|
421
|
+
outcome: 'failure',
|
|
422
|
+
conclusion: 'failure',
|
|
423
|
+
outputs: {}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
jobCtx.jobFailed = true;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (!runStep) {
|
|
430
|
+
logger_1.logger.info(`Skipping step: ${(0, job_runtime_1.stepDisplayName)(step, stepCtx)} (condition not met)`);
|
|
431
|
+
const skippedResult = {
|
|
432
|
+
step,
|
|
433
|
+
success: true,
|
|
434
|
+
outcome: 'skipped',
|
|
435
|
+
output: 'Skipped: condition not met',
|
|
436
|
+
duration: 0
|
|
437
|
+
};
|
|
438
|
+
stepResults.push(skippedResult);
|
|
439
|
+
if (step.id) {
|
|
440
|
+
jobCtx.steps[step.id] = {
|
|
441
|
+
outcome: 'skipped',
|
|
442
|
+
conclusion: 'skipped',
|
|
443
|
+
outputs: {}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
// timeout-minutes: the step's own, or what's left of the
|
|
449
|
+
// job's.
|
|
450
|
+
const stepMinutes = (0, job_plan_1.numberSetting)(step['timeout-minutes'], stepCtx);
|
|
451
|
+
const jobLeft = jobDeadline !== undefined
|
|
452
|
+
? Math.max(jobDeadline - Date.now(), 1)
|
|
398
453
|
: undefined;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
454
|
+
const stepMs = stepMinutes
|
|
455
|
+
? stepMinutes * 60000
|
|
456
|
+
: undefined;
|
|
457
|
+
runtime.stepTimeout =
|
|
458
|
+
jobLeft !== undefined &&
|
|
459
|
+
(stepMs === undefined || jobLeft < stepMs)
|
|
460
|
+
? { ms: jobLeft, message: jobTimeoutMessage }
|
|
461
|
+
: stepMs !== undefined
|
|
462
|
+
? {
|
|
463
|
+
ms: stepMs,
|
|
464
|
+
message: `The action '${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}' has timed out after ${stepMinutes} minutes.`
|
|
465
|
+
}
|
|
466
|
+
: undefined;
|
|
467
|
+
const stepResult = await (0, steps_1.executeStep)(step, options, runtime, stepCtx);
|
|
468
|
+
runtime.stepTimeout = undefined;
|
|
469
|
+
stepResults.push(stepResult);
|
|
470
|
+
if (signal.aborted) {
|
|
471
|
+
// Cancelled: the step was killed; nothing else runs.
|
|
472
|
+
if (!stepResult.success) {
|
|
473
|
+
stepResult.error = CANCELLED_MESSAGE;
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (jobDeadline !== undefined &&
|
|
478
|
+
Date.now() >= jobDeadline) {
|
|
479
|
+
// The job is cancelled: no further steps run.
|
|
480
|
+
if (stepResult.success) {
|
|
481
|
+
stepResult.success = false;
|
|
482
|
+
stepResult.error = jobTimeoutMessage;
|
|
483
|
+
}
|
|
484
|
+
logger_1.logger.error(jobTimeoutMessage);
|
|
485
|
+
jobCtx.jobFailed = true;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
const outcome = stepResult.success ? 'success' : 'failure';
|
|
489
|
+
if (step.id) {
|
|
490
|
+
jobCtx.steps[step.id] = {
|
|
491
|
+
outcome,
|
|
492
|
+
// continue-on-error turns a failure into a successful
|
|
493
|
+
// conclusion, which is what success()/failure() see.
|
|
494
|
+
conclusion: step['continue-on-error']
|
|
495
|
+
? 'success'
|
|
496
|
+
: outcome,
|
|
497
|
+
outputs: stepResult.outputs || {}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (!stepResult.success && !step['continue-on-error']) {
|
|
501
|
+
jobCtx.jobFailed = true;
|
|
502
|
+
logger_1.logger.error(`Step failed: ${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}`);
|
|
503
|
+
}
|
|
407
504
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
jobCtx.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// conclusion, which is what success()/failure() see.
|
|
418
|
-
conclusion: step['continue-on-error'] ? 'success' : outcome,
|
|
419
|
-
outputs: stepResult.outputs || {}
|
|
505
|
+
if (signal.aborted)
|
|
506
|
+
return cancelledResult();
|
|
507
|
+
await (0, job_runtime_1.runPostSteps)(runtime, jobCtx, stepResults);
|
|
508
|
+
if (runtime.summary.trim()) {
|
|
509
|
+
logger_1.logger.info(`Job summary (${jobLabel}):\n${runtime.summary.trim()}`);
|
|
510
|
+
}
|
|
511
|
+
jobCtx.job = {
|
|
512
|
+
...jobCtx.job,
|
|
513
|
+
status: jobCtx.jobFailed ? 'failure' : 'success'
|
|
420
514
|
};
|
|
515
|
+
unitOutputs[jobIndex] = (0, expression_1.substituteRecord)(job.outputs, {
|
|
516
|
+
...jobCtx,
|
|
517
|
+
env: { ...jobCtx.env, ...runtime.env }
|
|
518
|
+
});
|
|
519
|
+
Object.assign(jobOutputs, unitOutputs[jobIndex]);
|
|
421
520
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
521
|
+
finally {
|
|
522
|
+
signal.removeEventListener('abort', onAbort);
|
|
523
|
+
await stopping;
|
|
524
|
+
await (0, job_container_1.stopJobContainer)(runtime);
|
|
425
525
|
}
|
|
426
526
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
logger_1.logger.info(`Job summary (${jobLabel}):\n${runtime.summary.trim()}`);
|
|
527
|
+
finally {
|
|
528
|
+
await stopping;
|
|
430
529
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
530
|
+
const jobSuccess = stepResults.every((s) => s.success || s.step['continue-on-error']);
|
|
531
|
+
// Job-level continue-on-error (often
|
|
532
|
+
// `${{ matrix.experimental }}`) lets this combination fail
|
|
533
|
+
// without failing the job or run.
|
|
534
|
+
const allowedFailure = !jobSuccess &&
|
|
535
|
+
(0, job_plan_1.isTruthySetting)(job['continue-on-error'], jobCtx);
|
|
536
|
+
unitResults[jobIndex] = {
|
|
537
|
+
job: jobLabel,
|
|
538
|
+
success: jobSuccess,
|
|
539
|
+
...(allowedFailure ? { continueOnError: true } : {}),
|
|
540
|
+
...(runtime.summary ? { summary: runtime.summary } : {}),
|
|
541
|
+
steps: stepResults,
|
|
542
|
+
outputs: { ...jobOutputs },
|
|
543
|
+
duration: Date.now() - jobStartTime
|
|
434
544
|
};
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
...(runtime.summary ? { summary: runtime.summary } : {}),
|
|
452
|
-
steps: stepResults,
|
|
453
|
-
outputs: { ...jobOutputs },
|
|
454
|
-
duration: Date.now() - jobStartTime
|
|
545
|
+
return {
|
|
546
|
+
result: jobSuccess ? 'success' : 'failure',
|
|
547
|
+
allowedFailure
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
outcomes = await (0, scheduler_1.runMatrix)(selected.length, (index, signal) => {
|
|
551
|
+
const { index: jobIndex, values: matrixValues } = selected[index];
|
|
552
|
+
return tagged(labelOf(matrixValues), () => runCombination(jobIndex, matrixValues, signal), true);
|
|
553
|
+
}, {
|
|
554
|
+
failFast,
|
|
555
|
+
maxParallel,
|
|
556
|
+
slots,
|
|
557
|
+
signal: internal.signal,
|
|
558
|
+
onFailFast: () => logger_1.logger.info(concurrent
|
|
559
|
+
? `Cancelling remaining matrix jobs for ${jobId} (fail-fast)`
|
|
560
|
+
: `Skipping remaining matrix jobs for ${jobId} (fail-fast)`)
|
|
455
561
|
});
|
|
456
|
-
if (!jobSuccess && !allowedFailure) {
|
|
457
|
-
matrixFailed = true;
|
|
458
|
-
}
|
|
459
562
|
}
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
563
|
+
for (const result of unitResults) {
|
|
564
|
+
if (result)
|
|
565
|
+
jobs.push(result);
|
|
566
|
+
}
|
|
567
|
+
const failed = outcomes.some((o) => o?.result === 'failure' && !o.allowedFailure);
|
|
568
|
+
const cancelled = !failed && outcomes.some((o) => o?.result === 'cancelled');
|
|
569
|
+
if (!failed && !cancelled) {
|
|
466
570
|
logger_1.logger.success(`Job ${jobId} completed successfully`);
|
|
467
571
|
}
|
|
572
|
+
return {
|
|
573
|
+
result: failed ? 'failure' : cancelled ? 'cancelled' : 'success',
|
|
574
|
+
// As if the combinations ran in order: the last one's value wins.
|
|
575
|
+
outputs: Object.assign({}, ...unitOutputs)
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
const jobResults = await (0, scheduler_1.runJobGraph)(order, needsOf, runJob, concurrent);
|
|
579
|
+
const jobs = order.flatMap((jobId) => resultsByJob.get(jobId) ?? []);
|
|
580
|
+
for (const key of (0, job_plan_1.unknownMatrixKeys)(matrixFilter, definedMatrixKeys)) {
|
|
581
|
+
logger_1.logger.warn(`--matrix ${key}: no matrix job defines this key; it was ignored`);
|
|
468
582
|
}
|
|
469
583
|
const callOutputs = (0, github_context_1.workflowCallOutputs)(parsed.file.on);
|
|
470
584
|
return {
|
|
471
|
-
success: jobs.every((j) => j.success || j.continueOnError),
|
|
585
|
+
success: !matrixUnmatched && jobs.every((j) => j.success || j.continueOnError),
|
|
472
586
|
jobs,
|
|
473
587
|
duration: Date.now() - startTime,
|
|
474
588
|
...(callOutputs
|