@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,484 @@
|
|
|
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.builtinActionFor = builtinActionFor;
|
|
37
|
+
exports.hostFileSystem = hostFileSystem;
|
|
38
|
+
exports.containerFileSystem = containerFileSystem;
|
|
39
|
+
exports.collectFiles = collectFiles;
|
|
40
|
+
exports.uploadArtifact = uploadArtifact;
|
|
41
|
+
exports.downloadArtifact = downloadArtifact;
|
|
42
|
+
exports.runCacheAction = runCacheAction;
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const hash_files_1 = require("./hash-files");
|
|
48
|
+
/** Which built-in, if any, handles a `uses:` reference. */
|
|
49
|
+
function builtinActionFor(uses) {
|
|
50
|
+
const match = uses.match(/^actions\/(upload-artifact|download-artifact|cache(?:\/restore|\/save)?)@/i);
|
|
51
|
+
if (!match)
|
|
52
|
+
return undefined;
|
|
53
|
+
return match[1].toLowerCase().replace('/', '-');
|
|
54
|
+
}
|
|
55
|
+
/** A step file system for steps running directly on the host. */
|
|
56
|
+
function hostFileSystem(workspace) {
|
|
57
|
+
return {
|
|
58
|
+
workspace,
|
|
59
|
+
home: os.homedir(),
|
|
60
|
+
path: path,
|
|
61
|
+
readable: async (logical) => (fs.existsSync(logical) ? logical : null),
|
|
62
|
+
writeInto: async (logical, fill) => {
|
|
63
|
+
fs.mkdirSync(logical, { recursive: true });
|
|
64
|
+
fill(logical);
|
|
65
|
+
},
|
|
66
|
+
cleanup: () => undefined
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** A step file system for steps running in a job container. */
|
|
70
|
+
function containerFileSystem(access) {
|
|
71
|
+
const p = path.posix;
|
|
72
|
+
const temps = [];
|
|
73
|
+
const tempDir = () => {
|
|
74
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'secureci-files-'));
|
|
75
|
+
temps.push(dir);
|
|
76
|
+
return dir;
|
|
77
|
+
};
|
|
78
|
+
const inWorkspace = (logical) => logical === access.workspace || logical.startsWith(access.workspace + '/');
|
|
79
|
+
const toHost = (logical) => path.join(access.hostWorkspace, ...p.relative(access.workspace, logical).split('/'));
|
|
80
|
+
return {
|
|
81
|
+
workspace: access.workspace,
|
|
82
|
+
home: access.home,
|
|
83
|
+
path: p,
|
|
84
|
+
readable: async (logical) => {
|
|
85
|
+
if (inWorkspace(logical)) {
|
|
86
|
+
const host = toHost(logical);
|
|
87
|
+
return fs.existsSync(host) ? host : null;
|
|
88
|
+
}
|
|
89
|
+
const target = path.join(tempDir(), 'copy');
|
|
90
|
+
return (await access.copyFrom(logical, target)) ? target : null;
|
|
91
|
+
},
|
|
92
|
+
writeInto: async (logical, fill) => {
|
|
93
|
+
if (inWorkspace(logical)) {
|
|
94
|
+
const host = toHost(logical);
|
|
95
|
+
fs.mkdirSync(host, { recursive: true });
|
|
96
|
+
fill(host);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const stage = tempDir();
|
|
100
|
+
fill(stage);
|
|
101
|
+
await access.mkdir(logical);
|
|
102
|
+
if (!(await access.copyTo(`${stage}${path.sep}.`, logical))) {
|
|
103
|
+
throw new Error(`Could not copy files into the container at ${logical}`);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
cleanup: () => {
|
|
107
|
+
for (const dir of temps.splice(0)) {
|
|
108
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function listFiles(hostRoot, base = '') {
|
|
114
|
+
const stat = fs.statSync(hostRoot);
|
|
115
|
+
if (stat.isFile())
|
|
116
|
+
return [base];
|
|
117
|
+
const files = [];
|
|
118
|
+
for (const entry of fs.readdirSync(hostRoot, { withFileTypes: true })) {
|
|
119
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
120
|
+
const full = path.join(hostRoot, entry.name);
|
|
121
|
+
if (entry.isDirectory())
|
|
122
|
+
files.push(...listFiles(full, rel));
|
|
123
|
+
else if (entry.isFile())
|
|
124
|
+
files.push(rel);
|
|
125
|
+
}
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
/** Expand a leading `~` and resolve against the workspace. */
|
|
129
|
+
function resolveLogical(fsys, raw) {
|
|
130
|
+
const expanded = raw === '~' || raw.startsWith('~/')
|
|
131
|
+
? fsys.path.join(fsys.home, raw.slice(1))
|
|
132
|
+
: raw;
|
|
133
|
+
return fsys.path.resolve(fsys.workspace, expanded);
|
|
134
|
+
}
|
|
135
|
+
function commonAncestor(p, dirs) {
|
|
136
|
+
let common = dirs[0].split(p.sep);
|
|
137
|
+
for (const dir of dirs.slice(1)) {
|
|
138
|
+
const parts = dir.split(p.sep);
|
|
139
|
+
let i = 0;
|
|
140
|
+
while (i < common.length && i < parts.length && common[i] === parts[i]) {
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
common = common.slice(0, i);
|
|
144
|
+
}
|
|
145
|
+
return common.join(p.sep) || p.sep;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Files matching multi-line path patterns, the way upload-artifact and
|
|
149
|
+
* cache read them: each line is a file, a directory (all files under it)
|
|
150
|
+
* or a glob (`*`, `**`, `?`); `!pattern` excludes. `root` is the least
|
|
151
|
+
* common ancestor of the search paths, which file paths are relative to.
|
|
152
|
+
*/
|
|
153
|
+
async function collectFiles(fsys, patterns, options) {
|
|
154
|
+
const p = fsys.path;
|
|
155
|
+
const lines = patterns
|
|
156
|
+
.split('\n')
|
|
157
|
+
.map((line) => line.trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
const excludes = lines
|
|
160
|
+
.filter((line) => line.startsWith('!'))
|
|
161
|
+
.map((line) => (0, hash_files_1.globToRegExp)(resolveLogical(fsys, line.slice(1).trim())));
|
|
162
|
+
const searchPaths = [];
|
|
163
|
+
const found = new Map();
|
|
164
|
+
for (const line of lines.filter((l) => !l.startsWith('!'))) {
|
|
165
|
+
const pattern = resolveLogical(fsys, line);
|
|
166
|
+
const globAt = pattern.search(/[*?[]/);
|
|
167
|
+
const searchPath = globAt === -1
|
|
168
|
+
? pattern
|
|
169
|
+
: pattern.slice(0, pattern.lastIndexOf('/', globAt)) || p.sep;
|
|
170
|
+
const host = await fsys.readable(searchPath);
|
|
171
|
+
if (!host)
|
|
172
|
+
continue;
|
|
173
|
+
const isFile = fs.statSync(host).isFile();
|
|
174
|
+
searchPaths.push(isFile ? p.dirname(searchPath) : searchPath);
|
|
175
|
+
const matcher = globAt === -1 ? undefined : (0, hash_files_1.globToRegExp)(pattern);
|
|
176
|
+
for (const rel of listFiles(host)) {
|
|
177
|
+
const logical = rel ? p.join(searchPath, rel) : searchPath;
|
|
178
|
+
if (matcher && !matcher.test(logical))
|
|
179
|
+
continue;
|
|
180
|
+
found.set(logical, {
|
|
181
|
+
logical,
|
|
182
|
+
host: rel ? path.join(host, ...rel.split('/')) : host
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const root = searchPaths.length
|
|
187
|
+
? commonAncestor(p, searchPaths)
|
|
188
|
+
: fsys.workspace;
|
|
189
|
+
const files = [...found.values()].filter((file) => !excludes.some((re) => re.test(file.logical)) &&
|
|
190
|
+
(options.includeHidden ||
|
|
191
|
+
!p
|
|
192
|
+
.relative(root, file.logical)
|
|
193
|
+
.split('/')
|
|
194
|
+
.some((part) => part.startsWith('.'))));
|
|
195
|
+
return { root, files };
|
|
196
|
+
}
|
|
197
|
+
function copyFile(from, to) {
|
|
198
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
199
|
+
fs.copyFileSync(from, to);
|
|
200
|
+
}
|
|
201
|
+
function copyTree(from, to) {
|
|
202
|
+
fs.cpSync(from, to, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
function secureciDir(...parts) {
|
|
205
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.tmpdir();
|
|
206
|
+
return path.join(home, '.secureci', ...parts);
|
|
207
|
+
}
|
|
208
|
+
const ok = (output, outputs = {}) => ({
|
|
209
|
+
success: true,
|
|
210
|
+
output,
|
|
211
|
+
outputs
|
|
212
|
+
});
|
|
213
|
+
const fail = (error) => ({
|
|
214
|
+
success: false,
|
|
215
|
+
output: '',
|
|
216
|
+
outputs: {},
|
|
217
|
+
error
|
|
218
|
+
});
|
|
219
|
+
const isTrue = (value) => value?.trim().toLowerCase() === 'true';
|
|
220
|
+
/** Artifacts are kept per run, for 7 days. */
|
|
221
|
+
const ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
222
|
+
function artifactsDir(runId) {
|
|
223
|
+
return secureciDir('artifacts', runId.replace(/[^\w.-]/g, '_'));
|
|
224
|
+
}
|
|
225
|
+
function pruneOldArtifacts() {
|
|
226
|
+
const base = secureciDir('artifacts');
|
|
227
|
+
let runs;
|
|
228
|
+
try {
|
|
229
|
+
runs = fs.readdirSync(base);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const run of runs) {
|
|
235
|
+
const dir = path.join(base, run);
|
|
236
|
+
try {
|
|
237
|
+
if (Date.now() - fs.statSync(dir).mtimeMs > ARTIFACT_RETENTION_MS) {
|
|
238
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Best effort
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* GitHub's invalid artifact-name characters, plus anything that would let a
|
|
248
|
+
* crafted name escape the per-run store (`/`, `\\`, `..`). The store path
|
|
249
|
+
* is built from the name, so an unchecked name like `../../../.ssh` would
|
|
250
|
+
* write onto the host even from a fully containerized job.
|
|
251
|
+
*/
|
|
252
|
+
function safeName(name) {
|
|
253
|
+
try {
|
|
254
|
+
assertSafeArtifactName(name);
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function assertSafeArtifactName(name) {
|
|
262
|
+
if (name === '' ||
|
|
263
|
+
name === '.' ||
|
|
264
|
+
name === '..' ||
|
|
265
|
+
/[\\/"<>|:*?\r\n]/.test(name)) {
|
|
266
|
+
throw new Error(`Invalid artifact name: "${name}". Artifact names cannot be empty or contain any of: \\ / " < > | : * ? or line breaks.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** `actions/upload-artifact`. */
|
|
270
|
+
async function uploadArtifact(fsys, inputs, runId) {
|
|
271
|
+
const name = inputs.name || 'artifact';
|
|
272
|
+
try {
|
|
273
|
+
assertSafeArtifactName(name);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
277
|
+
}
|
|
278
|
+
if (!inputs.path)
|
|
279
|
+
return fail('Input required and not supplied: path');
|
|
280
|
+
const { root, files } = await collectFiles(fsys, inputs.path, {
|
|
281
|
+
includeHidden: isTrue(inputs['include-hidden-files'])
|
|
282
|
+
});
|
|
283
|
+
if (files.length === 0) {
|
|
284
|
+
const mode = (inputs['if-no-files-found'] || 'warn').toLowerCase();
|
|
285
|
+
const message = `No files were found with the provided path: ${inputs.path.trim()}. No artifacts will be uploaded.`;
|
|
286
|
+
if (mode === 'error')
|
|
287
|
+
return fail(message);
|
|
288
|
+
return { ...ok(''), ...(mode === 'ignore' ? {} : { warning: message }) };
|
|
289
|
+
}
|
|
290
|
+
pruneOldArtifacts();
|
|
291
|
+
const target = path.join(artifactsDir(runId), name);
|
|
292
|
+
if (fs.existsSync(target)) {
|
|
293
|
+
if (!isTrue(inputs.overwrite)) {
|
|
294
|
+
return fail(`An artifact with the name "${name}" already exists in this run; set overwrite: true to replace it`);
|
|
295
|
+
}
|
|
296
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
const digest = crypto.createHash('sha256');
|
|
299
|
+
for (const file of files.sort((a, b) => a.logical.localeCompare(b.logical))) {
|
|
300
|
+
const rel = fsys.path.relative(root, file.logical) ||
|
|
301
|
+
fsys.path.basename(file.logical);
|
|
302
|
+
copyFile(file.host, path.join(target, ...rel.split('/')));
|
|
303
|
+
digest.update(rel).update(fs.readFileSync(file.host));
|
|
304
|
+
}
|
|
305
|
+
const id = String(parseInt(crypto.createHash('sha1').update(target).digest('hex').slice(0, 8), 16));
|
|
306
|
+
return ok(`Uploaded ${files.length} file(s) as artifact "${name}" to ${target}`, {
|
|
307
|
+
'artifact-id': id,
|
|
308
|
+
'artifact-url': '',
|
|
309
|
+
'artifact-digest': digest.digest('hex')
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** `actions/download-artifact`. */
|
|
313
|
+
async function downloadArtifact(fsys, inputs, runId) {
|
|
314
|
+
const store = artifactsDir(runId);
|
|
315
|
+
const available = fs.existsSync(store) ? fs.readdirSync(store).sort() : [];
|
|
316
|
+
const dest = resolveLogical(fsys, inputs.path || '.');
|
|
317
|
+
let names;
|
|
318
|
+
if (inputs.name) {
|
|
319
|
+
try {
|
|
320
|
+
assertSafeArtifactName(inputs.name);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
324
|
+
}
|
|
325
|
+
if (!available.includes(inputs.name)) {
|
|
326
|
+
return fail(`Artifact not found for name: ${inputs.name}`);
|
|
327
|
+
}
|
|
328
|
+
names = [inputs.name];
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const pattern = inputs.pattern ? (0, hash_files_1.globToRegExp)(inputs.pattern) : undefined;
|
|
332
|
+
names = available.filter((name) => safeName(name) && (!pattern || pattern.test(name)));
|
|
333
|
+
}
|
|
334
|
+
// A single named artifact, or merge-multiple, goes straight into path;
|
|
335
|
+
// otherwise each artifact gets its own <path>/<name> directory.
|
|
336
|
+
const flat = !!inputs.name || isTrue(inputs['merge-multiple']);
|
|
337
|
+
await fsys.writeInto(dest, (hostDest) => {
|
|
338
|
+
for (const name of names) {
|
|
339
|
+
copyTree(path.join(store, name), flat ? hostDest : path.join(hostDest, name));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return ok(`Downloaded ${names.length} artifact(s) to ${dest}${names.length ? `: ${names.join(', ')}` : ''}`, { 'download-path': dest });
|
|
343
|
+
}
|
|
344
|
+
function cacheScope(repository) {
|
|
345
|
+
return secureciDir('step-cache', repository.replace(/[^\w.-]+/g, '_') || 'default');
|
|
346
|
+
}
|
|
347
|
+
/** Caches are versioned by their paths, as on GitHub. */
|
|
348
|
+
function cacheVersion(paths) {
|
|
349
|
+
return crypto
|
|
350
|
+
.createHash('sha256')
|
|
351
|
+
.update(paths
|
|
352
|
+
.split('\n')
|
|
353
|
+
.map((line) => line.trim())
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.join('\n'))
|
|
356
|
+
.digest('hex');
|
|
357
|
+
}
|
|
358
|
+
function cacheEntryDir(scope, version, key) {
|
|
359
|
+
return path.join(scope, crypto.createHash('sha256').update(`${version}\0${key}`).digest('hex'));
|
|
360
|
+
}
|
|
361
|
+
function findCache(scope, version, key, restoreKeys) {
|
|
362
|
+
const exact = cacheEntryDir(scope, version, key);
|
|
363
|
+
if (fs.existsSync(path.join(exact, 'meta.json'))) {
|
|
364
|
+
return { dir: exact, key, created: 0 };
|
|
365
|
+
}
|
|
366
|
+
let entries = [];
|
|
367
|
+
try {
|
|
368
|
+
entries = fs
|
|
369
|
+
.readdirSync(scope)
|
|
370
|
+
.map((name) => {
|
|
371
|
+
try {
|
|
372
|
+
const meta = JSON.parse(fs.readFileSync(path.join(scope, name, 'meta.json'), 'utf8'));
|
|
373
|
+
return meta.version === version
|
|
374
|
+
? {
|
|
375
|
+
dir: path.join(scope, name),
|
|
376
|
+
key: meta.key,
|
|
377
|
+
created: meta.created
|
|
378
|
+
}
|
|
379
|
+
: undefined;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
.filter((entry) => !!entry);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
for (const prefix of restoreKeys) {
|
|
391
|
+
const match = entries
|
|
392
|
+
.filter((entry) => entry.key.startsWith(prefix))
|
|
393
|
+
.sort((a, b) => b.created - a.created)[0];
|
|
394
|
+
if (match)
|
|
395
|
+
return match;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
/** Where a cached file is stored: relative to the workspace, home, or /. */
|
|
400
|
+
function cachedLocation(fsys, logical) {
|
|
401
|
+
const p = fsys.path;
|
|
402
|
+
for (const [area, base] of [
|
|
403
|
+
['workspace', fsys.workspace],
|
|
404
|
+
['home', fsys.home]
|
|
405
|
+
]) {
|
|
406
|
+
const rel = p.relative(base, logical);
|
|
407
|
+
if (rel && !rel.startsWith('..') && !p.isAbsolute(rel))
|
|
408
|
+
return `${area}/${rel}`;
|
|
409
|
+
}
|
|
410
|
+
return `root/${logical.replace(/^\/+/, '')}`;
|
|
411
|
+
}
|
|
412
|
+
async function restoreCache(fsys, entryDir) {
|
|
413
|
+
const files = path.join(entryDir, 'files');
|
|
414
|
+
for (const [area, base] of [
|
|
415
|
+
['workspace', fsys.workspace],
|
|
416
|
+
['home', fsys.home],
|
|
417
|
+
['root', '/']
|
|
418
|
+
]) {
|
|
419
|
+
const from = path.join(files, area);
|
|
420
|
+
if (fs.existsSync(from)) {
|
|
421
|
+
await fsys.writeInto(base, (hostDir) => copyTree(from, hostDir));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function saveCache(fsys, scope, key, paths) {
|
|
426
|
+
const version = cacheVersion(paths);
|
|
427
|
+
const dir = cacheEntryDir(scope, version, key);
|
|
428
|
+
if (fs.existsSync(path.join(dir, 'meta.json'))) {
|
|
429
|
+
return ok(`Cache for key "${key}" already exists; not saving`);
|
|
430
|
+
}
|
|
431
|
+
const { files } = await collectFiles(fsys, paths, { includeHidden: true });
|
|
432
|
+
if (files.length === 0) {
|
|
433
|
+
return {
|
|
434
|
+
...ok(''),
|
|
435
|
+
warning: 'Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.'
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const staging = `${dir}.tmp-${process.pid}`;
|
|
439
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
440
|
+
for (const file of files) {
|
|
441
|
+
copyFile(file.host, path.join(staging, 'files', ...cachedLocation(fsys, file.logical).split('/')));
|
|
442
|
+
}
|
|
443
|
+
fs.writeFileSync(path.join(staging, 'meta.json'), JSON.stringify({ key, version, created: Date.now() }));
|
|
444
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
445
|
+
fs.renameSync(staging, dir);
|
|
446
|
+
return ok(`Cache saved with key: ${key} (${files.length} file(s))`);
|
|
447
|
+
}
|
|
448
|
+
/** `actions/cache`, `actions/cache/restore` and `actions/cache/save`. */
|
|
449
|
+
async function runCacheAction(kind, fsys, inputs, repository) {
|
|
450
|
+
const key = (inputs.key || '').trim();
|
|
451
|
+
const paths = inputs.path || '';
|
|
452
|
+
if (!key)
|
|
453
|
+
return fail('Input required and not supplied: key');
|
|
454
|
+
if (!paths.trim())
|
|
455
|
+
return fail('Input required and not supplied: path');
|
|
456
|
+
const scope = cacheScope(repository);
|
|
457
|
+
if (kind === 'cache-save')
|
|
458
|
+
return saveCache(fsys, scope, key, paths);
|
|
459
|
+
const restoreKeys = (inputs['restore-keys'] || '')
|
|
460
|
+
.split('\n')
|
|
461
|
+
.map((line) => line.trim())
|
|
462
|
+
.filter(Boolean);
|
|
463
|
+
const found = findCache(scope, cacheVersion(paths), key, restoreKeys);
|
|
464
|
+
const exact = found?.key === key;
|
|
465
|
+
if (!found && isTrue(inputs['fail-on-cache-miss'])) {
|
|
466
|
+
return fail(`Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}`);
|
|
467
|
+
}
|
|
468
|
+
if (found && !isTrue(inputs['lookup-only'])) {
|
|
469
|
+
await restoreCache(fsys, found.dir);
|
|
470
|
+
}
|
|
471
|
+
const outputs = { 'cache-primary-key': key };
|
|
472
|
+
if (found) {
|
|
473
|
+
outputs['cache-hit'] = String(exact);
|
|
474
|
+
outputs['cache-matched-key'] = found.key;
|
|
475
|
+
}
|
|
476
|
+
const result = ok(found
|
|
477
|
+
? `Cache ${isTrue(inputs['lookup-only']) ? 'found' : 'restored'} from key: ${found.key}`
|
|
478
|
+
: `Cache not found for input keys: ${[key, ...restoreKeys].join(', ')}`, outputs);
|
|
479
|
+
if (kind === 'cache' && !exact && !isTrue(inputs['lookup-only'])) {
|
|
480
|
+
result.post = (postFs) => saveCache(postFs, scope, key, paths);
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
//# sourceMappingURL=builtin-actions.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
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.copyWorkingCopy = copyWorkingCopy;
|
|
37
|
+
exports.isLocalCheckout = isLocalCheckout;
|
|
38
|
+
exports.localCheckout = localCheckout;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const logger_1 = require("../utils/logger");
|
|
42
|
+
const expression_1 = require("./expression");
|
|
43
|
+
/** Marks a `path:` copy made by localCheckout, so it may be replaced. */
|
|
44
|
+
const CHECKOUT_MARKER = '.secureci-checkout';
|
|
45
|
+
/**
|
|
46
|
+
* Copy `srcDir` into `dstDir`, skipping `exclude` (the copy's own location,
|
|
47
|
+
* which may lie inside `srcDir`) and earlier local checkout copies.
|
|
48
|
+
*/
|
|
49
|
+
function copyWorkingCopy(srcDir, dstDir, exclude) {
|
|
50
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
51
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
52
|
+
const src = path.join(srcDir, entry.name);
|
|
53
|
+
const dst = path.join(dstDir, entry.name);
|
|
54
|
+
if (src === exclude)
|
|
55
|
+
continue;
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
if (fs.existsSync(path.join(src, CHECKOUT_MARKER)))
|
|
58
|
+
continue;
|
|
59
|
+
if (exclude.startsWith(src + path.sep)) {
|
|
60
|
+
copyWorkingCopy(src, dst, exclude);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
fs.cpSync(src, dst, {
|
|
65
|
+
recursive: true,
|
|
66
|
+
verbatimSymlinks: true,
|
|
67
|
+
filter: (file) => !fs.existsSync(path.join(file, CHECKOUT_MARKER))
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether a step is `actions/checkout` of the repository being run (no
|
|
73
|
+
* `repository:` input, or the current one in any case), which is served
|
|
74
|
+
* from the local working copy rather than cloned.
|
|
75
|
+
*/
|
|
76
|
+
function isLocalCheckout(uses, withValues, ctx) {
|
|
77
|
+
if (!/^actions\/checkout@/i.test(uses))
|
|
78
|
+
return false;
|
|
79
|
+
const repository = (0, expression_1.substituteRecord)(withValues, ctx).repository ?? '';
|
|
80
|
+
return (repository === '' ||
|
|
81
|
+
repository.toLowerCase() === String(ctx.github.repository).toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `actions/checkout` of the current repository: the workspace already is
|
|
85
|
+
* the user's working copy (uncommitted changes included), so nothing is
|
|
86
|
+
* cloned and `.git` is never touched. With `path:`, the working copy is
|
|
87
|
+
* copied into that subdirectory and the copy is removed at job end.
|
|
88
|
+
*/
|
|
89
|
+
function localCheckout(step, ctx, runtime, startTime) {
|
|
90
|
+
const withValues = (0, expression_1.substituteRecord)(step.with, ctx);
|
|
91
|
+
const outputs = {
|
|
92
|
+
ref: String(ctx.github.ref ?? ''),
|
|
93
|
+
commit: String(ctx.github.sha ?? '')
|
|
94
|
+
};
|
|
95
|
+
const fail = (error) => {
|
|
96
|
+
logger_1.logger.error(error);
|
|
97
|
+
return { step, success: false, error, duration: Date.now() - startTime };
|
|
98
|
+
};
|
|
99
|
+
const ref = withValues.ref;
|
|
100
|
+
if (ref &&
|
|
101
|
+
![ctx.github.ref, ctx.github.ref_name, ctx.github.sha].includes(ref)) {
|
|
102
|
+
logger_1.logger.warn(`actions/checkout: ignoring ref "${ref}"; using the local working copy (pass --no-local-checkout to run the real action)`);
|
|
103
|
+
}
|
|
104
|
+
const workspace = process.cwd();
|
|
105
|
+
const dest = path.resolve(workspace, withValues.path || '.');
|
|
106
|
+
const relative = path.relative(workspace, dest);
|
|
107
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
108
|
+
return fail(`actions/checkout: path "${withValues.path}" is outside the workspace`);
|
|
109
|
+
}
|
|
110
|
+
let output = 'Using local working copy';
|
|
111
|
+
if (relative !== '') {
|
|
112
|
+
if (fs.existsSync(dest)) {
|
|
113
|
+
if (fs.existsSync(path.join(dest, CHECKOUT_MARKER))) {
|
|
114
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
else if (fs.readdirSync(dest).length > 0) {
|
|
117
|
+
return fail(`actions/checkout: path "${withValues.path}" already exists in the working copy; remove it or pass --no-local-checkout`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
copyWorkingCopy(workspace, dest, dest);
|
|
121
|
+
fs.writeFileSync(path.join(dest, CHECKOUT_MARKER), '');
|
|
122
|
+
output = `Using local working copy (copied to ${relative})`;
|
|
123
|
+
const postStep = { name: `Post ${step.name || step.uses}` };
|
|
124
|
+
runtime.postSteps.push({
|
|
125
|
+
step: postStep,
|
|
126
|
+
condition: 'always()',
|
|
127
|
+
run: async () => {
|
|
128
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
129
|
+
return { step: postStep, success: true, duration: 0 };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
logger_1.logger.info(output);
|
|
134
|
+
return {
|
|
135
|
+
step,
|
|
136
|
+
success: true,
|
|
137
|
+
output,
|
|
138
|
+
outputs,
|
|
139
|
+
duration: Date.now() - startTime
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=checkout.js.map
|