@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.
@@ -0,0 +1,495 @@
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.resolveActionInputs = resolveActionInputs;
37
+ exports.warnUnsandboxedActions = warnUnsandboxedActions;
38
+ exports.stepFileSystem = stepFileSystem;
39
+ exports.runBuiltinAction = runBuiltinAction;
40
+ exports.fetchActionOnce = fetchActionOnce;
41
+ exports.runPreSteps = runPreSteps;
42
+ exports.isLocalActionRef = isLocalActionRef;
43
+ exports.resolveLocalAction = resolveLocalAction;
44
+ exports.executeAction = executeAction;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const workflow_parser_1 = require("../parser/workflow-parser");
48
+ const logger_1 = require("../utils/logger");
49
+ const action_fetcher_1 = require("./action-fetcher");
50
+ const shell_1 = require("./shell");
51
+ const builtin_actions_1 = require("./builtin-actions");
52
+ const expression_1 = require("./expression");
53
+ const container_runtime_1 = require("./container-runtime");
54
+ const job_runtime_1 = require("./job-runtime");
55
+ const checkout_1 = require("./checkout");
56
+ const steps_1 = require("./steps");
57
+ const docker_action_1 = require("./docker-action");
58
+ const node_action_1 = require("./node-action");
59
+ const validator_1 = require("../parser/validator");
60
+ /**
61
+ * Build an action's `inputs.*`: the step's `with:` values (resolved against
62
+ * the calling step's context), falling back to each declared input's
63
+ * `default` (which may itself contain expressions, e.g. `${{ github.token }}`).
64
+ */
65
+ function resolveActionInputs(action, withValues, callerCtx) {
66
+ const inputs = {};
67
+ for (const [name, def] of Object.entries(action.inputs || {})) {
68
+ if (def?.default !== undefined) {
69
+ inputs[name] = (0, expression_1.substituteExpressions)(String(def.default), callerCtx);
70
+ }
71
+ }
72
+ return { ...inputs, ...(0, expression_1.substituteRecord)(withValues, callerCtx) };
73
+ }
74
+ /**
75
+ * Warn that a job's remote actions are about to run on the host, with the
76
+ * user's privileges (container mode off, or no runtime available). Local
77
+ * actions, `docker://` steps and a local checkout are left out.
78
+ */
79
+ function warnUnsandboxedActions(steps, ctx, options) {
80
+ const remote = steps
81
+ .filter((step) => step.uses &&
82
+ !isLocalActionRef(step.uses) &&
83
+ !step.uses.startsWith('docker://') &&
84
+ !(0, builtin_actions_1.builtinActionFor)(step.uses) &&
85
+ !(options.localCheckout !== false &&
86
+ (0, checkout_1.isLocalCheckout)(step.uses, step.with, ctx)))
87
+ .map((step) => step.uses);
88
+ if (remote.length === 0)
89
+ return;
90
+ logger_1.logger.warn(`Running remote actions on the host without container isolation: ${[...new Set(remote)].join(', ')}. They run with your user's privileges; use a container runtime (--container-runtime docker|podman|containerd) to sandbox them.`);
91
+ }
92
+ /** The file system a step sees: the host, or the job container. */
93
+ async function stepFileSystem(runtime) {
94
+ const container = runtime.container;
95
+ if (!container)
96
+ return (0, builtin_actions_1.hostFileSystem)(process.cwd());
97
+ if (container.home === undefined) {
98
+ const probe = await container.runtime.exec(container.id, {
99
+ command: 'printf %s "$HOME"'
100
+ });
101
+ container.home = (probe.success && probe.stdout.trim()) || '/root';
102
+ }
103
+ return (0, builtin_actions_1.containerFileSystem)({
104
+ workspace: container_runtime_1.CONTAINER_WORKDIR,
105
+ hostWorkspace: process.cwd(),
106
+ home: container.home,
107
+ copyFrom: (from, to) => container.runtime.copyFrom(container.id, from, to),
108
+ copyTo: (from, to) => container.runtime.copyTo(container.id, from, to),
109
+ mkdir: async (dir) => {
110
+ await container.runtime.exec(container.id, {
111
+ args: ['mkdir', '-p', dir]
112
+ });
113
+ }
114
+ });
115
+ }
116
+ /**
117
+ * Run actions/upload-artifact, download-artifact or cache locally (see
118
+ * builtin-actions.ts); actions/cache's save is queued as a post step.
119
+ */
120
+ async function runBuiltinAction(kind, step, ctx, options, runtime, startTime) {
121
+ const inputs = (0, expression_1.substituteRecord)(step.with, ctx);
122
+ const runId = String(ctx.github.run_id ?? options.runId ?? 'local');
123
+ const repository = String(ctx.github.repository ?? '');
124
+ const fsys = await stepFileSystem(runtime);
125
+ try {
126
+ const result = kind === 'upload-artifact'
127
+ ? await (0, builtin_actions_1.uploadArtifact)(fsys, inputs, runId)
128
+ : kind === 'download-artifact'
129
+ ? await (0, builtin_actions_1.downloadArtifact)(fsys, inputs, runId)
130
+ : await (0, builtin_actions_1.runCacheAction)(kind, fsys, inputs, repository);
131
+ if (result.warning)
132
+ logger_1.logger.warn(result.warning);
133
+ if (result.output)
134
+ logger_1.logger.info(result.output);
135
+ if (!result.success)
136
+ logger_1.logger.error(result.error ?? 'failed');
137
+ const post = result.post;
138
+ if (post) {
139
+ const postStep = { name: `Post ${step.name || step.uses}` };
140
+ runtime.postSteps.push({
141
+ step: postStep,
142
+ condition: 'success()',
143
+ run: async () => {
144
+ const postFs = await stepFileSystem(runtime);
145
+ try {
146
+ const saved = await post(postFs);
147
+ if (saved.warning)
148
+ logger_1.logger.warn(saved.warning);
149
+ if (saved.output)
150
+ logger_1.logger.info(saved.output);
151
+ return {
152
+ step: postStep,
153
+ success: saved.success,
154
+ output: saved.output,
155
+ error: saved.error,
156
+ duration: 0
157
+ };
158
+ }
159
+ finally {
160
+ postFs.cleanup();
161
+ }
162
+ }
163
+ });
164
+ }
165
+ return {
166
+ step,
167
+ success: result.success,
168
+ output: result.output,
169
+ outputs: result.outputs,
170
+ error: result.error,
171
+ duration: Date.now() - startTime
172
+ };
173
+ }
174
+ finally {
175
+ fsys.cleanup();
176
+ }
177
+ }
178
+ /** fetchAction, once per remote action per job. */
179
+ async function fetchActionOnce(uses, options, runtime) {
180
+ if (!runtime.actionPaths.has(uses)) {
181
+ runtime.actionPaths.set(uses, await (0, action_fetcher_1.fetchAction)(uses, { offline: options.offline }));
182
+ }
183
+ return runtime.actionPaths.get(uses) ?? null;
184
+ }
185
+ /**
186
+ * Run the `pre` scripts of the job's remote JavaScript actions at job
187
+ * start, before any step, as GitHub does (gated by `pre-if`, default
188
+ * `always()`). Their `main` later continues from the state they saved.
189
+ * Local actions usually come from the checkout, so their `pre` still runs
190
+ * right before their `main`. A failed `pre` fails the job.
191
+ */
192
+ async function runPreSteps(steps, options, runtime, jobCtx, stepResults) {
193
+ for (const step of steps) {
194
+ if (!step.uses ||
195
+ isLocalActionRef(step.uses) ||
196
+ step.uses.startsWith('docker://') ||
197
+ (0, builtin_actions_1.builtinActionFor)(step.uses)) {
198
+ continue;
199
+ }
200
+ const envCtx = {
201
+ ...jobCtx,
202
+ env: { ...jobCtx.env, ...runtime.env }
203
+ };
204
+ const stepCtx = {
205
+ ...envCtx,
206
+ env: { ...envCtx.env, ...(0, expression_1.substituteRecord)(step.env, envCtx) }
207
+ };
208
+ if (options.localCheckout !== false &&
209
+ (0, checkout_1.isLocalCheckout)(step.uses, step.with, stepCtx)) {
210
+ continue;
211
+ }
212
+ // Unresolvable actions are reported when their step runs.
213
+ const actionPath = await fetchActionOnce(step.uses, options, runtime);
214
+ if (!actionPath)
215
+ continue;
216
+ let action;
217
+ try {
218
+ const parsed = (0, workflow_parser_1.parseFile)(actionPath);
219
+ if (parsed.type !== 'action')
220
+ continue;
221
+ action = parsed.file;
222
+ }
223
+ catch {
224
+ continue;
225
+ }
226
+ if (!/^node\d+$/.test(action.runs.using) || !action.runs.pre)
227
+ continue;
228
+ let runPre;
229
+ try {
230
+ runPre = (0, node_action_1.runPreIf)(action, step, stepCtx);
231
+ }
232
+ catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ logger_1.logger.error(message);
235
+ stepResults.push({
236
+ step: { name: `Pre ${step.name || step.uses}` },
237
+ success: false,
238
+ error: message,
239
+ duration: 0
240
+ });
241
+ jobCtx.jobFailed = true;
242
+ continue;
243
+ }
244
+ if (!runPre)
245
+ continue;
246
+ logger_1.logger.info(`Running: Pre ${step.name || step.uses}`);
247
+ const result = await (0, node_action_1.executeNodeAction)(action, actionPath, step, resolveActionInputs(action, step.with, stepCtx), stepCtx, options, runtime, Date.now(), 'pre');
248
+ stepResults.push(result);
249
+ if (!result.success) {
250
+ jobCtx.jobFailed = true;
251
+ logger_1.logger.error(`Step failed: Pre ${step.name || step.uses}`);
252
+ }
253
+ }
254
+ }
255
+ function isLocalActionRef(uses) {
256
+ return uses.startsWith('./') || uses.startsWith('../');
257
+ }
258
+ /**
259
+ * Resolve a local action (`uses: ./path/to/action`) relative to the
260
+ * workspace, like the runner does, to its action.yml / action.yaml.
261
+ */
262
+ function resolveLocalAction(uses) {
263
+ const dir = path.resolve(process.cwd(), uses);
264
+ for (const name of ['action.yml', 'action.yaml']) {
265
+ const candidate = path.join(dir, name);
266
+ if (fs.existsSync(candidate))
267
+ return candidate;
268
+ }
269
+ return null;
270
+ }
271
+ /**
272
+ * Execute an action step: local (`./`), remote (`owner/repo@ref`) or
273
+ * `docker://image`.
274
+ */
275
+ async function executeAction(step, options, startTime, runtime, ctx) {
276
+ if (!step.uses) {
277
+ return {
278
+ step,
279
+ success: false,
280
+ error: 'No action specified',
281
+ duration: Date.now() - startTime
282
+ };
283
+ }
284
+ try {
285
+ const callerCtx = ctx ?? (0, expression_1.createContext)({}, {}, options.secrets);
286
+ // `uses: docker://image` runs the image directly, with `with.args` /
287
+ // `with.entrypoint` as its argv and entrypoint.
288
+ if (step.uses.startsWith('docker://')) {
289
+ const withValues = (0, expression_1.substituteRecord)(step.with, callerCtx);
290
+ const dockerAction = {
291
+ name: step.uses,
292
+ description: '',
293
+ runs: {
294
+ using: 'docker',
295
+ image: step.uses,
296
+ entrypoint: withValues.entrypoint,
297
+ args: withValues.args ? (0, shell_1.splitArgs)(withValues.args) : []
298
+ }
299
+ };
300
+ return await (0, docker_action_1.executeDockerAction)(dockerAction, path.join(process.cwd(), 'action.yml'), step, {}, callerCtx.env, options, runtime, startTime);
301
+ }
302
+ if (options.localCheckout !== false &&
303
+ (0, checkout_1.isLocalCheckout)(step.uses, step.with, callerCtx)) {
304
+ return (0, checkout_1.localCheckout)(step, callerCtx, runtime, startTime);
305
+ }
306
+ const builtin = (0, builtin_actions_1.builtinActionFor)(step.uses);
307
+ if (builtin) {
308
+ return await runBuiltinAction(builtin, step, callerCtx, options, runtime, startTime);
309
+ }
310
+ const actionPath = isLocalActionRef(step.uses)
311
+ ? resolveLocalAction(step.uses)
312
+ : await fetchActionOnce(step.uses, options, runtime);
313
+ if (!actionPath) {
314
+ const error = `Could not resolve action "${step.uses}"${isLocalActionRef(step.uses)
315
+ ? ` (no action.yml or action.yaml in ${path.resolve(process.cwd(), step.uses)})`
316
+ : ''}`;
317
+ logger_1.logger.error(error);
318
+ return {
319
+ step,
320
+ success: false,
321
+ error,
322
+ duration: Date.now() - startTime
323
+ };
324
+ }
325
+ // Parse the action file
326
+ const parsed = (0, workflow_parser_1.parseFile)(actionPath);
327
+ if (parsed.type !== 'action') {
328
+ logger_1.logger.error(`Invalid action file: ${actionPath}`);
329
+ return {
330
+ step,
331
+ success: false,
332
+ error: 'Invalid action file',
333
+ duration: Date.now() - startTime
334
+ };
335
+ }
336
+ const action = parsed.file;
337
+ // Resolve `with:` values against the calling step's context before they
338
+ // become the action's inputs (INPUT_* env vars / `inputs.*`).
339
+ const actionInputs = resolveActionInputs(action, step.with, callerCtx);
340
+ // Handle different action types
341
+ if (action.runs.using === 'composite') {
342
+ const invalid = (0, validator_1.actionExpressionErrors)(action);
343
+ if (invalid.length > 0) {
344
+ const error = `Invalid action file for ${step.uses}: ${invalid
345
+ .map((e) => `${e.path}: ${e.message}`)
346
+ .join('; ')}`;
347
+ logger_1.logger.error(error);
348
+ return {
349
+ step,
350
+ success: false,
351
+ error,
352
+ duration: Date.now() - startTime
353
+ };
354
+ }
355
+ // Execute composite action steps
356
+ logger_1.logger.info(`Executing composite action: ${action.name}`);
357
+ const compositeSteps = action.runs.steps || [];
358
+ const outputs = {};
359
+ // Composite steps see only the action's own inputs, plus the
360
+ // caller's env and github context - never the workflow's inputs.
361
+ // The action's files, as its steps see them (copied into the job
362
+ // container in container mode).
363
+ const actionDir = runtime.container
364
+ ? (0, job_runtime_1.actionDirInContainer)(runtime.container, path.dirname(actionPath))
365
+ : path.dirname(actionPath);
366
+ const callerActionEnv = runtime.actionEnv;
367
+ const parent = {
368
+ repository: callerActionEnv.GITHUB_ACTION_REPOSITORY ?? '',
369
+ ref: callerActionEnv.GITHUB_ACTION_REF ?? '',
370
+ path: actionDir
371
+ };
372
+ const actionCtx = {
373
+ ...(0, expression_1.createContext)({ ...callerCtx.env }, actionInputs, {}, { ...callerCtx.github, action_path: actionDir }),
374
+ secretsUnavailable: true,
375
+ vars: callerCtx.vars,
376
+ job: callerCtx.job,
377
+ runner: callerCtx.runner,
378
+ strategy: callerCtx.strategy
379
+ };
380
+ // `defaults.run` doesn't apply inside composite actions.
381
+ const callerDefaults = runtime.defaults;
382
+ runtime.defaults = {};
383
+ try {
384
+ for (const compositeStep of compositeSteps) {
385
+ runtime.actionEnv = (0, job_runtime_1.stepActionEnv)(runtime, compositeStep, parent);
386
+ const envCtx = {
387
+ ...actionCtx,
388
+ github: {
389
+ ...actionCtx.github,
390
+ action: runtime.actionEnv.GITHUB_ACTION
391
+ },
392
+ env: { ...actionCtx.env, ...runtime.env }
393
+ };
394
+ const stepCtx = {
395
+ ...envCtx,
396
+ env: {
397
+ ...envCtx.env,
398
+ ...(0, expression_1.substituteRecord)(compositeStep.env, envCtx)
399
+ }
400
+ };
401
+ let runCompositeStep;
402
+ try {
403
+ runCompositeStep = (0, expression_1.evaluateCondition)(compositeStep.if, stepCtx);
404
+ }
405
+ catch (error) {
406
+ const message = (0, expression_1.ifEvaluationError)(`step '${(0, job_runtime_1.stepDisplayName)(compositeStep, stepCtx)}' of ${step.uses}`, error);
407
+ logger_1.logger.error(message);
408
+ return {
409
+ step,
410
+ success: false,
411
+ error: message,
412
+ duration: Date.now() - startTime
413
+ };
414
+ }
415
+ if (!runCompositeStep) {
416
+ logger_1.logger.info(`Skipping composite step: ${(0, job_runtime_1.stepDisplayName)(compositeStep, stepCtx)} (condition not met)`);
417
+ if (compositeStep.id) {
418
+ actionCtx.steps[compositeStep.id] = {
419
+ outcome: 'skipped',
420
+ outputs: {}
421
+ };
422
+ }
423
+ continue;
424
+ }
425
+ const stepOptions = {
426
+ ...options,
427
+ inputs: actionInputs
428
+ };
429
+ const stepResult = await (0, steps_1.executeStep)(compositeStep, stepOptions, runtime, stepCtx);
430
+ if (compositeStep.id) {
431
+ actionCtx.steps[compositeStep.id] = {
432
+ outcome: stepResult.success ? 'success' : 'failure',
433
+ outputs: stepResult.outputs || {}
434
+ };
435
+ }
436
+ if (!stepResult.success) {
437
+ actionCtx.jobFailed = true;
438
+ return {
439
+ step,
440
+ success: false,
441
+ error: `Composite action step failed: ${(0, job_runtime_1.stepDisplayName)(compositeStep, stepCtx)}`,
442
+ duration: Date.now() - startTime
443
+ };
444
+ }
445
+ }
446
+ }
447
+ finally {
448
+ runtime.defaults = callerDefaults;
449
+ runtime.actionEnv = callerActionEnv;
450
+ }
451
+ // Resolve the action's declared outputs (e.g.
452
+ // `outputs.greeting.value: ${{ steps.greet.outputs.greeting }}`)
453
+ // against the accumulated step outcomes/outputs.
454
+ for (const [outputName, outputDef] of Object.entries(action.outputs || {})) {
455
+ const resolved = (0, expression_1.resolveExpressionValue)(outputDef.value, actionCtx);
456
+ if (resolved !== undefined) {
457
+ outputs[outputName] = String(resolved);
458
+ }
459
+ }
460
+ return {
461
+ step,
462
+ success: true,
463
+ output: JSON.stringify(outputs),
464
+ outputs,
465
+ duration: Date.now() - startTime
466
+ };
467
+ }
468
+ else if (/^node\d+$/.test(action.runs.using)) {
469
+ return await (0, node_action_1.executeNodeAction)(action, actionPath, step, actionInputs, callerCtx, options, runtime, startTime);
470
+ }
471
+ else if (action.runs.using === 'docker') {
472
+ return await (0, docker_action_1.executeDockerAction)(action, actionPath, step, actionInputs, callerCtx.env, options, runtime, startTime);
473
+ }
474
+ else {
475
+ const error = `Unsupported action type "${action.runs.using}" in ${step.uses}`;
476
+ logger_1.logger.error(error);
477
+ return {
478
+ step,
479
+ success: false,
480
+ error,
481
+ duration: Date.now() - startTime
482
+ };
483
+ }
484
+ }
485
+ catch (error) {
486
+ logger_1.logger.error(`Action execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
487
+ return {
488
+ step,
489
+ success: false,
490
+ error: error instanceof Error ? error.message : 'Unknown error',
491
+ duration: Date.now() - startTime
492
+ };
493
+ }
494
+ }
495
+ //# sourceMappingURL=actions.js.map