@secureci/cli 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +201 -2
- package/dist/cli.js +545 -0
- package/dist/config.js +190 -0
- package/dist/index.js +23 -0
- package/dist/init.js +45 -0
- package/dist/inventory.js +212 -0
- package/dist/parser/pipeline-contract-parser.js +131 -0
- package/dist/parser/pipeline-contract-validator.js +448 -0
- package/dist/parser/validator.js +232 -0
- package/dist/parser/workflow-parser.js +122 -0
- package/dist/report.js +75 -0
- package/dist/runner/action-fetcher.js +431 -0
- package/dist/runner/actions.js +495 -0
- package/dist/runner/builtin-actions.js +484 -0
- package/dist/runner/checkout.js +142 -0
- package/dist/runner/container-runtime.js +374 -0
- package/dist/runner/docker-action.js +179 -0
- package/dist/runner/dry-run.js +142 -0
- package/dist/runner/executor.js +554 -0
- package/dist/runner/expression.js +676 -0
- package/dist/runner/github-context.js +235 -0
- package/dist/runner/hash-files.js +123 -0
- package/dist/runner/job-container.js +322 -0
- package/dist/runner/job-plan.js +138 -0
- package/dist/runner/job-runtime.js +338 -0
- package/dist/runner/node-action.js +278 -0
- package/dist/runner/runner-env.js +303 -0
- package/dist/runner/shell.js +61 -0
- package/dist/runner/steps.js +250 -0
- package/dist/types/index.js +10 -0
- package/dist/utils/key-value.js +33 -0
- package/dist/utils/logger.js +87 -0
- package/dist/utils/repository.js +51 -0
- package/package.json +77 -4
|
@@ -0,0 +1,554 @@
|
|
|
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.DEFAULT_RUNNER_IMAGES = exports.DEFAULT_CONTAINER_IMAGE = exports.topologicalJobOrder = exports.expandMatrix = exports.parseRepositoryFromRemote = void 0;
|
|
37
|
+
exports.executeWorkflow = executeWorkflow;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const workflow_parser_1 = require("../parser/workflow-parser");
|
|
41
|
+
const validator_1 = require("../parser/validator");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
const action_fetcher_1 = require("./action-fetcher");
|
|
44
|
+
const repository_1 = require("../utils/repository");
|
|
45
|
+
const expression_1 = require("./expression");
|
|
46
|
+
const container_runtime_1 = require("./container-runtime");
|
|
47
|
+
const github_context_1 = require("./github-context");
|
|
48
|
+
const job_plan_1 = require("./job-plan");
|
|
49
|
+
const job_runtime_1 = require("./job-runtime");
|
|
50
|
+
const job_container_1 = require("./job-container");
|
|
51
|
+
const steps_1 = require("./steps");
|
|
52
|
+
const actions_1 = require("./actions");
|
|
53
|
+
// Public API, kept here so `executor` stays the entry point.
|
|
54
|
+
var github_context_2 = require("./github-context");
|
|
55
|
+
Object.defineProperty(exports, "parseRepositoryFromRemote", { enumerable: true, get: function () { return github_context_2.parseRepositoryFromRemote; } });
|
|
56
|
+
var job_plan_2 = require("./job-plan");
|
|
57
|
+
Object.defineProperty(exports, "expandMatrix", { enumerable: true, get: function () { return job_plan_2.expandMatrix; } });
|
|
58
|
+
Object.defineProperty(exports, "topologicalJobOrder", { enumerable: true, get: function () { return job_plan_2.topologicalJobOrder; } });
|
|
59
|
+
var job_container_2 = require("./job-container");
|
|
60
|
+
Object.defineProperty(exports, "DEFAULT_CONTAINER_IMAGE", { enumerable: true, get: function () { return job_container_2.DEFAULT_CONTAINER_IMAGE; } });
|
|
61
|
+
Object.defineProperty(exports, "DEFAULT_RUNNER_IMAGES", { enumerable: true, get: function () { return job_container_2.DEFAULT_RUNNER_IMAGES; } });
|
|
62
|
+
/**
|
|
63
|
+
* Execute a reusable workflow called via `jobs.<id>.uses:`: a local path
|
|
64
|
+
* (`./.github/workflows/x.yml`, relative to the repository root) or a
|
|
65
|
+
* remote `owner/repo/path.yml@ref`. `with:` and `secrets:` are resolved in
|
|
66
|
+
* the calling job's context; the called workflow sees only those secrets
|
|
67
|
+
* (all of them with `secrets: inherit`), and its `on.workflow_call.outputs`
|
|
68
|
+
* become the calling job's outputs.
|
|
69
|
+
*/
|
|
70
|
+
async function executeReusableWorkflowJob(jobId, job, callerPath, options, ctx) {
|
|
71
|
+
const startTime = Date.now();
|
|
72
|
+
const uses = job.uses;
|
|
73
|
+
const failed = (error) => {
|
|
74
|
+
logger_1.logger.error(error);
|
|
75
|
+
return {
|
|
76
|
+
job: jobId,
|
|
77
|
+
success: false,
|
|
78
|
+
steps: [],
|
|
79
|
+
duration: Date.now() - startTime
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
let resolvedPath;
|
|
83
|
+
if ((0, actions_1.isLocalActionRef)(uses)) {
|
|
84
|
+
const candidates = [
|
|
85
|
+
path.resolve((0, repository_1.repositoryRoot)(callerPath), uses),
|
|
86
|
+
path.resolve(path.dirname(callerPath), uses)
|
|
87
|
+
];
|
|
88
|
+
resolvedPath = candidates.find((file) => fs.existsSync(file)) ?? null;
|
|
89
|
+
if (!resolvedPath) {
|
|
90
|
+
return failed(`Reusable workflow not found: ${candidates[0]}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
resolvedPath = await (0, action_fetcher_1.fetchRepoFile)(uses, { offline: options.offline });
|
|
95
|
+
if (!resolvedPath) {
|
|
96
|
+
return failed(`Could not fetch reusable workflow ${uses}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const inputs = (0, expression_1.substituteRecord)(job.with, ctx);
|
|
100
|
+
const secrets = job.secrets === 'inherit'
|
|
101
|
+
? { ...(options.secrets || {}) }
|
|
102
|
+
: {
|
|
103
|
+
// GITHUB_TOKEN is always available to called workflows.
|
|
104
|
+
...(options.secrets?.GITHUB_TOKEN !== undefined
|
|
105
|
+
? { GITHUB_TOKEN: options.secrets.GITHUB_TOKEN }
|
|
106
|
+
: {}),
|
|
107
|
+
...(0, expression_1.substituteRecord)(job.secrets, ctx)
|
|
108
|
+
};
|
|
109
|
+
logger_1.logger.step(`Executing reusable workflow: ${uses} (job: ${jobId})`);
|
|
110
|
+
const subResult = await executeWorkflow(resolvedPath, {
|
|
111
|
+
...options,
|
|
112
|
+
inputs,
|
|
113
|
+
secrets,
|
|
114
|
+
job: undefined
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
job: jobId,
|
|
118
|
+
success: subResult.success,
|
|
119
|
+
steps: subResult.jobs.flatMap((j) => j.steps),
|
|
120
|
+
outputs: subResult.outputs ?? {},
|
|
121
|
+
duration: Date.now() - startTime
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Execute a workflow or action file
|
|
126
|
+
*/
|
|
127
|
+
async function executeWorkflow(filePath, options = {}) {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
logger_1.logger.registerSecrets(options.secrets);
|
|
130
|
+
// One run id for every job and called workflow of this run.
|
|
131
|
+
options = {
|
|
132
|
+
...options,
|
|
133
|
+
runId: options.runId || process.env.GITHUB_RUN_ID || String(Date.now())
|
|
134
|
+
};
|
|
135
|
+
// Steps never fall back to running on the host: without the requested
|
|
136
|
+
// container runtime, stop before anything runs. The host is opt-in
|
|
137
|
+
// (--container-runtime host).
|
|
138
|
+
const preference = options.containerRuntime ?? 'host';
|
|
139
|
+
if (preference !== 'host' && !(await (0, container_runtime_1.resolveContainerRuntime)(preference))) {
|
|
140
|
+
logger_1.logger.error((0, job_container_1.noRuntimeMessage)(preference));
|
|
141
|
+
return { success: false, jobs: [], duration: Date.now() - startTime };
|
|
142
|
+
}
|
|
143
|
+
const runDir = (0, job_runtime_1.createRunDir)();
|
|
144
|
+
try {
|
|
145
|
+
const parsed = (0, workflow_parser_1.parseFile)(filePath);
|
|
146
|
+
if (parsed.type === 'workflow') {
|
|
147
|
+
// Like GitHub, refuse a workflow with an invalid if: before any job.
|
|
148
|
+
const invalid = (0, validator_1.workflowExpressionErrors)(parsed.file);
|
|
149
|
+
if (invalid.length > 0) {
|
|
150
|
+
logger_1.logger.error(`Invalid workflow file: ${filePath}`);
|
|
151
|
+
for (const error of invalid) {
|
|
152
|
+
logger_1.logger.error(` ${error.path}: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
return { success: false, jobs: [], duration: Date.now() - startTime };
|
|
155
|
+
}
|
|
156
|
+
logger_1.logger.info(`Executing workflow: ${parsed.file.name}`);
|
|
157
|
+
options = {
|
|
158
|
+
...options,
|
|
159
|
+
inputs: {
|
|
160
|
+
...(0, github_context_1.declaredInputDefaults)(parsed.file.on),
|
|
161
|
+
...(options.inputs ?? {})
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const event = (0, github_context_1.loadEvent)(options, options.inputs ?? {}, parsed.file.on);
|
|
165
|
+
const inputs = (0, github_context_1.typedInputs)(parsed.file.on, options.inputs ?? {});
|
|
166
|
+
fs.writeFileSync(path.join(runDir, 'event.json'), JSON.stringify(event.payload));
|
|
167
|
+
const jobs = [];
|
|
168
|
+
const jobResults = new Map();
|
|
169
|
+
const jobOrder = (0, job_plan_1.topologicalJobOrder)(parsed.file.jobs);
|
|
170
|
+
for (const jobId of jobOrder) {
|
|
171
|
+
const job = parsed.file.jobs[jobId];
|
|
172
|
+
// Skip if specific job requested and this isn't it
|
|
173
|
+
if (options.job && jobId !== options.job) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const github = (0, github_context_1.buildGithubContext)(parsed.file.name, jobId, process.cwd(), event, options.secrets?.GITHUB_TOKEN, options.runId);
|
|
177
|
+
// `needs` results feed the job's `if:` (implicitly `success()`,
|
|
178
|
+
// which requires every dependency to have succeeded). With --job,
|
|
179
|
+
// dependencies aren't run and count as successful.
|
|
180
|
+
const needsIds = job.needs
|
|
181
|
+
? Array.isArray(job.needs)
|
|
182
|
+
? job.needs
|
|
183
|
+
: [job.needs]
|
|
184
|
+
: [];
|
|
185
|
+
const needs = {};
|
|
186
|
+
for (const dep of needsIds) {
|
|
187
|
+
needs[dep] = jobResults.get(dep) ?? {
|
|
188
|
+
result: 'success',
|
|
189
|
+
outputs: {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const vars = options.vars ?? {};
|
|
193
|
+
const jobIfCtx = {
|
|
194
|
+
...(0, expression_1.createContext)({}, inputs, options.secrets, github),
|
|
195
|
+
needs,
|
|
196
|
+
vars,
|
|
197
|
+
dependencies: {
|
|
198
|
+
allSucceeded: Object.values(needs).every((n) => n.result === 'success'),
|
|
199
|
+
anyFailed: Object.values(needs).some((n) => n.result === 'failure')
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
let runJob;
|
|
203
|
+
try {
|
|
204
|
+
runJob = (0, expression_1.evaluateCondition)((0, expression_1.withImplicitSuccess)(job.if), jobIfCtx);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// An if: that can't be evaluated fails the job, as on GitHub.
|
|
208
|
+
logger_1.logger.error((0, expression_1.ifEvaluationError)(`job '${jobId}'`, error));
|
|
209
|
+
jobs.push({ job: jobId, success: false, steps: [], duration: 0 });
|
|
210
|
+
jobResults.set(jobId, { result: 'failure', outputs: {} });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!runJob) {
|
|
214
|
+
const notOk = needsIds.find((dep) => needs[dep].result !== 'success');
|
|
215
|
+
logger_1.logger.info(notOk
|
|
216
|
+
? `Skipping job: ${jobId} (dependency "${notOk}" ${needs[notOk].result === 'skipped' ? 'was skipped' : 'did not succeed'})`
|
|
217
|
+
: `Skipping job: ${jobId} (condition not met)`);
|
|
218
|
+
jobResults.set(jobId, { result: 'skipped', outputs: {} });
|
|
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;
|
|
246
|
+
}
|
|
247
|
+
const matrixCombinations = (0, job_plan_1.expandMatrix)(job.strategy?.matrix, jobIfCtx);
|
|
248
|
+
const failFast = job.strategy?.['fail-fast'] !== false;
|
|
249
|
+
let matrixFailed = false;
|
|
250
|
+
const jobOutputs = {};
|
|
251
|
+
for (const [jobIndex, matrixValues] of matrixCombinations.entries()) {
|
|
252
|
+
if (matrixFailed && failFast) {
|
|
253
|
+
logger_1.logger.info(`Skipping remaining matrix jobs for ${jobId} (fail-fast)`);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
const matrixSuffix = matrixCombinations.length > 1
|
|
257
|
+
? ` (${(0, job_plan_1.matrixLabel)(matrixValues)})`
|
|
258
|
+
: '';
|
|
259
|
+
const jobLabel = `${jobId}${matrixSuffix}`;
|
|
260
|
+
const runtime = (0, job_runtime_1.createJobRuntime)(github, runDir, {
|
|
261
|
+
shell: job.defaults?.run?.shell ?? parsed.file.defaults?.run?.shell,
|
|
262
|
+
workingDirectory: job.defaults?.run?.['working-directory'] ??
|
|
263
|
+
parsed.file.defaults?.run?.['working-directory']
|
|
264
|
+
});
|
|
265
|
+
// Workflow-level env can't reference `env` itself; job-level env
|
|
266
|
+
// sees the workflow env plus matrix (matching GitHub's contexts).
|
|
267
|
+
const workflowEnv = {
|
|
268
|
+
...options.env,
|
|
269
|
+
...(0, expression_1.substituteRecord)(parsed.file.env, {
|
|
270
|
+
...(0, expression_1.createContext)({ ...options.env }, inputs, options.secrets, github),
|
|
271
|
+
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
|
+
});
|
|
309
|
+
matrixFailed = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!runtime.container) {
|
|
313
|
+
(0, actions_1.warnUnsandboxedActions)(job.steps ?? [], jobCtx, options);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await (0, actions_1.runPreSteps)(job.steps ?? [], options, runtime, jobCtx, stepResults);
|
|
317
|
+
// Execute each step
|
|
318
|
+
for (const step of job.steps || []) {
|
|
319
|
+
jobCtx.job = {
|
|
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);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
// An if: that can't be evaluated fails the step.
|
|
346
|
+
const message = (0, expression_1.ifEvaluationError)(`step '${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}'`, error);
|
|
347
|
+
logger_1.logger.error(message);
|
|
348
|
+
stepResults.push({
|
|
349
|
+
step,
|
|
350
|
+
success: false,
|
|
351
|
+
error: message,
|
|
352
|
+
duration: 0
|
|
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
|
|
372
|
+
};
|
|
373
|
+
stepResults.push(skippedResult);
|
|
374
|
+
if (step.id) {
|
|
375
|
+
jobCtx.steps[step.id] = {
|
|
376
|
+
outcome: 'skipped',
|
|
377
|
+
conclusion: 'skipped',
|
|
378
|
+
outputs: {}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// timeout-minutes: the step's own, or what's left of the job's.
|
|
384
|
+
const stepMinutes = (0, job_plan_1.numberSetting)(step['timeout-minutes'], stepCtx);
|
|
385
|
+
const jobLeft = jobDeadline !== undefined
|
|
386
|
+
? Math.max(jobDeadline - Date.now(), 1)
|
|
387
|
+
: undefined;
|
|
388
|
+
const stepMs = stepMinutes ? stepMinutes * 60000 : undefined;
|
|
389
|
+
runtime.stepTimeout =
|
|
390
|
+
jobLeft !== undefined &&
|
|
391
|
+
(stepMs === undefined || jobLeft < stepMs)
|
|
392
|
+
? { ms: jobLeft, message: jobTimeoutMessage }
|
|
393
|
+
: stepMs !== undefined
|
|
394
|
+
? {
|
|
395
|
+
ms: stepMs,
|
|
396
|
+
message: `The action '${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}' has timed out after ${stepMinutes} minutes.`
|
|
397
|
+
}
|
|
398
|
+
: undefined;
|
|
399
|
+
const stepResult = await (0, steps_1.executeStep)(step, options, runtime, stepCtx);
|
|
400
|
+
runtime.stepTimeout = undefined;
|
|
401
|
+
stepResults.push(stepResult);
|
|
402
|
+
if (jobDeadline !== undefined && Date.now() >= jobDeadline) {
|
|
403
|
+
// The job is cancelled: no further steps run.
|
|
404
|
+
if (stepResult.success) {
|
|
405
|
+
stepResult.success = false;
|
|
406
|
+
stepResult.error = jobTimeoutMessage;
|
|
407
|
+
}
|
|
408
|
+
logger_1.logger.error(jobTimeoutMessage);
|
|
409
|
+
jobCtx.jobFailed = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
const outcome = stepResult.success ? 'success' : 'failure';
|
|
413
|
+
if (step.id) {
|
|
414
|
+
jobCtx.steps[step.id] = {
|
|
415
|
+
outcome,
|
|
416
|
+
// continue-on-error turns a failure into a successful
|
|
417
|
+
// conclusion, which is what success()/failure() see.
|
|
418
|
+
conclusion: step['continue-on-error'] ? 'success' : outcome,
|
|
419
|
+
outputs: stepResult.outputs || {}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (!stepResult.success && !step['continue-on-error']) {
|
|
423
|
+
jobCtx.jobFailed = true;
|
|
424
|
+
logger_1.logger.error(`Step failed: ${(0, job_runtime_1.stepDisplayName)(step, stepCtx)}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
await (0, job_runtime_1.runPostSteps)(runtime, jobCtx, stepResults);
|
|
428
|
+
if (runtime.summary.trim()) {
|
|
429
|
+
logger_1.logger.info(`Job summary (${jobLabel}):\n${runtime.summary.trim()}`);
|
|
430
|
+
}
|
|
431
|
+
jobCtx.job = {
|
|
432
|
+
...jobCtx.job,
|
|
433
|
+
status: jobCtx.jobFailed ? 'failure' : 'success'
|
|
434
|
+
};
|
|
435
|
+
Object.assign(jobOutputs, (0, expression_1.substituteRecord)(job.outputs, {
|
|
436
|
+
...jobCtx,
|
|
437
|
+
env: { ...jobCtx.env, ...runtime.env }
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
await (0, job_container_1.stopJobContainer)(runtime);
|
|
442
|
+
}
|
|
443
|
+
const jobSuccess = stepResults.every((s) => s.success || s.step['continue-on-error']);
|
|
444
|
+
// Job-level continue-on-error (often `${{ matrix.experimental }}`)
|
|
445
|
+
// lets this combination fail without failing the job or run.
|
|
446
|
+
const allowedFailure = !jobSuccess && (0, job_plan_1.isTruthySetting)(job['continue-on-error'], jobCtx);
|
|
447
|
+
jobs.push({
|
|
448
|
+
job: jobLabel,
|
|
449
|
+
success: jobSuccess,
|
|
450
|
+
...(allowedFailure ? { continueOnError: true } : {}),
|
|
451
|
+
...(runtime.summary ? { summary: runtime.summary } : {}),
|
|
452
|
+
steps: stepResults,
|
|
453
|
+
outputs: { ...jobOutputs },
|
|
454
|
+
duration: Date.now() - jobStartTime
|
|
455
|
+
});
|
|
456
|
+
if (!jobSuccess && !allowedFailure) {
|
|
457
|
+
matrixFailed = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const jobSuccess = !matrixFailed;
|
|
461
|
+
jobResults.set(jobId, {
|
|
462
|
+
result: jobSuccess ? 'success' : 'failure',
|
|
463
|
+
outputs: jobOutputs
|
|
464
|
+
});
|
|
465
|
+
if (jobSuccess) {
|
|
466
|
+
logger_1.logger.success(`Job ${jobId} completed successfully`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const callOutputs = (0, github_context_1.workflowCallOutputs)(parsed.file.on);
|
|
470
|
+
return {
|
|
471
|
+
success: jobs.every((j) => j.success || j.continueOnError),
|
|
472
|
+
jobs,
|
|
473
|
+
duration: Date.now() - startTime,
|
|
474
|
+
...(callOutputs
|
|
475
|
+
? {
|
|
476
|
+
outputs: (0, expression_1.substituteRecord)(callOutputs, {
|
|
477
|
+
...(0, expression_1.createContext)({}, inputs, options.secrets),
|
|
478
|
+
vars: options.vars ?? {},
|
|
479
|
+
jobs: Object.fromEntries(jobResults)
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
: {})
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// Handle action execution
|
|
487
|
+
logger_1.logger.info(`Executing action: ${parsed.file.name}`);
|
|
488
|
+
if (parsed.file.runs.using === 'composite' && parsed.file.runs.steps) {
|
|
489
|
+
const stepResults = [];
|
|
490
|
+
const github = (0, github_context_1.buildGithubContext)(parsed.file.name, 'action', process.cwd(), undefined, options.secrets?.GITHUB_TOKEN, options.runId);
|
|
491
|
+
const runtime = (0, job_runtime_1.createJobRuntime)(github, runDir);
|
|
492
|
+
const actionCtx = (0, expression_1.createContext)({}, options.inputs, options.secrets, github);
|
|
493
|
+
if (!(await (0, job_container_1.startJobContainer)(undefined, actionCtx, options, runtime, runDir))) {
|
|
494
|
+
return { success: false, jobs: [], duration: Date.now() - startTime };
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
for (const step of parsed.file.runs.steps) {
|
|
498
|
+
const stepResult = await (0, steps_1.executeStep)(step, options, runtime);
|
|
499
|
+
stepResults.push(stepResult);
|
|
500
|
+
if (!stepResult.success) {
|
|
501
|
+
return {
|
|
502
|
+
success: false,
|
|
503
|
+
jobs: [
|
|
504
|
+
{
|
|
505
|
+
job: 'action',
|
|
506
|
+
success: false,
|
|
507
|
+
steps: stepResults,
|
|
508
|
+
duration: Date.now() - startTime
|
|
509
|
+
}
|
|
510
|
+
],
|
|
511
|
+
duration: Date.now() - startTime
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
success: true,
|
|
517
|
+
jobs: [
|
|
518
|
+
{
|
|
519
|
+
job: 'action',
|
|
520
|
+
success: true,
|
|
521
|
+
steps: stepResults,
|
|
522
|
+
duration: Date.now() - startTime
|
|
523
|
+
}
|
|
524
|
+
],
|
|
525
|
+
duration: Date.now() - startTime
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
await (0, job_container_1.stopJobContainer)(runtime);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
logger_1.logger.error(`Only composite actions can be run directly; call this ${parsed.file.runs.using} action from a workflow step instead.`);
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
jobs: [],
|
|
537
|
+
duration: Date.now() - startTime
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
logger_1.logger.error(`Execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
544
|
+
return {
|
|
545
|
+
success: false,
|
|
546
|
+
jobs: [],
|
|
547
|
+
duration: Date.now() - startTime
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
//# sourceMappingURL=executor.js.map
|