@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,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