@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.
- package/LICENSE +21 -0
- package/README.md +230 -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/ansi.js +47 -0
- package/dist/utils/key-value.js +33 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/repository.js +51 -0
- package/package.json +76 -4
|
@@ -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
|