@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,235 @@
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.tryGit = tryGit;
37
+ exports.parseRepositoryFromRemote = parseRepositoryFromRemote;
38
+ exports.payloadString = payloadString;
39
+ exports.buildGithubContext = buildGithubContext;
40
+ exports.declaredInputDefaults = declaredInputDefaults;
41
+ exports.typedInputs = typedInputs;
42
+ exports.workflowCallOutputs = workflowCallOutputs;
43
+ exports.workflowTriggers = workflowTriggers;
44
+ exports.loadEvent = loadEvent;
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const child_process_1 = require("child_process");
49
+ /**
50
+ * Best-effort local git lookup, used to populate `github.sha` / `github.ref`.
51
+ * Returns undefined if git isn't available or the directory isn't a repo.
52
+ */
53
+ function tryGit(command, cwd) {
54
+ try {
55
+ return (0, child_process_1.execSync)(command, { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
56
+ .toString()
57
+ .trim();
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
63
+ /**
64
+ * `owner/repo` from a git remote URL (`git@host:o/r.git`,
65
+ * `https://host/o/r`, `ssh://git@host/o/r.git`); undefined for local paths.
66
+ */
67
+ function parseRepositoryFromRemote(url) {
68
+ const match = url
69
+ .trim()
70
+ .match(/^(?:[\w.-]+@[\w.-]+:|(?:https?|ssh|git):\/\/(?:[^@/]+@)?[^/]+\/)([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
71
+ return match ? `${match[1]}/${match[2]}` : undefined;
72
+ }
73
+ function payloadString(payload, ...keys) {
74
+ let value = payload;
75
+ for (const key of keys) {
76
+ if (!value || typeof value !== 'object')
77
+ return undefined;
78
+ value = value[key];
79
+ }
80
+ return typeof value === 'string' && value ? value : undefined;
81
+ }
82
+ /**
83
+ * Build a (best-effort, local-only) `github.*` context object. Real GitHub
84
+ * Actions populates this from the live Actions run; locally we fall back to
85
+ * `GITHUB_*` environment variables (if the user has set them), the event
86
+ * payload, and local git state (HEAD, branch, `origin` remote), matching
87
+ * act's approach of approximating the context for local runs.
88
+ */
89
+ function buildGithubContext(workflowName, jobId, cwd, event = { name: 'workflow_dispatch', payload: {} }, token = '', runId = '') {
90
+ const env = process.env;
91
+ const sha = env.GITHUB_SHA || tryGit('git rev-parse HEAD', cwd) || '';
92
+ const ref = env.GITHUB_REF ||
93
+ payloadString(event.payload, 'ref') ||
94
+ (() => {
95
+ const branch = tryGit('git rev-parse --abbrev-ref HEAD', cwd);
96
+ return branch ? `refs/heads/${branch}` : '';
97
+ })();
98
+ const refName = env.GITHUB_REF_NAME || ref.replace(/^refs\/(heads|tags)\//, '');
99
+ const remote = tryGit('git config --get remote.origin.url', cwd);
100
+ const repository = env.GITHUB_REPOSITORY ||
101
+ payloadString(event.payload, 'repository', 'full_name') ||
102
+ (remote && parseRepositoryFromRemote(remote)) ||
103
+ path.basename(cwd);
104
+ return {
105
+ workflow: workflowName,
106
+ job: jobId,
107
+ sha,
108
+ ref,
109
+ ref_name: refName,
110
+ event_name: event.name,
111
+ event: event.payload,
112
+ actor: env.GITHUB_ACTOR || os.userInfo().username,
113
+ repository,
114
+ repository_owner: env.GITHUB_REPOSITORY_OWNER ||
115
+ payloadString(event.payload, 'repository', 'owner', 'login') ||
116
+ (repository.includes('/') ? repository.split('/')[0] : ''),
117
+ run_id: runId || env.GITHUB_RUN_ID || String(Date.now()),
118
+ run_number: env.GITHUB_RUN_NUMBER || '1',
119
+ run_attempt: env.GITHUB_RUN_ATTEMPT || '1',
120
+ workspace: env.GITHUB_WORKSPACE || cwd,
121
+ server_url: env.GITHUB_SERVER_URL || 'https://github.com',
122
+ api_url: env.GITHUB_API_URL || 'https://api.github.com',
123
+ // Only what the user passed as secrets.GITHUB_TOKEN: a local token is
124
+ // never picked up implicitly, since every action in the run could read it.
125
+ token
126
+ };
127
+ }
128
+ /**
129
+ * Defaults declared for `on.workflow_dispatch.inputs` / `on.workflow_call.
130
+ * inputs`, which populate the `inputs` context when not passed explicitly.
131
+ */
132
+ function declaredInputDefaults(on) {
133
+ const defaults = {};
134
+ if (!on || typeof on !== 'object' || Array.isArray(on))
135
+ return defaults;
136
+ for (const trigger of ['workflow_dispatch', 'workflow_call']) {
137
+ const inputs = on[trigger]
138
+ ?.inputs;
139
+ if (!inputs || typeof inputs !== 'object')
140
+ continue;
141
+ for (const [name, def] of Object.entries(inputs)) {
142
+ if (def && def.default !== undefined && !(name in defaults)) {
143
+ defaults[name] = String(def.default);
144
+ }
145
+ }
146
+ }
147
+ return defaults;
148
+ }
149
+ /**
150
+ * The `inputs` context: values as given (strings), with `boolean` and
151
+ * `number` inputs declared in `on.workflow_dispatch` / `on.workflow_call`
152
+ * converted to their types, as on GitHub (so `if: inputs.flag` is false
153
+ * for `flag: false`, not the truthy string "false").
154
+ */
155
+ function typedInputs(on, inputs) {
156
+ const types = {};
157
+ if (on && typeof on === 'object' && !Array.isArray(on)) {
158
+ for (const trigger of ['workflow_dispatch', 'workflow_call']) {
159
+ const declared = on[trigger]
160
+ ?.inputs;
161
+ if (!declared || typeof declared !== 'object')
162
+ continue;
163
+ for (const [name, def] of Object.entries(declared)) {
164
+ if (def && typeof def.type === 'string' && !(name in types)) {
165
+ types[name] = def.type;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ const typed = { ...inputs };
171
+ for (const [name, value] of Object.entries(inputs)) {
172
+ if (types[name] === 'boolean' && /^(true|false)$/i.test(value.trim())) {
173
+ typed[name] = value.trim().toLowerCase() === 'true';
174
+ }
175
+ else if (types[name] === 'number' &&
176
+ value.trim() !== '' &&
177
+ Number.isFinite(Number(value))) {
178
+ typed[name] = Number(value);
179
+ }
180
+ }
181
+ return typed;
182
+ }
183
+ /** `on.workflow_call.outputs` as name -> value expression, if declared. */
184
+ function workflowCallOutputs(on) {
185
+ const outputs = on && typeof on === 'object' && !Array.isArray(on)
186
+ ? on.workflow_call?.outputs
187
+ : undefined;
188
+ if (!outputs || typeof outputs !== 'object')
189
+ return undefined;
190
+ const values = {};
191
+ for (const [name, def] of Object.entries(outputs)) {
192
+ if (def?.value !== undefined)
193
+ values[name] = String(def.value);
194
+ }
195
+ return values;
196
+ }
197
+ /** The workflow's own triggers, in declaration order. */
198
+ function workflowTriggers(on) {
199
+ if (typeof on === 'string')
200
+ return [on];
201
+ if (Array.isArray(on))
202
+ return on.map(String);
203
+ if (on && typeof on === 'object')
204
+ return Object.keys(on);
205
+ return [];
206
+ }
207
+ /**
208
+ * The event for this run: `--event-name`, else $GITHUB_EVENT_NAME, else
209
+ * workflow_dispatch if the workflow declares it, else its first trigger.
210
+ * The payload comes from `--event-path`; without one, workflow_dispatch
211
+ * gets `{ inputs }` like on GitHub, other events `{}`.
212
+ */
213
+ function loadEvent(options, inputs, on) {
214
+ const triggers = workflowTriggers(on);
215
+ const name = options.eventName ||
216
+ process.env.GITHUB_EVENT_NAME ||
217
+ (triggers.includes('workflow_dispatch') || triggers.length === 0
218
+ ? 'workflow_dispatch'
219
+ : triggers[0]);
220
+ if (!options.eventPath) {
221
+ return { name, payload: name === 'workflow_dispatch' ? { inputs } : {} };
222
+ }
223
+ let payload;
224
+ try {
225
+ payload = JSON.parse(fs.readFileSync(options.eventPath, 'utf-8'));
226
+ }
227
+ catch (error) {
228
+ throw new Error(`Invalid event payload ${options.eventPath}: ${error instanceof Error ? error.message : String(error)}`);
229
+ }
230
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
231
+ throw new Error(`Invalid event payload ${options.eventPath}: expected a JSON object`);
232
+ }
233
+ return { name, payload: payload };
234
+ }
235
+ //# sourceMappingURL=github-context.js.map
@@ -0,0 +1,123 @@
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.globToRegExp = globToRegExp;
37
+ exports.hashFiles = hashFiles;
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ /**
42
+ * Convert a glob (`**`, `*`, `?`) to a regex over `/`-separated paths
43
+ * relative to the workspace.
44
+ */
45
+ function globToRegExp(pattern) {
46
+ let source = '';
47
+ for (let i = 0; i < pattern.length; i++) {
48
+ const ch = pattern[i];
49
+ if (ch === '*' && pattern[i + 1] === '*') {
50
+ if (pattern[i + 2] === '/') {
51
+ source += '(?:.*/)?';
52
+ i += 2;
53
+ }
54
+ else {
55
+ source += '.*';
56
+ i += 1;
57
+ }
58
+ }
59
+ else if (ch === '*') {
60
+ source += '[^/]*';
61
+ }
62
+ else if (ch === '?') {
63
+ source += '[^/]';
64
+ }
65
+ else {
66
+ source += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
67
+ }
68
+ }
69
+ return new RegExp(`^${source}$`);
70
+ }
71
+ function walk(dir, root, files) {
72
+ let entries;
73
+ try {
74
+ entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ for (const entry of entries) {
80
+ const full = path.join(dir, entry.name);
81
+ if (entry.isDirectory()) {
82
+ walk(full, root, files);
83
+ }
84
+ else if (entry.isFile()) {
85
+ files.push(path.relative(root, full).split(path.sep).join('/'));
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * GitHub's `hashFiles(...patterns)`: SHA-256 over the concatenated SHA-256
91
+ * digests of every matching file (sorted by path), or '' when nothing
92
+ * matches. Patterns are relative to the workspace; `!pattern` excludes.
93
+ */
94
+ function hashFiles(workspace, patterns) {
95
+ const includes = [];
96
+ const excludes = [];
97
+ for (const raw of patterns) {
98
+ const pattern = raw.trim().replace(/^\.\//, '');
99
+ if (!pattern)
100
+ continue;
101
+ if (pattern.startsWith('!')) {
102
+ excludes.push(globToRegExp(pattern.slice(1).replace(/^\.\//, '')));
103
+ }
104
+ else {
105
+ includes.push(globToRegExp(pattern));
106
+ }
107
+ }
108
+ const all = [];
109
+ walk(workspace, workspace, all);
110
+ const matched = all
111
+ .filter((file) => includes.some((re) => re.test(file)) &&
112
+ !excludes.some((re) => re.test(file)))
113
+ .map((file) => path.join(workspace, file))
114
+ .sort();
115
+ if (matched.length === 0)
116
+ return '';
117
+ const combined = crypto.createHash('sha256');
118
+ for (const file of matched) {
119
+ combined.update(crypto.createHash('sha256').update(fs.readFileSync(file)).digest());
120
+ }
121
+ return combined.digest('hex');
122
+ }
123
+ //# sourceMappingURL=hash-files.js.map
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_RUNNER_IMAGES = exports.DEFAULT_CONTAINER_IMAGE = void 0;
37
+ exports.installContainerCleanup = installContainerCleanup;
38
+ exports.noRuntimeMessage = noRuntimeMessage;
39
+ exports.startJobContainer = startJobContainer;
40
+ exports.runnerImage = runnerImage;
41
+ exports.stopJobContainer = stopJobContainer;
42
+ exports.startJobServices = startJobServices;
43
+ exports.stopJobServices = stopJobServices;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const crypto = __importStar(require("crypto"));
47
+ const logger_1 = require("../utils/logger");
48
+ const expression_1 = require("./expression");
49
+ const container_runtime_1 = require("./container-runtime");
50
+ const shell_1 = require("./shell");
51
+ /**
52
+ * Image for jobs whose `runs-on` has no image mapping. Runner-like (Node.js,
53
+ * git, curl and common tools), as used by nektos/act.
54
+ */
55
+ exports.DEFAULT_CONTAINER_IMAGE = 'catthehacker/ubuntu:act-22.04';
56
+ /** Built-in `runs-on` label -> image mapping, extended by `runner-images`. */
57
+ exports.DEFAULT_RUNNER_IMAGES = {
58
+ 'ubuntu-latest': 'catthehacker/ubuntu:act-latest',
59
+ 'ubuntu-24.04': 'catthehacker/ubuntu:act-24.04',
60
+ 'ubuntu-22.04': 'catthehacker/ubuntu:act-22.04',
61
+ 'ubuntu-20.04': 'catthehacker/ubuntu:act-20.04'
62
+ };
63
+ const activeContainers = new Set();
64
+ const activeNetworks = new Set();
65
+ let containerCleanupInstalled = false;
66
+ /**
67
+ * Make sure job containers are removed if the CLI exits early (Ctrl-C,
68
+ * SIGTERM, or an exit that skips the normal per-job cleanup).
69
+ */
70
+ function installContainerCleanup() {
71
+ if (containerCleanupInstalled)
72
+ return;
73
+ containerCleanupInstalled = true;
74
+ const cleanup = () => {
75
+ for (const container of activeContainers) {
76
+ container.runtime.stopSync(container.id);
77
+ }
78
+ activeContainers.clear();
79
+ for (const network of activeNetworks) {
80
+ network.runtime.removeNetworkSync(network.name);
81
+ }
82
+ activeNetworks.clear();
83
+ };
84
+ process.once('exit', cleanup);
85
+ for (const [signal, code] of [
86
+ ['SIGINT', 130],
87
+ ['SIGTERM', 143]
88
+ ]) {
89
+ process.once(signal, () => {
90
+ cleanup();
91
+ process.exit(code);
92
+ });
93
+ }
94
+ }
95
+ /** Why a run can't start without its container runtime, and what to do. */
96
+ function noRuntimeMessage(preference) {
97
+ const which = preference === 'auto'
98
+ ? 'docker, podman or containerd (nerdctl)'
99
+ : `"${preference}"`;
100
+ return `No container runtime available: ${which} is not installed or not running. Start it, pick another with --container-runtime, or run steps directly on this machine, with your user's privileges, with --container-runtime host.`;
101
+ }
102
+ /**
103
+ * Start the job's container when container isolation is on, using
104
+ * `jobs.<id>.container` if set, else the image for its `runs-on`. Returns
105
+ * false (the job then fails) when the runtime is unavailable or the
106
+ * container can't be started; steps never fall back to the host.
107
+ */
108
+ async function startJobContainer(job, ctx, options, runtime, runDir) {
109
+ const preference = options.containerRuntime ?? 'host';
110
+ const spec = typeof job?.container === 'string'
111
+ ? { image: job.container }
112
+ : job?.container;
113
+ if (preference === 'host') {
114
+ if (spec) {
115
+ logger_1.logger.warn(`Ignoring jobs.<id>.container (${spec.image}): container runtime is "host", running on the host.`);
116
+ }
117
+ return true;
118
+ }
119
+ const containerRuntime = await (0, container_runtime_1.resolveContainerRuntime)(preference);
120
+ if (!containerRuntime) {
121
+ logger_1.logger.error(noRuntimeMessage(preference));
122
+ return false;
123
+ }
124
+ if (spec &&
125
+ 'image' in spec &&
126
+ (('options' in spec && spec.options) ||
127
+ ('volumes' in spec && spec.volumes) ||
128
+ ('ports' in spec && spec.ports) ||
129
+ ('credentials' in spec && spec.credentials))) {
130
+ logger_1.logger.warn('jobs.<id>.container: only image and env are supported; ignoring options/volumes/ports/credentials.');
131
+ }
132
+ // `container: ${{ inputs.image || null }}` that comes out empty means no
133
+ // container image, as on GitHub: use the image for runs-on.
134
+ const specImage = spec?.image
135
+ ? (0, expression_1.substituteExpressions)(spec.image, ctx).trim()
136
+ : '';
137
+ const image = specImage ||
138
+ options.containerImage ||
139
+ runnerImage(job?.['runs-on'], ctx, options);
140
+ const containerEnv = (0, expression_1.substituteRecord)(specImage && spec && 'env' in spec ? spec.env : undefined, ctx);
141
+ const exchangeDir = fs.mkdtempSync(path.join(runDir, 'container-'));
142
+ fs.chmodSync(exchangeDir, 0o777);
143
+ fs.copyFileSync(runtime.eventPath, path.join(exchangeDir, 'event.json'));
144
+ logger_1.logger.info(`Starting job container: ${image} (${containerRuntime.name})`);
145
+ const started = await containerRuntime.start({
146
+ image,
147
+ workingDir: process.cwd(),
148
+ exchangeDir,
149
+ env: containerEnv,
150
+ memoryLimit: options.containerMemoryLimit,
151
+ networkDisabled: options.networkDisabled === true,
152
+ network: runtime.services?.network
153
+ });
154
+ if (!started.success || !started.id) {
155
+ logger_1.logger.error(`Failed to start job container (${image}): ${started.error}`);
156
+ return false;
157
+ }
158
+ const bashProbe = await containerRuntime.exec(started.id, {
159
+ command: 'command -v bash'
160
+ });
161
+ runtime.container = {
162
+ runtime: containerRuntime,
163
+ id: started.id,
164
+ image,
165
+ exchangeDir,
166
+ bash: bashProbe.success,
167
+ actionDirs: new Map()
168
+ };
169
+ installContainerCleanup();
170
+ activeContainers.add(runtime.container);
171
+ return true;
172
+ }
173
+ /**
174
+ * The image for a job's `runs-on`: the first label with a mapping in
175
+ * `runner-images` (or the built-in one), else the default image, with a
176
+ * warning for labels that have no mapping.
177
+ */
178
+ function runnerImage(runsOn, ctx, options) {
179
+ const fallback = options.defaultContainerImage || exports.DEFAULT_CONTAINER_IMAGE;
180
+ const raw = runsOn;
181
+ const labels = (Array.isArray(raw)
182
+ ? raw
183
+ : raw && typeof raw === 'object'
184
+ ? [raw.labels ?? []].flat()
185
+ : [raw])
186
+ .filter((label) => typeof label === 'string')
187
+ .map((label) => (0, expression_1.substituteExpressions)(label, ctx));
188
+ if (labels.length === 0)
189
+ return fallback;
190
+ const mapping = { ...exports.DEFAULT_RUNNER_IMAGES, ...options.runnerImages };
191
+ for (const label of labels) {
192
+ if (mapping[label])
193
+ return mapping[label];
194
+ }
195
+ const shown = labels.join(', ');
196
+ if (labels.some((label) => /windows|macos/i.test(label))) {
197
+ logger_1.logger.warn(`runs-on "${shown}": only Linux containers are supported; running in ${fallback}.`);
198
+ }
199
+ else {
200
+ logger_1.logger.warn(`runs-on "${shown}" has no image mapping; using ${fallback} (map it with runner-images in ~/.secureci/config.yml).`);
201
+ }
202
+ return fallback;
203
+ }
204
+ async function stopJobContainer(runtime) {
205
+ const container = runtime.container;
206
+ if (container) {
207
+ activeContainers.delete(container);
208
+ runtime.container = undefined;
209
+ await container.runtime.stop(container.id);
210
+ }
211
+ await stopJobServices(runtime);
212
+ }
213
+ /** How long services may take to report healthy. */
214
+ const SERVICE_HEALTH_TIMEOUT_MS = 5 * 60 * 1000;
215
+ const SERVICE_HEALTH_POLL_MS = 1000;
216
+ /**
217
+ * Start `jobs.<id>.services`, before the job container. In container
218
+ * mode, services join a per-job network under their service id (so steps
219
+ * reach `postgres:5432`), and the job container joins it too; on the host,
220
+ * they're reached through their published `ports` on localhost. Waits
221
+ * for health checks, and exposes `job.services.<id>.{id,network,ports}`.
222
+ * Returns false (after cleaning up) if a service can't be started.
223
+ */
224
+ async function startJobServices(job, ctx, options, runtime) {
225
+ const specs = Object.entries(job.services ?? {});
226
+ if (specs.length === 0)
227
+ return true;
228
+ const preference = options.containerRuntime ?? 'host';
229
+ const jobRuntime = preference === 'host' ? null : await (0, container_runtime_1.resolveContainerRuntime)(preference);
230
+ const containerRuntime = jobRuntime ?? (await (0, container_runtime_1.resolveContainerRuntime)('auto'));
231
+ if (!containerRuntime) {
232
+ logger_1.logger.error('jobs.<id>.services need a container runtime (docker, podman or containerd), but none is available.');
233
+ return false;
234
+ }
235
+ if (jobRuntime && options.networkDisabled) {
236
+ logger_1.logger.error('jobs.<id>.services need networking, but container networking is disabled (--no-network).');
237
+ return false;
238
+ }
239
+ const services = {
240
+ runtime: containerRuntime,
241
+ ids: []
242
+ };
243
+ runtime.services = services;
244
+ installContainerCleanup();
245
+ if (jobRuntime) {
246
+ const network = `secureci-${crypto.randomBytes(6).toString('hex')}`;
247
+ if (!(await containerRuntime.createNetwork(network))) {
248
+ logger_1.logger.error(`Failed to create the job network ${network}`);
249
+ return false;
250
+ }
251
+ services.network = network;
252
+ activeNetworks.add({ runtime: containerRuntime, name: network });
253
+ }
254
+ const contexts = {};
255
+ for (const [serviceId, spec] of specs) {
256
+ const service = typeof spec === 'string' ? { image: spec } : spec;
257
+ if (service.credentials) {
258
+ logger_1.logger.warn(`services.${serviceId}: credentials are ignored; log in with \`docker login\` instead.`);
259
+ }
260
+ const image = (0, expression_1.substituteExpressions)(service.image, ctx);
261
+ logger_1.logger.info(`Starting service ${serviceId}: ${image}`);
262
+ const started = await containerRuntime.start({
263
+ image,
264
+ service: true,
265
+ ...(services.network
266
+ ? { network: services.network, networkAlias: serviceId }
267
+ : {}),
268
+ ports: (service.ports ?? []).map((port) => (0, expression_1.substituteExpressions)(String(port), ctx)),
269
+ volumes: (service.volumes ?? []).map((v) => (0, expression_1.substituteExpressions)(v, ctx)),
270
+ extraArgs: service.options
271
+ ? (0, shell_1.splitArgs)((0, expression_1.substituteExpressions)(service.options, ctx))
272
+ : [],
273
+ env: (0, expression_1.substituteRecord)(service.env, ctx)
274
+ });
275
+ if (!started.success || !started.id) {
276
+ logger_1.logger.error(`Failed to start service ${serviceId} (${image}): ${started.error}`);
277
+ return false;
278
+ }
279
+ services.ids.push(started.id);
280
+ activeContainers.add({ runtime: containerRuntime, id: started.id });
281
+ const deadline = Date.now() + SERVICE_HEALTH_TIMEOUT_MS;
282
+ for (;;) {
283
+ const health = await containerRuntime.health(started.id);
284
+ if (health === 'healthy' || health === 'none')
285
+ break;
286
+ if (health === 'unhealthy' || Date.now() > deadline) {
287
+ logger_1.logger.error(`Service ${serviceId} is not healthy (${health}).`);
288
+ return false;
289
+ }
290
+ await new Promise((resolve) => setTimeout(resolve, SERVICE_HEALTH_POLL_MS));
291
+ }
292
+ contexts[serviceId] = {
293
+ id: started.id,
294
+ network: services.network ?? '',
295
+ ports: await containerRuntime.ports(started.id)
296
+ };
297
+ }
298
+ ctx.job = { ...ctx.job, services: contexts };
299
+ return true;
300
+ }
301
+ /** Remove the job's service containers and network. */
302
+ async function stopJobServices(runtime) {
303
+ const services = runtime.services;
304
+ if (!services)
305
+ return;
306
+ runtime.services = undefined;
307
+ for (const id of services.ids) {
308
+ for (const entry of activeContainers) {
309
+ if (entry.id === id)
310
+ activeContainers.delete(entry);
311
+ }
312
+ await services.runtime.stop(id);
313
+ }
314
+ if (services.network) {
315
+ for (const entry of activeNetworks) {
316
+ if (entry.name === services.network)
317
+ activeNetworks.delete(entry);
318
+ }
319
+ await services.runtime.removeNetwork(services.network);
320
+ }
321
+ }
322
+ //# sourceMappingURL=job-container.js.map