@secureci/cli 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sameValue = sameValue;
4
+ exports.matrixLabel = matrixLabel;
5
+ exports.isTruthySetting = isTruthySetting;
6
+ exports.numberSetting = numberSetting;
7
+ exports.matrixValue = matrixValue;
8
+ exports.expandMatrix = expandMatrix;
9
+ exports.topologicalJobOrder = topologicalJobOrder;
10
+ const logger_1 = require("../utils/logger");
11
+ const expression_1 = require("./expression");
12
+ /** Deep equality for matrix values (JSON-like data). */
13
+ function sameValue(a, b) {
14
+ if (a === b)
15
+ return true;
16
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
17
+ return false;
18
+ if (Array.isArray(a) !== Array.isArray(b))
19
+ return false;
20
+ const keysA = Object.keys(a);
21
+ const keysB = Object.keys(b);
22
+ return (keysA.length === keysB.length &&
23
+ keysA.every((key) => sameValue(a[key], b[key])));
24
+ }
25
+ /** `key: value, ...` for a matrix combination's job label. */
26
+ function matrixLabel(values) {
27
+ return Object.entries(values)
28
+ .map(([k, v]) => `${k}: ${v && typeof v === 'object' ? JSON.stringify(v) : v}`)
29
+ .join(', ');
30
+ }
31
+ /** A boolean setting that may be an expression (`continue-on-error`). */
32
+ function isTruthySetting(value, ctx) {
33
+ const resolved = matrixValue(value, ctx);
34
+ return resolved === true || resolved === 'true';
35
+ }
36
+ /** A positive number setting (e.g. timeout-minutes), possibly an expression. */
37
+ function numberSetting(value, ctx) {
38
+ if (value === undefined || value === null || value === '')
39
+ return undefined;
40
+ const n = Number(matrixValue(value, ctx));
41
+ return Number.isFinite(n) && n > 0 ? n : undefined;
42
+ }
43
+ /** A `${{ }}` string resolved to its value (e.g. via fromJSON), else as is. */
44
+ function matrixValue(value, ctx) {
45
+ if (typeof value !== 'string' || !/^\s*\$\{\{[\s\S]*\}\}\s*$/.test(value)) {
46
+ return value;
47
+ }
48
+ return (0, expression_1.resolveExpressionValue)(value, ctx);
49
+ }
50
+ /**
51
+ * Expand a job's `strategy.matrix` into its combinations, as GitHub does:
52
+ * the cartesian product of the axes, minus combinations matching an
53
+ * `exclude` entry (on all of its keys), then each `include` entry is
54
+ * merged into every combination whose original axis values it doesn't
55
+ * change (values added by an earlier include may be overwritten), or
56
+ * added as a new combination if it fits none. The matrix, or any axis,
57
+ * may be an expression such as `${{ fromJSON(needs.x.outputs.m) }}`.
58
+ */
59
+ function expandMatrix(rawMatrix, ctx) {
60
+ const matrix = matrixValue(rawMatrix, ctx);
61
+ if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) {
62
+ return [{}];
63
+ }
64
+ const entries = (key) => {
65
+ const list = matrixValue(matrix[key], ctx);
66
+ return Array.isArray(list)
67
+ ? list.filter((e) => !!e && typeof e === 'object')
68
+ : [];
69
+ };
70
+ const include = entries('include');
71
+ const exclude = entries('exclude');
72
+ const axes = Object.entries(matrix)
73
+ .filter(([key]) => key !== 'include' && key !== 'exclude')
74
+ .map(([key, raw]) => {
75
+ const values = matrixValue(raw, ctx);
76
+ return [key, Array.isArray(values) ? values : [values]];
77
+ });
78
+ const axisKeys = new Set(axes.map(([key]) => key));
79
+ let combinations = axes.length ? [{}] : [];
80
+ for (const [key, values] of axes) {
81
+ combinations = combinations.flatMap((combo) => values.map((value) => ({ ...combo, [key]: value })));
82
+ }
83
+ combinations = combinations.filter((combo) => !exclude.some((entry) => Object.entries(entry).every(([key, value]) => sameValue(combo[key], value))));
84
+ const added = [];
85
+ for (const entry of include) {
86
+ let merged = false;
87
+ for (const combo of combinations) {
88
+ const fits = Object.entries(entry).every(([key, value]) => !axisKeys.has(key) || sameValue(combo[key], value));
89
+ if (fits) {
90
+ Object.assign(combo, entry);
91
+ merged = true;
92
+ }
93
+ }
94
+ if (!merged)
95
+ added.push({ ...entry });
96
+ }
97
+ combinations.push(...added);
98
+ return combinations.length ? combinations : [{}];
99
+ }
100
+ /**
101
+ * Order job IDs so dependencies (via `needs`) run before dependents, using
102
+ * Kahn's algorithm. Falls back to declaration order (with a warning) if a
103
+ * cycle is detected, since that indicates an invalid workflow.
104
+ */
105
+ function topologicalJobOrder(jobs) {
106
+ const jobIds = Object.keys(jobs);
107
+ const needsOf = (id) => {
108
+ const needs = jobs[id].needs;
109
+ if (!needs)
110
+ return [];
111
+ return Array.isArray(needs) ? needs : [needs];
112
+ };
113
+ const inDegree = new Map();
114
+ for (const id of jobIds) {
115
+ inDegree.set(id, needsOf(id).filter((dep) => jobIds.includes(dep)).length);
116
+ }
117
+ const queue = jobIds.filter((id) => inDegree.get(id) === 0);
118
+ const ordered = [];
119
+ while (queue.length > 0) {
120
+ const id = queue.shift();
121
+ ordered.push(id);
122
+ for (const other of jobIds) {
123
+ if (needsOf(other).includes(id)) {
124
+ const remaining = (inDegree.get(other) || 0) - 1;
125
+ inDegree.set(other, remaining);
126
+ if (remaining === 0) {
127
+ queue.push(other);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ if (ordered.length !== jobIds.length) {
133
+ logger_1.logger.warn('Detected a cycle in job "needs" dependencies; falling back to declaration order.');
134
+ return jobIds;
135
+ }
136
+ return ordered;
137
+ }
138
+ //# sourceMappingURL=job-plan.js.map
@@ -0,0 +1,338 @@
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.createContainerStepFiles = createContainerStepFiles;
37
+ exports.killStepProcesses = killStepProcesses;
38
+ exports.containerPathPrefix = containerPathPrefix;
39
+ exports.createRunDir = createRunDir;
40
+ exports.runnerContext = runnerContext;
41
+ exports.hostHasBash = hostHasBash;
42
+ exports.createJobRuntime = createJobRuntime;
43
+ exports.runPostSteps = runPostSteps;
44
+ exports.fileCommandEnv = fileCommandEnv;
45
+ exports.containerRunnerEnv = containerRunnerEnv;
46
+ exports.finishStepOutput = finishStepOutput;
47
+ exports.stepActionEnv = stepActionEnv;
48
+ exports.actionDirInContainer = actionDirInContainer;
49
+ exports.effectiveTimeout = effectiveTimeout;
50
+ exports.stepDisplayName = stepDisplayName;
51
+ const crypto = __importStar(require("crypto"));
52
+ const fs = __importStar(require("fs"));
53
+ const os = __importStar(require("os"));
54
+ const path = __importStar(require("path"));
55
+ const logger_1 = require("../utils/logger");
56
+ const child_process_1 = require("child_process");
57
+ const expression_1 = require("./expression");
58
+ const container_runtime_1 = require("./container-runtime");
59
+ const runner_env_1 = require("./runner-env");
60
+ /** Step files inside the job container's exchange dir, writable from it. */
61
+ function createContainerStepFiles(container) {
62
+ const dir = fs.mkdtempSync(path.join(container.exchangeDir, 'step-'));
63
+ const files = (0, runner_env_1.createStepFiles)(dir);
64
+ fs.chmodSync(dir, 0o777);
65
+ for (const file of [
66
+ files.output,
67
+ files.env,
68
+ files.path,
69
+ files.state,
70
+ files.summary
71
+ ]) {
72
+ fs.chmodSync(file, 0o666);
73
+ }
74
+ return {
75
+ files,
76
+ containerDir: `${container_runtime_1.CONTAINER_EXCHANGE_DIR}/${path.basename(dir)}`
77
+ };
78
+ }
79
+ /**
80
+ * Kill what a timed-out step left running in the job container: every
81
+ * process whose environment carries the step's SECURECI_STEP marker
82
+ * (children inherit it). Killing the container client alone leaves the
83
+ * command running until the container is removed.
84
+ */
85
+ async function killStepProcesses(container, marker) {
86
+ await container.runtime.exec(container.id, {
87
+ command: `for p in /proc/[0-9]*; do tr '\\0' '\\n' < "$p/environ" 2>/dev/null | grep -qx 'SECURECI_STEP=${marker}' && kill -9 "\${p#/proc/}" 2>/dev/null; done; true`
88
+ });
89
+ }
90
+ /** `export PATH=...` prefix applying `$GITHUB_PATH` additions in a shell. */
91
+ function containerPathPrefix(runtime) {
92
+ return runtime.path.length
93
+ ? `export PATH="${runtime.path.join(':')}:$PATH"\n`
94
+ : '';
95
+ }
96
+ /**
97
+ * Create the per-run scratch directory holding the event payload
98
+ * (`$GITHUB_EVENT_PATH`) and `$RUNNER_TEMP`.
99
+ */
100
+ function createRunDir() {
101
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'secureci-run-'));
102
+ fs.writeFileSync(path.join(dir, 'event.json'), '{}');
103
+ fs.mkdirSync(path.join(dir, 'tmp'));
104
+ return dir;
105
+ }
106
+ /** The `runner` context, derived from the runner env. */
107
+ function runnerContext(runtime) {
108
+ return {
109
+ name: 'secureci',
110
+ os: runtime.runnerEnv.RUNNER_OS,
111
+ arch: process.arch === 'arm64' ? 'ARM64' : 'X64',
112
+ temp: runtime.runnerEnv.RUNNER_TEMP,
113
+ tool_cache: runtime.runnerEnv.RUNNER_TOOL_CACHE
114
+ };
115
+ }
116
+ let hostBash;
117
+ /** Whether bash is available on the host (decides the default shell). */
118
+ function hostHasBash() {
119
+ if (hostBash === undefined) {
120
+ try {
121
+ (0, child_process_1.execSync)('command -v bash', { stdio: 'ignore' });
122
+ hostBash = true;
123
+ }
124
+ catch {
125
+ hostBash = false;
126
+ }
127
+ }
128
+ return hostBash;
129
+ }
130
+ function createJobRuntime(github, runDir, defaults = {}) {
131
+ const homeDir = process.env.HOME || process.env.USERPROFILE || os.tmpdir();
132
+ const eventPath = path.join(runDir, 'event.json');
133
+ return {
134
+ runnerEnv: (0, runner_env_1.buildRunnerEnv)(github, {
135
+ workspace: process.cwd(),
136
+ eventPath,
137
+ tempDir: path.join(runDir, 'tmp'),
138
+ toolCache: path.join(homeDir, '.secureci', 'toolcache')
139
+ }),
140
+ eventPath,
141
+ env: {},
142
+ path: [],
143
+ postSteps: [],
144
+ defaults,
145
+ summary: '',
146
+ actionEnv: {},
147
+ actionNames: new Map(),
148
+ preState: new Map(),
149
+ actionPaths: new Map()
150
+ };
151
+ }
152
+ /**
153
+ * Run the job's queued `post:` steps in reverse registration order, each
154
+ * gated on its `post-if` condition (default `always()`).
155
+ */
156
+ async function runPostSteps(runtime, ctx, stepResults) {
157
+ for (const post of [...runtime.postSteps].reverse()) {
158
+ let run;
159
+ try {
160
+ run = (0, expression_1.evaluateCondition)(post.condition, ctx);
161
+ }
162
+ catch (error) {
163
+ const message = (0, expression_1.ifEvaluationError)(`step '${post.step.name}'`, error);
164
+ logger_1.logger.error(message);
165
+ stepResults.push({
166
+ step: post.step,
167
+ success: false,
168
+ error: message,
169
+ duration: 0
170
+ });
171
+ ctx.jobFailed = true;
172
+ continue;
173
+ }
174
+ if (!run) {
175
+ logger_1.logger.info(`Skipping step: ${post.step.name ?? 'post'} (condition not met)`);
176
+ continue;
177
+ }
178
+ logger_1.logger.info(`Running: ${post.step.name}`);
179
+ const result = await post.run();
180
+ stepResults.push(result);
181
+ if (!result.success) {
182
+ ctx.jobFailed = true;
183
+ }
184
+ }
185
+ runtime.postSteps = [];
186
+ }
187
+ /** `GITHUB_OUTPUT` / `GITHUB_ENV` / ... pointing at a step's files. */
188
+ function fileCommandEnv(files, containerDir) {
189
+ const at = (file) => containerDir ? `${containerDir}/${path.basename(file)}` : file;
190
+ return {
191
+ GITHUB_OUTPUT: at(files.output),
192
+ GITHUB_ENV: at(files.env),
193
+ GITHUB_PATH: at(files.path),
194
+ GITHUB_STATE: at(files.state),
195
+ GITHUB_STEP_SUMMARY: at(files.summary)
196
+ };
197
+ }
198
+ /**
199
+ * The runner env as seen from inside a container: workspace and event
200
+ * payload at their mounted paths (the event payload is copied into the
201
+ * exchange dir's root).
202
+ */
203
+ function containerRunnerEnv(runtime) {
204
+ return {
205
+ ...runtime.runnerEnv,
206
+ GITHUB_WORKSPACE: container_runtime_1.CONTAINER_WORKDIR,
207
+ GITHUB_EVENT_PATH: `${container_runtime_1.CONTAINER_EXCHANGE_DIR}/event.json`,
208
+ RUNNER_TEMP: '/tmp',
209
+ RUNNER_TOOL_CACHE: '/opt/hostedtoolcache'
210
+ };
211
+ }
212
+ /**
213
+ * Handle a finished step's stdout and file commands: register
214
+ * `::add-mask::` values before anything is logged, log the remaining
215
+ * output, and fold `$GITHUB_ENV` / `$GITHUB_PATH` changes into the job
216
+ * runtime. Returns the step's outputs and saved state.
217
+ */
218
+ function finishStepOutput(stdout, files, runtime) {
219
+ const commands = (0, runner_env_1.processWorkflowCommands)(stdout || '');
220
+ commands.masks.forEach((mask, i) => logger_1.logger.registerSecrets({ [`mask-${i}`]: mask }));
221
+ for (const annotation of commands.annotations) {
222
+ if (annotation.level === 'warning')
223
+ logger_1.logger.warn(annotation.message);
224
+ else if (annotation.level === 'error')
225
+ logger_1.logger.error(annotation.message);
226
+ else if (annotation.level === 'debug')
227
+ logger_1.logger.debug(annotation.message);
228
+ else if (annotation.level === 'group')
229
+ logger_1.logger.info(`▸ ${annotation.message}`);
230
+ else
231
+ logger_1.logger.info(annotation.message);
232
+ }
233
+ const text = commands.text.trim();
234
+ if (text) {
235
+ logger_1.logger.info(text);
236
+ }
237
+ const fileResults = (0, runner_env_1.readStepFiles)(files);
238
+ runtime.summary += fileResults.summary;
239
+ Object.assign(runtime.env, fileResults.env);
240
+ runtime.path.unshift(...[...fileResults.path].reverse());
241
+ return {
242
+ outputs: { ...fileResults.outputs, ...commands.outputs },
243
+ state: { ...fileResults.state, ...commands.state }
244
+ };
245
+ }
246
+ /**
247
+ * The runner's per-step GITHUB_ACTION variables: GITHUB_ACTION is the
248
+ * step's id, else `__run` / `__owner_repo` (numbered `_2`, `_3`... when
249
+ * repeated); a remote `uses:` sets GITHUB_ACTION_REPOSITORY / _REF. Inside
250
+ * a composite action, steps also get its GITHUB_ACTION_PATH, and run steps
251
+ * its repository and ref.
252
+ */
253
+ function stepActionEnv(runtime, step, parent) {
254
+ let name = step.id;
255
+ if (!name) {
256
+ const base = step.uses
257
+ ? `__${step.uses
258
+ .replace(/@.*$/, '')
259
+ .replace(/[^A-Za-z0-9]+/g, '_')
260
+ .replace(/^_+|_+$/g, '')}`
261
+ : '__run';
262
+ const count = (runtime.actionNames.get(base) ?? 0) + 1;
263
+ runtime.actionNames.set(base, count);
264
+ name = count === 1 ? base : `${base}_${count}`;
265
+ }
266
+ const remote = step.uses?.match(/^([^./][^/@]*\/[^/@]+)(?:\/[^@]*)?@(.+)$/);
267
+ const env = {
268
+ GITHUB_ACTION: name,
269
+ GITHUB_ACTION_REPOSITORY: remote
270
+ ? remote[1]
271
+ : step.uses
272
+ ? ''
273
+ : (parent?.repository ?? ''),
274
+ GITHUB_ACTION_REF: remote ? remote[2] : step.uses ? '' : (parent?.ref ?? '')
275
+ };
276
+ if (parent)
277
+ env.GITHUB_ACTION_PATH = parent.path;
278
+ return env;
279
+ }
280
+ /**
281
+ * An action directory's path inside the job container: copied once into
282
+ * the mounted exchange dir (the action cache isn't mounted).
283
+ */
284
+ function actionDirInContainer(container, actionDir) {
285
+ let containerDir = container.actionDirs.get(actionDir);
286
+ if (!containerDir) {
287
+ const hash = crypto
288
+ .createHash('sha1')
289
+ .update(actionDir)
290
+ .digest('hex')
291
+ .slice(0, 12);
292
+ fs.cpSync(actionDir, path.join(container.exchangeDir, 'actions', hash), {
293
+ recursive: true
294
+ });
295
+ containerDir = `${container_runtime_1.CONTAINER_EXCHANGE_DIR}/actions/${hash}`;
296
+ container.actionDirs.set(actionDir, containerDir);
297
+ }
298
+ return containerDir;
299
+ }
300
+ /**
301
+ * The time limit for the running step: its (or its job's) timeout-minutes,
302
+ * or --container-timeout in container mode, whichever is shorter, with the
303
+ * message to fail it with.
304
+ */
305
+ function effectiveTimeout(runtime, containerTimeoutMs, inContainer = !!runtime.container) {
306
+ const own = runtime.stepTimeout;
307
+ const flag = inContainer && containerTimeoutMs ? containerTimeoutMs : undefined;
308
+ if (own && (flag === undefined || own.ms <= flag))
309
+ return own;
310
+ if (flag !== undefined) {
311
+ return {
312
+ ms: flag,
313
+ message: `Timed out after ${flag}ms (--container-timeout)`
314
+ };
315
+ }
316
+ return { message: '' };
317
+ }
318
+ /**
319
+ * The label shown for a step, like GitHub: its `name` with `${{ }}`
320
+ * expressions resolved, else its `uses:` reference, else the first
321
+ * non-empty line of its `run:` script, else "step".
322
+ */
323
+ function stepDisplayName(step, ctx) {
324
+ if (step.name)
325
+ return ctx ? (0, expression_1.substituteExpressions)(step.name, ctx) : step.name;
326
+ if (step.uses)
327
+ return step.uses;
328
+ if (step.run) {
329
+ const line = step.run
330
+ .split('\n')
331
+ .map((l) => l.trim())
332
+ .find(Boolean);
333
+ if (line)
334
+ return ctx ? (0, expression_1.substituteExpressions)(line, ctx) : line;
335
+ }
336
+ return 'step';
337
+ }
338
+ //# sourceMappingURL=job-runtime.js.map