@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,374 @@
|
|
|
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.CONTAINER_EXCHANGE_DIR = exports.CONTAINER_WORKDIR = void 0;
|
|
37
|
+
exports.resolveContainerRuntime = resolveContainerRuntime;
|
|
38
|
+
exports._resetRuntimeCacheForTests = _resetRuntimeCacheForTests;
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const util_1 = require("util");
|
|
44
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
45
|
+
/** The CLI binary invoked for each runtime. `containerd` has no Docker-CLI-
|
|
46
|
+
* compatible binary of its own, so `nerdctl` is used as its front-end. */
|
|
47
|
+
const RUNTIME_BINARIES = {
|
|
48
|
+
docker: 'docker',
|
|
49
|
+
podman: 'podman',
|
|
50
|
+
containerd: 'nerdctl'
|
|
51
|
+
};
|
|
52
|
+
exports.CONTAINER_WORKDIR = '/secureci-workspace';
|
|
53
|
+
exports.CONTAINER_EXCHANGE_DIR = '/secureci-exchange';
|
|
54
|
+
/**
|
|
55
|
+
* Whether a variable can go in an `--env-file` verbatim. Env files are
|
|
56
|
+
* line-based, and podman strips quotes from and trims values, so anything
|
|
57
|
+
* multi-line, quote-leading or whitespace-padded is passed another way.
|
|
58
|
+
*/
|
|
59
|
+
function fitsEnvFile(key, value) {
|
|
60
|
+
return (!/[\s#]/.test(key) &&
|
|
61
|
+
!/[\r\n]/.test(value) &&
|
|
62
|
+
!/^["']/.test(value) &&
|
|
63
|
+
value === value.trim());
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the env arguments for `run` / `exec`. Plain values go in an env file
|
|
67
|
+
* (mode 0600, in a private temp dir) passed with `--env-file`. The rest are
|
|
68
|
+
* passed as `-e KEY` with the value in the client's environment; only those
|
|
69
|
+
* keys are added to it, so a `PATH` or `HOME` meant for the container
|
|
70
|
+
* doesn't change how the client itself runs.
|
|
71
|
+
*/
|
|
72
|
+
function envTransport(env = {}) {
|
|
73
|
+
const fileLines = [];
|
|
74
|
+
const viaClient = {};
|
|
75
|
+
for (const [key, value] of Object.entries(env)) {
|
|
76
|
+
if (fitsEnvFile(key, value))
|
|
77
|
+
fileLines.push(`${key}=${value}\n`);
|
|
78
|
+
else
|
|
79
|
+
viaClient[key] = value;
|
|
80
|
+
}
|
|
81
|
+
const args = [];
|
|
82
|
+
let dir;
|
|
83
|
+
if (fileLines.length > 0) {
|
|
84
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'secureci-env-'));
|
|
85
|
+
const file = path.join(dir, 'env');
|
|
86
|
+
fs.writeFileSync(file, fileLines.join(''), { mode: 0o600 });
|
|
87
|
+
args.push('--env-file', file);
|
|
88
|
+
}
|
|
89
|
+
const clientKeys = Object.keys(viaClient);
|
|
90
|
+
for (const key of clientKeys)
|
|
91
|
+
args.push('-e', key);
|
|
92
|
+
return {
|
|
93
|
+
args,
|
|
94
|
+
clientEnv: clientKeys.length > 0 ? { ...process.env, ...viaClient } : undefined,
|
|
95
|
+
cleanup: () => {
|
|
96
|
+
if (dir)
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Probe timeout for `<bin> version` availability checks. */
|
|
102
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
103
|
+
class CliContainerRuntime {
|
|
104
|
+
constructor(name) {
|
|
105
|
+
this.name = name;
|
|
106
|
+
this.binary = RUNTIME_BINARIES[name];
|
|
107
|
+
}
|
|
108
|
+
async isAvailable() {
|
|
109
|
+
if (this.availableCache !== undefined)
|
|
110
|
+
return this.availableCache;
|
|
111
|
+
try {
|
|
112
|
+
// Plain `version` (no --format) to stay compatible across engines
|
|
113
|
+
// (nerdctl's version output format differs from docker/podman's);
|
|
114
|
+
// we only care whether the daemon responds, not the output.
|
|
115
|
+
await execAsync(`${this.binary} version`, {
|
|
116
|
+
timeout: PROBE_TIMEOUT_MS
|
|
117
|
+
});
|
|
118
|
+
this.availableCache = true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.availableCache = false;
|
|
122
|
+
}
|
|
123
|
+
return this.availableCache;
|
|
124
|
+
}
|
|
125
|
+
execute(options) {
|
|
126
|
+
const args = [
|
|
127
|
+
'run',
|
|
128
|
+
'--rm',
|
|
129
|
+
'-w',
|
|
130
|
+
exports.CONTAINER_WORKDIR,
|
|
131
|
+
'-v',
|
|
132
|
+
`${options.workingDir}:${exports.CONTAINER_WORKDIR}`,
|
|
133
|
+
'-v',
|
|
134
|
+
`${options.exchangeDir}:${exports.CONTAINER_EXCHANGE_DIR}`
|
|
135
|
+
];
|
|
136
|
+
if (options.networkDisabled === true) {
|
|
137
|
+
args.push('--network', 'none');
|
|
138
|
+
}
|
|
139
|
+
if (options.memoryLimit) {
|
|
140
|
+
args.push('--memory', options.memoryLimit);
|
|
141
|
+
}
|
|
142
|
+
if (options.entrypoint) {
|
|
143
|
+
args.push('--entrypoint', options.entrypoint);
|
|
144
|
+
}
|
|
145
|
+
const env = envTransport(options.env);
|
|
146
|
+
args.push(...env.args);
|
|
147
|
+
args.push(options.image);
|
|
148
|
+
if (options.args) {
|
|
149
|
+
args.push(...options.args);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
args.push('sh', '-c', options.command || '');
|
|
153
|
+
}
|
|
154
|
+
return this.runCli(args, options.timeoutMs, env);
|
|
155
|
+
}
|
|
156
|
+
start(options) {
|
|
157
|
+
const args = ['run', '-d'];
|
|
158
|
+
if (options.workingDir) {
|
|
159
|
+
args.push('-w', exports.CONTAINER_WORKDIR, '-v', `${options.workingDir}:${exports.CONTAINER_WORKDIR}`);
|
|
160
|
+
}
|
|
161
|
+
if (options.exchangeDir) {
|
|
162
|
+
args.push('-v', `${options.exchangeDir}:${exports.CONTAINER_EXCHANGE_DIR}`);
|
|
163
|
+
}
|
|
164
|
+
if (options.networkDisabled === true) {
|
|
165
|
+
args.push('--network', 'none');
|
|
166
|
+
}
|
|
167
|
+
else if (options.network) {
|
|
168
|
+
args.push('--network', options.network);
|
|
169
|
+
if (options.networkAlias) {
|
|
170
|
+
args.push('--network-alias', options.networkAlias);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (options.memoryLimit) {
|
|
174
|
+
args.push('--memory', options.memoryLimit);
|
|
175
|
+
}
|
|
176
|
+
for (const port of options.ports ?? [])
|
|
177
|
+
args.push('-p', port);
|
|
178
|
+
for (const volume of options.volumes ?? [])
|
|
179
|
+
args.push('-v', volume);
|
|
180
|
+
args.push(...(options.extraArgs ?? []));
|
|
181
|
+
const env = envTransport(options.env);
|
|
182
|
+
args.push(...env.args);
|
|
183
|
+
if (options.service) {
|
|
184
|
+
args.push(options.image);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Idle forever; steps are run in it via `exec` and it's removed at
|
|
188
|
+
// job end. `tail` exists in practically every image (busybox too).
|
|
189
|
+
args.push('--entrypoint', 'tail', options.image, '-f', '/dev/null');
|
|
190
|
+
}
|
|
191
|
+
return this.runCli(args, undefined, env).then((result) => result.success
|
|
192
|
+
? { success: true, id: result.stdout.trim() }
|
|
193
|
+
: {
|
|
194
|
+
success: false,
|
|
195
|
+
error: result.stderr.trim() || `exit code ${result.exitCode}`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
exec(id, options) {
|
|
199
|
+
const env = envTransport(options.env);
|
|
200
|
+
const args = [
|
|
201
|
+
'exec',
|
|
202
|
+
'-w',
|
|
203
|
+
options.workingDir || exports.CONTAINER_WORKDIR,
|
|
204
|
+
...env.args
|
|
205
|
+
];
|
|
206
|
+
if (options.args) {
|
|
207
|
+
args.push(id, ...options.args);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
args.push(id, 'sh', '-c', options.command || '');
|
|
211
|
+
}
|
|
212
|
+
return this.runCli(args, options.timeoutMs, env);
|
|
213
|
+
}
|
|
214
|
+
async createNetwork(name) {
|
|
215
|
+
return (await this.runCli(['network', 'create', name])).success;
|
|
216
|
+
}
|
|
217
|
+
async removeNetwork(name) {
|
|
218
|
+
await this.runCli(['network', 'rm', name]);
|
|
219
|
+
}
|
|
220
|
+
removeNetworkSync(name) {
|
|
221
|
+
try {
|
|
222
|
+
(0, child_process_1.execFileSync)(this.binary, ['network', 'rm', name], { stdio: 'ignore' });
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Best effort: the network may already be gone
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async copyFrom(id, containerPath, hostPath) {
|
|
229
|
+
return (await this.runCli(['cp', `${id}:${containerPath}`, hostPath]))
|
|
230
|
+
.success;
|
|
231
|
+
}
|
|
232
|
+
async copyTo(id, hostPath, containerPath) {
|
|
233
|
+
return (await this.runCli(['cp', hostPath, `${id}:${containerPath}`]))
|
|
234
|
+
.success;
|
|
235
|
+
}
|
|
236
|
+
async health(id) {
|
|
237
|
+
const result = await this.runCli([
|
|
238
|
+
'inspect',
|
|
239
|
+
'--format',
|
|
240
|
+
'{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}',
|
|
241
|
+
id
|
|
242
|
+
]);
|
|
243
|
+
return result.success ? result.stdout.trim() : 'unknown';
|
|
244
|
+
}
|
|
245
|
+
async ports(id) {
|
|
246
|
+
const result = await this.runCli(['port', id]);
|
|
247
|
+
const ports = {};
|
|
248
|
+
for (const line of result.stdout.split('\n')) {
|
|
249
|
+
const match = line.match(/^(\d+)\/\w+\s*->\s*.*:(\d+)\s*$/);
|
|
250
|
+
if (match && !(match[1] in ports))
|
|
251
|
+
ports[match[1]] = match[2];
|
|
252
|
+
}
|
|
253
|
+
return ports;
|
|
254
|
+
}
|
|
255
|
+
async stop(id) {
|
|
256
|
+
await this.runCli(['rm', '-f', id]);
|
|
257
|
+
}
|
|
258
|
+
stopSync(id) {
|
|
259
|
+
try {
|
|
260
|
+
(0, child_process_1.execFileSync)(this.binary, ['rm', '-f', id], { stdio: 'ignore' });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// Best effort: the container may already be gone
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
runCli(args, timeoutMs, env) {
|
|
267
|
+
const startTime = Date.now();
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
let child;
|
|
270
|
+
try {
|
|
271
|
+
child = (0, child_process_1.spawn)(this.binary, args, { env: env?.clientEnv });
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
env?.cleanup();
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
let stdout = '';
|
|
278
|
+
let stderr = '';
|
|
279
|
+
let timedOut = false;
|
|
280
|
+
const timeout = timeoutMs
|
|
281
|
+
? setTimeout(() => {
|
|
282
|
+
timedOut = true;
|
|
283
|
+
child.kill('SIGKILL');
|
|
284
|
+
}, timeoutMs)
|
|
285
|
+
: undefined;
|
|
286
|
+
child.stdout.on('data', (chunk) => (stdout += chunk.toString()));
|
|
287
|
+
child.stderr.on('data', (chunk) => (stderr += chunk.toString()));
|
|
288
|
+
child.on('error', (err) => {
|
|
289
|
+
if (timeout)
|
|
290
|
+
clearTimeout(timeout);
|
|
291
|
+
env?.cleanup();
|
|
292
|
+
resolve({
|
|
293
|
+
success: false,
|
|
294
|
+
stdout,
|
|
295
|
+
stderr: stderr || err.message,
|
|
296
|
+
exitCode: null,
|
|
297
|
+
timedOut,
|
|
298
|
+
durationMs: Date.now() - startTime
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
child.on('close', (exitCode) => {
|
|
302
|
+
if (timeout)
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
env?.cleanup();
|
|
305
|
+
resolve({
|
|
306
|
+
success: !timedOut && exitCode === 0,
|
|
307
|
+
stdout,
|
|
308
|
+
stderr,
|
|
309
|
+
exitCode,
|
|
310
|
+
timedOut,
|
|
311
|
+
durationMs: Date.now() - startTime
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async build(contextDir, tag, dockerfile) {
|
|
317
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
318
|
+
const dockerfileArg = dockerfile && dockerfile !== 'Dockerfile'
|
|
319
|
+
? `-f ${quote(dockerfile)} `
|
|
320
|
+
: '';
|
|
321
|
+
try {
|
|
322
|
+
const { stdout, stderr } = await execAsync(`${this.binary} build -t ${quote(tag)} ${dockerfileArg}${quote(contextDir)}`);
|
|
323
|
+
return { success: true, stdout, stderr };
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
const err = error;
|
|
327
|
+
return {
|
|
328
|
+
success: false,
|
|
329
|
+
stdout: err.stdout || '',
|
|
330
|
+
stderr: err.stderr || '',
|
|
331
|
+
error: err.message
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const runtimeInstances = {
|
|
337
|
+
docker: new CliContainerRuntime('docker'),
|
|
338
|
+
podman: new CliContainerRuntime('podman'),
|
|
339
|
+
containerd: new CliContainerRuntime('containerd')
|
|
340
|
+
};
|
|
341
|
+
const AUTO_DETECT_ORDER = [
|
|
342
|
+
'docker',
|
|
343
|
+
'podman',
|
|
344
|
+
'containerd'
|
|
345
|
+
];
|
|
346
|
+
/**
|
|
347
|
+
* Resolve a usable container runtime according to preference:
|
|
348
|
+
* - 'host' → no container: the caller runs steps on the host (opt-in only).
|
|
349
|
+
* - 'docker' | 'podman' | 'containerd' → that engine specifically, or null
|
|
350
|
+
* if unavailable.
|
|
351
|
+
* - 'auto' → first available of docker, podman, then containerd; null if
|
|
352
|
+
* none is installed/running (the caller then refuses to run).
|
|
353
|
+
*/
|
|
354
|
+
async function resolveContainerRuntime(preference = 'auto') {
|
|
355
|
+
if (preference === 'host')
|
|
356
|
+
return null;
|
|
357
|
+
if (preference !== 'auto') {
|
|
358
|
+
const runtime = runtimeInstances[preference];
|
|
359
|
+
return (await runtime.isAvailable()) ? runtime : null;
|
|
360
|
+
}
|
|
361
|
+
for (const name of AUTO_DETECT_ORDER) {
|
|
362
|
+
const runtime = runtimeInstances[name];
|
|
363
|
+
if (await runtime.isAvailable())
|
|
364
|
+
return runtime;
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
/** Test-only: reset cached availability probes between test cases. */
|
|
369
|
+
function _resetRuntimeCacheForTests() {
|
|
370
|
+
for (const runtime of Object.values(runtimeInstances)) {
|
|
371
|
+
runtime['availableCache'] = undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
//# sourceMappingURL=container-runtime.js.map
|
|
@@ -0,0 +1,179 @@
|
|
|
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.executeDockerAction = executeDockerAction;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const logger_1 = require("../utils/logger");
|
|
42
|
+
const expression_1 = require("./expression");
|
|
43
|
+
const container_runtime_1 = require("./container-runtime");
|
|
44
|
+
const runner_env_1 = require("./runner-env");
|
|
45
|
+
const job_runtime_1 = require("./job-runtime");
|
|
46
|
+
const node_action_1 = require("./node-action");
|
|
47
|
+
/**
|
|
48
|
+
* Execute a Docker-type action (`runs.using: docker`). Unlike `run:` steps,
|
|
49
|
+
* container isolation is mandatory here (the image *is* the action) - there
|
|
50
|
+
* is no safe host-execution fallback, so this fails the step (rather than
|
|
51
|
+
* skip-as-success) if no container runtime is available.
|
|
52
|
+
*/
|
|
53
|
+
async function executeDockerAction(action, actionPath, step, actionInputs, callerEnv, options, runtime, startTime) {
|
|
54
|
+
// The image is the action, so even with steps on the host (`host`) it
|
|
55
|
+
// runs in a container, using whichever engine is available.
|
|
56
|
+
const preference = options.containerRuntime ?? 'auto';
|
|
57
|
+
const containerRuntime = await (0, container_runtime_1.resolveContainerRuntime)(preference === 'host' ? 'auto' : preference);
|
|
58
|
+
if (!containerRuntime) {
|
|
59
|
+
logger_1.logger.error(`Docker action "${step.uses}" needs a container runtime (docker, podman or containerd), but none is available.`);
|
|
60
|
+
return {
|
|
61
|
+
step,
|
|
62
|
+
success: false,
|
|
63
|
+
error: 'No container runtime (docker, podman or containerd) available for the Docker action',
|
|
64
|
+
duration: Date.now() - startTime
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const actionDir = path.dirname(actionPath);
|
|
68
|
+
const rawImage = action.runs.image;
|
|
69
|
+
if (!rawImage) {
|
|
70
|
+
return {
|
|
71
|
+
step,
|
|
72
|
+
success: false,
|
|
73
|
+
error: 'No image specified for Docker action',
|
|
74
|
+
duration: Date.now() - startTime
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let image;
|
|
78
|
+
if (rawImage.startsWith('docker://')) {
|
|
79
|
+
image = rawImage.slice('docker://'.length);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const dockerfileRelative = rawImage === 'Dockerfile' ? 'Dockerfile' : rawImage;
|
|
83
|
+
const dockerfilePath = path.join(actionDir, dockerfileRelative);
|
|
84
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
85
|
+
return {
|
|
86
|
+
step,
|
|
87
|
+
success: false,
|
|
88
|
+
error: `Dockerfile not found for action: ${dockerfilePath}`,
|
|
89
|
+
duration: Date.now() - startTime
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const tag = `secureci-action-${crypto.createHash('sha1').update(actionDir).digest('hex').slice(0, 12)}`;
|
|
93
|
+
logger_1.logger.info(`Building Docker action image: ${action.name} (${tag})`);
|
|
94
|
+
const buildResult = await containerRuntime.build(actionDir, tag, dockerfileRelative);
|
|
95
|
+
if (!buildResult.success) {
|
|
96
|
+
logger_1.logger.error(`Docker action image build failed: ${buildResult.error}`);
|
|
97
|
+
if (buildResult.stderr)
|
|
98
|
+
logger_1.logger.error(buildResult.stderr.trim());
|
|
99
|
+
return {
|
|
100
|
+
step,
|
|
101
|
+
success: false,
|
|
102
|
+
error: `Docker action image build failed: ${buildResult.error}`,
|
|
103
|
+
duration: Date.now() - startTime
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
image = tag;
|
|
107
|
+
}
|
|
108
|
+
// Inputs are passed as INPUT_<NAME> env vars, matching GitHub's own
|
|
109
|
+
// convention (the same one used for JS actions).
|
|
110
|
+
const inputEnv = {};
|
|
111
|
+
for (const [key, value] of Object.entries(actionInputs)) {
|
|
112
|
+
inputEnv[(0, node_action_1.inputEnvName)(key)] = value;
|
|
113
|
+
}
|
|
114
|
+
// `runs.args` may reference ${{ inputs.* }} / ${{ secrets.* }}.
|
|
115
|
+
const actionCtx = (0, expression_1.createContext)({}, actionInputs, options.secrets);
|
|
116
|
+
const args = (action.runs.args || []).map((arg) => (0, expression_1.substituteExpressions)(arg, actionCtx));
|
|
117
|
+
logger_1.logger.info(`Executing Docker action: ${action.name}`);
|
|
118
|
+
const exchangeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'secureci-exchange-'));
|
|
119
|
+
const files = (0, runner_env_1.createStepFiles)(exchangeDir);
|
|
120
|
+
fs.copyFileSync(runtime.eventPath, path.join(exchangeDir, 'event.json'));
|
|
121
|
+
try {
|
|
122
|
+
const limit = (0, job_runtime_1.effectiveTimeout)(runtime, options.containerTimeoutMs, true);
|
|
123
|
+
const result = await containerRuntime.execute({
|
|
124
|
+
image,
|
|
125
|
+
args,
|
|
126
|
+
entrypoint: action.runs.entrypoint,
|
|
127
|
+
workingDir: process.cwd(),
|
|
128
|
+
exchangeDir,
|
|
129
|
+
env: {
|
|
130
|
+
...(0, job_runtime_1.containerRunnerEnv)(runtime),
|
|
131
|
+
...runtime.actionEnv,
|
|
132
|
+
...callerEnv,
|
|
133
|
+
...inputEnv,
|
|
134
|
+
...(0, job_runtime_1.fileCommandEnv)(files, container_runtime_1.CONTAINER_EXCHANGE_DIR)
|
|
135
|
+
},
|
|
136
|
+
timeoutMs: limit.ms,
|
|
137
|
+
memoryLimit: options.containerMemoryLimit,
|
|
138
|
+
networkDisabled: options.networkDisabled === true
|
|
139
|
+
});
|
|
140
|
+
const { outputs } = (0, job_runtime_1.finishStepOutput)(result.stdout, files, runtime);
|
|
141
|
+
if (result.stderr) {
|
|
142
|
+
logger_1.logger[result.success ? 'warn' : 'error'](result.stderr.trim());
|
|
143
|
+
}
|
|
144
|
+
if (result.timedOut) {
|
|
145
|
+
logger_1.logger.error(`${limit.message} (${containerRuntime.name})`);
|
|
146
|
+
return {
|
|
147
|
+
step,
|
|
148
|
+
success: false,
|
|
149
|
+
error: limit.message,
|
|
150
|
+
output: result.stdout,
|
|
151
|
+
outputs,
|
|
152
|
+
duration: Date.now() - startTime
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
logger_1.logger.error(`Docker action failed (${containerRuntime.name}, exit code: ${result.exitCode})`);
|
|
157
|
+
return {
|
|
158
|
+
step,
|
|
159
|
+
success: false,
|
|
160
|
+
error: `Command exited with code ${result.exitCode}`,
|
|
161
|
+
output: result.stdout,
|
|
162
|
+
outputs,
|
|
163
|
+
duration: Date.now() - startTime
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
logger_1.logger.success(`Docker action completed: ${action.name}`);
|
|
167
|
+
return {
|
|
168
|
+
step,
|
|
169
|
+
success: true,
|
|
170
|
+
output: result.stdout,
|
|
171
|
+
outputs,
|
|
172
|
+
duration: Date.now() - startTime
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
fs.rmSync(exchangeDir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=docker-action.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dryRun = dryRun;
|
|
4
|
+
const workflow_parser_1 = require("../parser/workflow-parser");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
const validator_1 = require("../parser/validator");
|
|
7
|
+
/**
|
|
8
|
+
* Perform a dry run of a workflow or action file
|
|
9
|
+
* Shows what would be executed without actually running anything
|
|
10
|
+
*/
|
|
11
|
+
function dryRun(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = (0, workflow_parser_1.parseFile)(filePath);
|
|
14
|
+
// A file a real run would reject fails the dry run too.
|
|
15
|
+
const invalid = parsed.type === 'workflow'
|
|
16
|
+
? (0, validator_1.workflowExpressionErrors)(parsed.file)
|
|
17
|
+
: (0, validator_1.actionExpressionErrors)(parsed.file);
|
|
18
|
+
if (invalid.length > 0) {
|
|
19
|
+
logger_1.logger.error(`Invalid ${parsed.type} file: ${filePath}`);
|
|
20
|
+
for (const error of invalid) {
|
|
21
|
+
logger_1.logger.error(` ${error.path}: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
return { success: false, type: parsed.type };
|
|
24
|
+
}
|
|
25
|
+
if (parsed.type === 'workflow') {
|
|
26
|
+
return dryRunWorkflow(parsed);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return dryRunAction(parsed);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger_1.logger.error(`Failed to perform dry run: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
36
|
+
type: 'workflow'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function dryRunWorkflow(parsed) {
|
|
41
|
+
const workflow = parsed.file;
|
|
42
|
+
logger_1.logger.info(`Workflow: ${workflow.name}`);
|
|
43
|
+
logger_1.logger.info(`Trigger: ${JSON.stringify(workflow.on)}`);
|
|
44
|
+
const jobs = Object.entries(workflow.jobs).map(([jobId, job]) => {
|
|
45
|
+
logger_1.logger.step(`Job: ${jobId} (${job.name || jobId})`);
|
|
46
|
+
// Handle reusable workflow calls
|
|
47
|
+
if (job.uses) {
|
|
48
|
+
logger_1.logger.info(` Uses workflow: ${job.uses}`);
|
|
49
|
+
if (job.with) {
|
|
50
|
+
logger_1.logger.info(` With inputs: ${JSON.stringify(job.with)}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: jobId,
|
|
54
|
+
steps: [`Calls reusable workflow: ${job.uses}`]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Handle regular jobs
|
|
58
|
+
if (job['runs-on']) {
|
|
59
|
+
logger_1.logger.info(` Runs on: ${job['runs-on']}`);
|
|
60
|
+
}
|
|
61
|
+
const steps = (job.steps || []).map((step, idx) => {
|
|
62
|
+
const stepInfo = [];
|
|
63
|
+
stepInfo.push(` Step ${idx + 1}:`);
|
|
64
|
+
if (step.name) {
|
|
65
|
+
stepInfo.push(step.name);
|
|
66
|
+
}
|
|
67
|
+
if (step.uses) {
|
|
68
|
+
stepInfo.push(`uses: ${step.uses}`);
|
|
69
|
+
}
|
|
70
|
+
else if (step.run) {
|
|
71
|
+
stepInfo.push(`run: ${step.run}`);
|
|
72
|
+
}
|
|
73
|
+
const stepDesc = stepInfo.join(' ');
|
|
74
|
+
logger_1.logger.info(stepDesc);
|
|
75
|
+
return stepDesc;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
name: jobId,
|
|
79
|
+
steps
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
logger_1.logger.success('Dry run completed successfully');
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
type: 'workflow',
|
|
86
|
+
jobs
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function dryRunAction(parsed) {
|
|
90
|
+
const action = parsed.file;
|
|
91
|
+
logger_1.logger.info(`Action: ${action.name}`);
|
|
92
|
+
logger_1.logger.info(`Description: ${action.description}`);
|
|
93
|
+
const inputs = [];
|
|
94
|
+
if (action.inputs) {
|
|
95
|
+
logger_1.logger.step('Inputs:');
|
|
96
|
+
Object.entries(action.inputs).forEach(([name, input]) => {
|
|
97
|
+
const required = input.required ? '(required)' : '(optional)';
|
|
98
|
+
const defaultVal = input.default ? ` [default: ${input.default}]` : '';
|
|
99
|
+
const inputInfo = ` ${name} ${required}${defaultVal}`;
|
|
100
|
+
logger_1.logger.info(inputInfo);
|
|
101
|
+
inputs.push(inputInfo);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const outputs = [];
|
|
105
|
+
if (action.outputs) {
|
|
106
|
+
logger_1.logger.step('Outputs:');
|
|
107
|
+
Object.keys(action.outputs).forEach((name) => {
|
|
108
|
+
const outputInfo = ` ${name}`;
|
|
109
|
+
logger_1.logger.info(outputInfo);
|
|
110
|
+
outputs.push(outputInfo);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const steps = [];
|
|
114
|
+
if (action.runs.steps) {
|
|
115
|
+
logger_1.logger.step('Steps:');
|
|
116
|
+
action.runs.steps.forEach((step, idx) => {
|
|
117
|
+
const stepInfo = [];
|
|
118
|
+
stepInfo.push(` Step ${idx + 1}:`);
|
|
119
|
+
if (step.name) {
|
|
120
|
+
stepInfo.push(step.name);
|
|
121
|
+
}
|
|
122
|
+
if (step.uses) {
|
|
123
|
+
stepInfo.push(`uses: ${step.uses}`);
|
|
124
|
+
}
|
|
125
|
+
else if (step.run) {
|
|
126
|
+
stepInfo.push(`run: ${step.run}`);
|
|
127
|
+
}
|
|
128
|
+
const stepDesc = stepInfo.join(' ');
|
|
129
|
+
logger_1.logger.info(stepDesc);
|
|
130
|
+
steps.push(stepDesc);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
logger_1.logger.success('Dry run completed successfully');
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
type: 'action',
|
|
137
|
+
inputs,
|
|
138
|
+
outputs,
|
|
139
|
+
steps
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=dry-run.js.map
|