@izykitten/ce 0.0.3-dev.0 → 0.0.3-dev.10
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/README.md +290 -26
- package/dist/ce.js +3369 -1006
- package/package.json +4 -3
package/dist/ce.js
CHANGED
|
@@ -1,116 +1,3 @@
|
|
|
1
|
-
// src/command.ts
|
|
2
|
-
var VALUE_OPTIONS = {
|
|
3
|
-
docker: {
|
|
4
|
-
long: new Set(["config", "context", "host", "log-level", "tlscacert", "tlscert", "tlskey", "builder"]),
|
|
5
|
-
short: "cHl"
|
|
6
|
-
},
|
|
7
|
-
podman: {
|
|
8
|
-
long: new Set([
|
|
9
|
-
"cdi-spec-dir",
|
|
10
|
-
"cgroup-manager",
|
|
11
|
-
"cni-config-dir",
|
|
12
|
-
"config",
|
|
13
|
-
"conmon",
|
|
14
|
-
"connection",
|
|
15
|
-
"context",
|
|
16
|
-
"cpu-profile",
|
|
17
|
-
"db-backend",
|
|
18
|
-
"default-mounts-file",
|
|
19
|
-
"events-backend",
|
|
20
|
-
"hooks-dir",
|
|
21
|
-
"host",
|
|
22
|
-
"identity",
|
|
23
|
-
"imagestore",
|
|
24
|
-
"log-level",
|
|
25
|
-
"max-workers",
|
|
26
|
-
"memory-profile",
|
|
27
|
-
"module",
|
|
28
|
-
"namespace",
|
|
29
|
-
"network-backend",
|
|
30
|
-
"network-cmd-path",
|
|
31
|
-
"network-config-dir",
|
|
32
|
-
"out",
|
|
33
|
-
"pull-option",
|
|
34
|
-
"registries-conf",
|
|
35
|
-
"root",
|
|
36
|
-
"runroot",
|
|
37
|
-
"runtime",
|
|
38
|
-
"runtime-flag",
|
|
39
|
-
"ssh",
|
|
40
|
-
"storage-driver",
|
|
41
|
-
"storage-opt",
|
|
42
|
-
"tmpdir",
|
|
43
|
-
"url",
|
|
44
|
-
"volumepath"
|
|
45
|
-
]),
|
|
46
|
-
short: "cH"
|
|
47
|
-
},
|
|
48
|
-
buildah: {
|
|
49
|
-
long: new Set([
|
|
50
|
-
"cgroup-manager",
|
|
51
|
-
"cpu-profile",
|
|
52
|
-
"default-mounts-file",
|
|
53
|
-
"experimental-timeout",
|
|
54
|
-
"imagestore",
|
|
55
|
-
"log-level",
|
|
56
|
-
"memory-profile",
|
|
57
|
-
"registries-conf",
|
|
58
|
-
"registries-conf-dir",
|
|
59
|
-
"root",
|
|
60
|
-
"runroot",
|
|
61
|
-
"short-name-alias-conf",
|
|
62
|
-
"storage-driver",
|
|
63
|
-
"storage-opt",
|
|
64
|
-
"userns-gid-map",
|
|
65
|
-
"userns-uid-map"
|
|
66
|
-
]),
|
|
67
|
-
short: ""
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
function takesNext(engine, arg) {
|
|
71
|
-
const options = VALUE_OPTIONS[engine];
|
|
72
|
-
if (arg.startsWith("--"))
|
|
73
|
-
return options.long.has(arg.slice(2));
|
|
74
|
-
for (let j = 1;j < arg.length; j++) {
|
|
75
|
-
if (options.short.includes(arg[j]))
|
|
76
|
-
return j === arg.length - 1;
|
|
77
|
-
}
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
var COMMANDS = {
|
|
81
|
-
docker: [
|
|
82
|
-
{ words: ["build"] },
|
|
83
|
-
{ words: ["image", "build"] },
|
|
84
|
-
{ words: ["builder", "build"] },
|
|
85
|
-
{ words: ["buildx", "build"] },
|
|
86
|
-
{ words: ["buildx", "b"] },
|
|
87
|
-
{ words: ["buildx", "imagetools", "create"], tagsOnly: true }
|
|
88
|
-
],
|
|
89
|
-
podman: [{ words: ["build"] }, { words: ["image", "build"] }, { words: ["buildx", "build"] }],
|
|
90
|
-
buildah: [{ words: ["build"] }, { words: ["bud"] }]
|
|
91
|
-
};
|
|
92
|
-
function findBuildCommand(engine, argv) {
|
|
93
|
-
const words = [];
|
|
94
|
-
for (let i = 0;i < argv.length; i++) {
|
|
95
|
-
const arg = argv[i];
|
|
96
|
-
if (arg === "--")
|
|
97
|
-
return null;
|
|
98
|
-
if (arg.startsWith("-")) {
|
|
99
|
-
if (takesNext(engine, arg))
|
|
100
|
-
i++;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
words.push(arg);
|
|
104
|
-
const candidates = COMMANDS[engine].filter((command) => words.every((word, j) => command.words[j] === word));
|
|
105
|
-
const match = candidates.find((command) => command.words.length === words.length);
|
|
106
|
-
if (match)
|
|
107
|
-
return { start: i + 1, name: arg, tagsOnly: match.tagsOnly ?? false };
|
|
108
|
-
if (!candidates.length)
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
1
|
// src/errors.ts
|
|
115
2
|
class CeError extends Error {
|
|
116
3
|
name = "CeError";
|
|
@@ -210,189 +97,237 @@ function registryKey(value) {
|
|
|
210
97
|
return registryServer(value).toLowerCase();
|
|
211
98
|
}
|
|
212
99
|
|
|
213
|
-
// src/
|
|
214
|
-
var
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
var SEMVER = /^v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?)$/u;
|
|
231
|
-
var nonEmpty = (value) => value ? value : undefined;
|
|
232
|
-
|
|
233
|
-
class CiMetadata {
|
|
234
|
-
#env;
|
|
235
|
-
#cache = new Map;
|
|
236
|
-
#ref;
|
|
237
|
-
#event;
|
|
238
|
-
constructor(env = process.env) {
|
|
239
|
-
this.#env = env;
|
|
240
|
-
}
|
|
241
|
-
known(name) {
|
|
242
|
-
return CI_VARIABLES.includes(name) || ENV_VARIABLE.test(name);
|
|
243
|
-
}
|
|
244
|
-
get(name) {
|
|
245
|
-
const envName = ENV_VARIABLE.exec(name)?.[1];
|
|
246
|
-
if (envName)
|
|
247
|
-
return nonEmpty(this.#env[envName]);
|
|
248
|
-
if (!this.#cache.has(name))
|
|
249
|
-
this.#cache.set(name, this.#resolve(name));
|
|
250
|
-
return this.#cache.get(name);
|
|
251
|
-
}
|
|
252
|
-
labels() {
|
|
253
|
-
const labels = {};
|
|
254
|
-
const add = (key, value) => {
|
|
255
|
-
try {
|
|
256
|
-
const v = value();
|
|
257
|
-
if (v)
|
|
258
|
-
labels[`org.opencontainers.image.${key}`] = v;
|
|
259
|
-
} catch (exc) {
|
|
260
|
-
if (!(exc instanceof CeError))
|
|
261
|
-
throw exc;
|
|
262
|
-
}
|
|
100
|
+
// src/oidc.ts
|
|
101
|
+
var REFRESH_WINDOW_S = 5 * 60;
|
|
102
|
+
var REQUEST_TIMEOUT_MS = 30000;
|
|
103
|
+
var decodeUtf8 = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
104
|
+
async function forgejoOidcJwt(audience, requestUrl, requestToken) {
|
|
105
|
+
assertAbsoluteHttpUrl(requestUrl, "token request URL must be an absolute HTTP(S) URL");
|
|
106
|
+
let tokenUrl;
|
|
107
|
+
let headers;
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(requestUrl);
|
|
110
|
+
if (url.username || url.password)
|
|
111
|
+
throw new Error("credentials in URL");
|
|
112
|
+
url.searchParams.set("audience", audience);
|
|
113
|
+
tokenUrl = url.href;
|
|
114
|
+
headers = {
|
|
115
|
+
Accept: "application/json",
|
|
116
|
+
Authorization: `bearer ${requestToken}`
|
|
263
117
|
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
add("revision", () => this.#ciEnv("SHA"));
|
|
268
|
-
add("version", () => this.get("version") ?? this.#rawRef());
|
|
269
|
-
add("title", () => repository?.split("/").at(-1));
|
|
270
|
-
add("description", () => {
|
|
271
|
-
const repo = this.#payload().repository;
|
|
272
|
-
return isObject(repo) && typeof repo.description === "string" ? repo.description : undefined;
|
|
273
|
-
});
|
|
274
|
-
return labels;
|
|
275
|
-
}
|
|
276
|
-
#resolve(name) {
|
|
277
|
-
switch (name) {
|
|
278
|
-
case "image": {
|
|
279
|
-
const server = this.#require("SERVER_URL", "{image}");
|
|
280
|
-
const repository = this.#require("REPOSITORY", "{image}");
|
|
281
|
-
return `${registryServer(server)}/${repository}`.toLowerCase();
|
|
282
|
-
}
|
|
283
|
-
case "ref":
|
|
284
|
-
return this.#rawRef();
|
|
285
|
-
case "branch": {
|
|
286
|
-
const ref = this.#parsedRef();
|
|
287
|
-
return ref.kind === "branch" ? ref.name : undefined;
|
|
288
|
-
}
|
|
289
|
-
case "tag": {
|
|
290
|
-
const ref = this.#parsedRef();
|
|
291
|
-
return ref.kind === "tag" ? ref.name : undefined;
|
|
292
|
-
}
|
|
293
|
-
case "pr": {
|
|
294
|
-
const ref = this.#parsedRef();
|
|
295
|
-
return ref.kind === "pr" ? ref.number : undefined;
|
|
296
|
-
}
|
|
297
|
-
case "default": {
|
|
298
|
-
const ref = this.#parsedRef();
|
|
299
|
-
if (ref.kind !== "branch")
|
|
300
|
-
return;
|
|
301
|
-
return ref.name === this.#defaultBranch() ? ref.name : undefined;
|
|
302
|
-
}
|
|
303
|
-
case "revision":
|
|
304
|
-
return this.#require("SHA", `{${name}}`);
|
|
305
|
-
case "sha":
|
|
306
|
-
return this.#require("SHA", `{${name}}`).slice(0, 7);
|
|
307
|
-
case "version":
|
|
308
|
-
return this.#semver()?.[1];
|
|
309
|
-
case "major":
|
|
310
|
-
return this.#semver()?.[2];
|
|
311
|
-
case "minor":
|
|
312
|
-
return this.#semver()?.[3];
|
|
313
|
-
case "patch":
|
|
314
|
-
return this.#semver()?.[4];
|
|
315
|
-
case "stable": {
|
|
316
|
-
const semver = this.#semver();
|
|
317
|
-
return semver && semver[5] === undefined ? semver[1] : undefined;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
throw new CeError(`ce: unknown placeholder {${name}}`);
|
|
118
|
+
new Headers(headers);
|
|
119
|
+
} catch {
|
|
120
|
+
throw new CeError("token request URL or headers are invalid");
|
|
321
121
|
}
|
|
322
|
-
|
|
323
|
-
|
|
122
|
+
let body;
|
|
123
|
+
try {
|
|
124
|
+
const response = await fetch(tokenUrl, {
|
|
125
|
+
headers,
|
|
126
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok)
|
|
129
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
130
|
+
body = await response.arrayBuffer();
|
|
131
|
+
} catch (exc) {
|
|
132
|
+
throw new CeError(`token request failed: ${errorMessage(exc)}`);
|
|
324
133
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
return value;
|
|
134
|
+
let payload;
|
|
135
|
+
try {
|
|
136
|
+
payload = JSON.parse(decodeUtf8(body));
|
|
137
|
+
} catch {
|
|
138
|
+
throw new CeError("token response is not valid UTF-8 JSON");
|
|
331
139
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
140
|
+
if (!isObject(payload))
|
|
141
|
+
throw new CeError("token response is not a JSON object");
|
|
142
|
+
const jwt = payload.value;
|
|
143
|
+
if (typeof jwt !== "string" || !jwt.trim()) {
|
|
144
|
+
throw new CeError("token response has no non-empty string 'value'");
|
|
335
145
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
throw new CeError(`ce: cannot find the pull request number for ${event} on ${ref}`);
|
|
348
|
-
this.#ref = { kind: "pr", number };
|
|
349
|
-
} else if (ref.startsWith("refs/heads/")) {
|
|
350
|
-
this.#ref = { kind: "branch", name: ref.slice("refs/heads/".length) };
|
|
351
|
-
} else if (ref.startsWith("refs/tags/")) {
|
|
352
|
-
this.#ref = { kind: "tag", name: ref.slice("refs/tags/".length) };
|
|
353
|
-
} else {
|
|
354
|
-
throw new CeError(`ce: cannot tell what ${JSON.stringify(ref)} is: not a branch, tag or pull request`);
|
|
355
|
-
}
|
|
356
|
-
return this.#ref;
|
|
146
|
+
return jwt.trim();
|
|
147
|
+
}
|
|
148
|
+
function jwtExpiry(jwt) {
|
|
149
|
+
const parts = jwt.split(".");
|
|
150
|
+
if (parts.length !== 3)
|
|
151
|
+
throw new CeError("OIDC token is not a three-part JWT");
|
|
152
|
+
let payload;
|
|
153
|
+
try {
|
|
154
|
+
payload = JSON.parse(decodeUtf8(Buffer.from(parts[1], "base64url")));
|
|
155
|
+
} catch {
|
|
156
|
+
throw new CeError("OIDC token has an invalid JWT payload");
|
|
357
157
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
158
|
+
const expiry = isObject(payload) ? payload.exp : undefined;
|
|
159
|
+
if (typeof expiry !== "number")
|
|
160
|
+
throw new CeError("OIDC token has no numeric 'exp' claim");
|
|
161
|
+
return expiry;
|
|
162
|
+
}
|
|
163
|
+
function needsRefresh(expiry) {
|
|
164
|
+
return expiry <= Date.now() / 1000 + REFRESH_WINDOW_S;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/state.ts
|
|
168
|
+
import fs3 from "node:fs";
|
|
169
|
+
import { homedir } from "node:os";
|
|
170
|
+
import path2 from "node:path";
|
|
171
|
+
|
|
172
|
+
// src/lock.ts
|
|
173
|
+
import fs2 from "node:fs";
|
|
174
|
+
import { hostname } from "node:os";
|
|
175
|
+
import { setTimeout } from "node:timers/promises";
|
|
176
|
+
var STALE_AFTER_MS = 120000;
|
|
177
|
+
var POLL_MS = 50;
|
|
178
|
+
function snapshot(lockPath) {
|
|
179
|
+
try {
|
|
180
|
+
const { ino, mtimeMs } = fs2.statSync(lockPath);
|
|
181
|
+
return { ino, mtimeMs, owner: fs2.readFileSync(lockPath, "utf8").trim() };
|
|
182
|
+
} catch (exc) {
|
|
183
|
+
if (isErrno(exc, "ENOENT"))
|
|
184
|
+
return null;
|
|
185
|
+
throw exc;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function isStale({ mtimeMs, owner }) {
|
|
189
|
+
if (Date.now() - mtimeMs > STALE_AFTER_MS)
|
|
190
|
+
return true;
|
|
191
|
+
const match = /^(\d+)@(.*)$/u.exec(owner);
|
|
192
|
+
if (!match || match[2] !== hostname())
|
|
193
|
+
return false;
|
|
194
|
+
try {
|
|
195
|
+
process.kill(Number(match[1]), 0);
|
|
196
|
+
return false;
|
|
197
|
+
} catch (exc) {
|
|
198
|
+
return !isErrno(exc, "EPERM");
|
|
361
199
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
200
|
+
}
|
|
201
|
+
function breakIfStale(lockPath) {
|
|
202
|
+
const stale = snapshot(lockPath);
|
|
203
|
+
if (!stale || !isStale(stale))
|
|
204
|
+
return;
|
|
205
|
+
const guard = `${lockPath}.break`;
|
|
206
|
+
try {
|
|
207
|
+
fs2.writeFileSync(guard, "", { flag: "wx", mode: 384 });
|
|
208
|
+
} catch (exc) {
|
|
209
|
+
if (!isErrno(exc, "EEXIST"))
|
|
210
|
+
throw exc;
|
|
211
|
+
const held = snapshot(guard);
|
|
212
|
+
if (held && Date.now() - held.mtimeMs > STALE_AFTER_MS) {
|
|
213
|
+
throw new CeError(`stale lock guard ${JSON.stringify(guard)}: remove it if no ce is running`);
|
|
367
214
|
}
|
|
368
|
-
return
|
|
215
|
+
return;
|
|
369
216
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
217
|
+
try {
|
|
218
|
+
const current = snapshot(lockPath);
|
|
219
|
+
if (current && current.ino === stale.ino && current.mtimeMs === stale.mtimeMs && current.owner === stale.owner) {
|
|
220
|
+
fs2.rmSync(lockPath, { force: true });
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
fs2.rmSync(guard, { force: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function withFileLock(lockPath, fn) {
|
|
227
|
+
const owner = `${process.pid}@${hostname()}`;
|
|
228
|
+
let ino;
|
|
229
|
+
for (;; ) {
|
|
230
|
+
let fd;
|
|
375
231
|
try {
|
|
376
|
-
|
|
232
|
+
fd = fs2.openSync(lockPath, "wx", 384);
|
|
377
233
|
} catch (exc) {
|
|
378
|
-
|
|
234
|
+
if (!isErrno(exc, "EEXIST"))
|
|
235
|
+
throw exc;
|
|
236
|
+
breakIfStale(lockPath);
|
|
237
|
+
await setTimeout(POLL_MS);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
fs2.writeSync(fd, `${owner}
|
|
242
|
+
`);
|
|
243
|
+
ino = fs2.fstatSync(fd).ino;
|
|
244
|
+
} finally {
|
|
245
|
+
fs2.closeSync(fd);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
return await fn();
|
|
251
|
+
} finally {
|
|
252
|
+
const current = snapshot(lockPath);
|
|
253
|
+
if (current?.ino === ino && current.owner === owner)
|
|
254
|
+
fs2.rmSync(lockPath, { force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/state.ts
|
|
259
|
+
var HELPER_SUFFIX = "ce-forgejo";
|
|
260
|
+
var STATE_FILE = "ce-forgejo-credentials.json";
|
|
261
|
+
function dockerConfigDir() {
|
|
262
|
+
const configured = process.env.DOCKER_CONFIG;
|
|
263
|
+
if (!configured)
|
|
264
|
+
return path2.join(homedir(), ".docker");
|
|
265
|
+
const expanded = configured === "~" || configured.startsWith("~/") ? path2.join(homedir(), configured.slice(1)) : configured;
|
|
266
|
+
return path2.resolve(expanded);
|
|
267
|
+
}
|
|
268
|
+
function helperStatePath() {
|
|
269
|
+
return path2.join(dockerConfigDir(), STATE_FILE);
|
|
270
|
+
}
|
|
271
|
+
async function withHelperState(fn) {
|
|
272
|
+
const statePath = helperStatePath();
|
|
273
|
+
fs3.mkdirSync(path2.dirname(statePath), { recursive: true, mode: 448 });
|
|
274
|
+
return withFileLock(`${statePath}.pidlock`, async () => {
|
|
275
|
+
const state = readJsonFile(statePath, { version: 1, registries: {} });
|
|
276
|
+
if (!isObject(state) || !isObject(state.registries)) {
|
|
277
|
+
throw new CeError("Forgejo credential-helper state is invalid");
|
|
278
|
+
}
|
|
279
|
+
const before = JSON.stringify(state);
|
|
280
|
+
const result = await fn(state);
|
|
281
|
+
if (JSON.stringify(state) !== before)
|
|
282
|
+
writePrivateJson(statePath, state);
|
|
283
|
+
return result;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async function saveHelperState(registry, username, audience, authority, jwt) {
|
|
287
|
+
const expiry = jwtExpiry(jwt);
|
|
288
|
+
if (needsRefresh(expiry))
|
|
289
|
+
throw new CeError("OIDC token expires within the five-minute refresh window");
|
|
290
|
+
await withHelperState((state) => {
|
|
291
|
+
state.registries[registryKey(registry)] = {
|
|
292
|
+
audience,
|
|
293
|
+
authority: [authority[0], authority[1]],
|
|
294
|
+
expires_at: expiry,
|
|
295
|
+
token: jwt,
|
|
296
|
+
username
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async function configureDockerCredentialHelper(registry, username, audience, authority, jwt) {
|
|
301
|
+
await saveHelperState(registry, username, audience, authority, jwt);
|
|
302
|
+
const configPath = path2.join(dockerConfigDir(), "config.json");
|
|
303
|
+
const config = readJsonFile(configPath, {});
|
|
304
|
+
if (!isObject(config))
|
|
305
|
+
throw new CeError("Docker config is not a JSON object");
|
|
306
|
+
config.credHelpers ??= {};
|
|
307
|
+
const helpers = config.credHelpers;
|
|
308
|
+
if (!isObject(helpers))
|
|
309
|
+
throw new CeError("Docker config 'credHelpers' is not a JSON object");
|
|
310
|
+
helpers[registry] = HELPER_SUFFIX;
|
|
311
|
+
const auths = config.auths;
|
|
312
|
+
if (isObject(auths)) {
|
|
313
|
+
for (const key of Object.keys(auths)) {
|
|
314
|
+
if (registryKey(key) === registryKey(registry))
|
|
315
|
+
delete auths[key];
|
|
379
316
|
}
|
|
380
|
-
if (!isObject(payload))
|
|
381
|
-
throw new CeError(`ce: the event file ${JSON.stringify(file)} is missing or not a JSON object`);
|
|
382
|
-
this.#event = payload;
|
|
383
|
-
return payload;
|
|
384
317
|
}
|
|
318
|
+
writePrivateJson(configPath, config);
|
|
385
319
|
}
|
|
386
320
|
|
|
387
321
|
// src/system.ts
|
|
388
322
|
import { spawnSync } from "node:child_process";
|
|
389
|
-
import
|
|
390
|
-
import
|
|
391
|
-
import {
|
|
323
|
+
import fs4 from "node:fs";
|
|
324
|
+
import path3 from "node:path";
|
|
325
|
+
import { constants as osConstants } from "node:os";
|
|
326
|
+
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
392
327
|
var io = {
|
|
393
|
-
stdout: (text) => void
|
|
394
|
-
stderr: (text) => void
|
|
395
|
-
stdin: () =>
|
|
328
|
+
stdout: (text) => void fs4.writeSync(1, text),
|
|
329
|
+
stderr: (text) => void fs4.writeSync(2, text),
|
|
330
|
+
stdin: () => fs4.readFileSync(0, "utf8"),
|
|
396
331
|
argv0: () => process.argv[1] ?? "ce"
|
|
397
332
|
};
|
|
398
333
|
function log(line) {
|
|
@@ -403,15 +338,33 @@ function logError(line) {
|
|
|
403
338
|
io.stderr(`${line}
|
|
404
339
|
`);
|
|
405
340
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
341
|
+
function envFlag(name, unset = false, allowed = "true or false") {
|
|
342
|
+
const value = (process.env[name] ?? "").toLowerCase();
|
|
343
|
+
if (value === "")
|
|
344
|
+
return unset;
|
|
345
|
+
if (["0", "false", "no"].includes(value))
|
|
346
|
+
return false;
|
|
347
|
+
if (["1", "true", "yes"].includes(value))
|
|
348
|
+
return true;
|
|
349
|
+
throw new CeError(`ce: ${name} must be ${allowed}, not ${JSON.stringify(process.env[name])}`);
|
|
350
|
+
}
|
|
351
|
+
function envValue(name, flag) {
|
|
352
|
+
if (!/^[A-Za-z_][A-Za-z\d_]*$/u.test(name))
|
|
353
|
+
throw new CeError(`ce: ${flag} requires an environment variable name`);
|
|
354
|
+
return process.env[name] ?? "";
|
|
355
|
+
}
|
|
356
|
+
function shellQuote(arg) {
|
|
357
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
358
|
+
}
|
|
359
|
+
function childEnv(overrides = {}) {
|
|
360
|
+
return Object.fromEntries(Object.entries({ ...process.env, ...overrides }).filter((entry) => entry[1] !== undefined));
|
|
408
361
|
}
|
|
409
362
|
function which(command) {
|
|
410
|
-
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(
|
|
363
|
+
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean).map((dir) => path3.join(dir, command));
|
|
411
364
|
for (const candidate of candidates) {
|
|
412
365
|
try {
|
|
413
|
-
|
|
414
|
-
if (
|
|
366
|
+
fs4.accessSync(candidate, fs4.constants.X_OK);
|
|
367
|
+
if (fs4.statSync(candidate).isFile())
|
|
415
368
|
return candidate;
|
|
416
369
|
} catch {}
|
|
417
370
|
}
|
|
@@ -419,11 +372,11 @@ function which(command) {
|
|
|
419
372
|
}
|
|
420
373
|
var system = {
|
|
421
374
|
which,
|
|
422
|
-
run(argv, input) {
|
|
375
|
+
run(argv, input, env) {
|
|
423
376
|
const result = spawnSync(argv[0], argv.slice(1), {
|
|
424
377
|
input,
|
|
425
378
|
stdio: [input === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
426
|
-
env: childEnv(),
|
|
379
|
+
env: childEnv(env),
|
|
427
380
|
encoding: "utf8",
|
|
428
381
|
maxBuffer: Infinity
|
|
429
382
|
});
|
|
@@ -431,768 +384,2290 @@ var system = {
|
|
|
431
384
|
throw result.error;
|
|
432
385
|
return { status: result.status ?? 1, stderr: result.stderr ?? "" };
|
|
433
386
|
},
|
|
434
|
-
|
|
435
|
-
|
|
387
|
+
output(argv) {
|
|
388
|
+
const result = spawnSync(argv[0], argv.slice(1), {
|
|
389
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
390
|
+
env: childEnv(),
|
|
391
|
+
encoding: "utf8"
|
|
392
|
+
});
|
|
393
|
+
if (result.error)
|
|
394
|
+
throw result.error;
|
|
395
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? "" };
|
|
396
|
+
},
|
|
397
|
+
capture(argv, env) {
|
|
398
|
+
const result = spawnSync(argv[0], argv.slice(1), {
|
|
399
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
400
|
+
env: childEnv(env),
|
|
401
|
+
encoding: "utf8",
|
|
402
|
+
maxBuffer: Infinity
|
|
403
|
+
});
|
|
404
|
+
if (result.error)
|
|
405
|
+
throw result.error;
|
|
406
|
+
return {
|
|
407
|
+
status: result.status ?? (result.signal ? 128 + osConstants.signals[result.signal] : 1),
|
|
408
|
+
stdout: result.stdout ?? "",
|
|
409
|
+
stderr: result.stderr ?? ""
|
|
410
|
+
};
|
|
411
|
+
},
|
|
412
|
+
sleep: (ms) => setTimeout2(ms),
|
|
413
|
+
sleepSync(ms) {
|
|
414
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
415
|
+
},
|
|
416
|
+
exec(file, argv, env) {
|
|
436
417
|
const resolved = system.which(file);
|
|
437
418
|
if (!resolved) {
|
|
438
419
|
logError(`ce: ${file} not found on PATH`);
|
|
439
420
|
return 1;
|
|
440
421
|
}
|
|
441
|
-
process.execve(resolved, argv, childEnv());
|
|
422
|
+
process.execve(resolved, argv, childEnv(env));
|
|
442
423
|
throw new Error(`execve returned for ${file}`);
|
|
443
424
|
}
|
|
444
425
|
};
|
|
445
426
|
function binDir() {
|
|
446
|
-
return
|
|
427
|
+
return fs4.realpathSync(path3.join(import.meta.dirname, "..", "bin"));
|
|
447
428
|
}
|
|
448
429
|
function ensureHelperOnPath() {
|
|
449
430
|
const dir = binDir();
|
|
450
|
-
const parts = (process.env.PATH ?? "").split(
|
|
431
|
+
const parts = (process.env.PATH ?? "").split(path3.delimiter);
|
|
451
432
|
if (!parts.includes(dir))
|
|
452
|
-
process.env.PATH = [dir, ...parts].join(
|
|
433
|
+
process.env.PATH = [dir, ...parts].join(path3.delimiter);
|
|
453
434
|
}
|
|
454
435
|
|
|
455
|
-
// src/
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const text = (value) => {
|
|
463
|
-
if (hasPlaceholders(value))
|
|
464
|
-
throw new CeError(`ce: unmatched brace in ${JSON.stringify(template)}`);
|
|
465
|
-
if (value)
|
|
466
|
-
parts.push({ text: value });
|
|
467
|
-
};
|
|
468
|
-
let last = 0;
|
|
469
|
-
for (const match of template.matchAll(PLACEHOLDER)) {
|
|
470
|
-
text(template.slice(last, match.index));
|
|
471
|
-
last = match.index + match[0].length;
|
|
472
|
-
const guard = match[1].startsWith("?");
|
|
473
|
-
const name = guard ? match[1].slice(1) : match[1];
|
|
474
|
-
if (!vars.known(name))
|
|
475
|
-
throw new CeError(`ce: unknown placeholder ${match[0]} in ${JSON.stringify(template)}`);
|
|
476
|
-
parts.push({ name, guard });
|
|
477
|
-
}
|
|
478
|
-
text(template.slice(last));
|
|
479
|
-
return parts;
|
|
480
|
-
}
|
|
481
|
-
function expand(template, vars, encode) {
|
|
482
|
-
const parts = parse(template, vars);
|
|
483
|
-
for (const part of parts) {
|
|
484
|
-
if ("name" in part && part.guard && vars.get(part.name) === undefined)
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
let out = "";
|
|
488
|
-
for (const part of parts) {
|
|
489
|
-
if ("text" in part)
|
|
490
|
-
out += part.text;
|
|
491
|
-
else if (!part.guard) {
|
|
492
|
-
const value = vars.get(part.name);
|
|
493
|
-
if (value === undefined) {
|
|
494
|
-
throw new CeError(`ce: {${part.name}} has no value here, in ${JSON.stringify(template)}. ` + `To leave the tag out instead, guard it with {?${part.name}}.`);
|
|
495
|
-
}
|
|
496
|
-
out += encode(value);
|
|
436
|
+
// src/credential-helper.ts
|
|
437
|
+
async function helperGet(registry) {
|
|
438
|
+
const key = registryKey(registry);
|
|
439
|
+
return withHelperState(async (state) => {
|
|
440
|
+
const entry = state.registries[key];
|
|
441
|
+
if (!isObject(entry)) {
|
|
442
|
+
throw new CeError(`no Forgejo OIDC credentials configured for ${JSON.stringify(registry)}`);
|
|
497
443
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
444
|
+
const { username, token, expires_at: expiry } = entry;
|
|
445
|
+
if (typeof username !== "string" || !username) {
|
|
446
|
+
throw new CeError("Forgejo credential-helper username is missing");
|
|
447
|
+
}
|
|
448
|
+
if (typeof token !== "string" || typeof expiry !== "number") {
|
|
449
|
+
throw new CeError("Forgejo credential-helper token state is invalid");
|
|
450
|
+
}
|
|
451
|
+
if (!needsRefresh(expiry))
|
|
452
|
+
return { Username: username, Secret: token };
|
|
453
|
+
const { audience, authority } = entry;
|
|
454
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
455
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
456
|
+
if (typeof audience !== "string" || !audience) {
|
|
457
|
+
throw new CeError("Forgejo credential-helper audience is missing");
|
|
458
|
+
}
|
|
459
|
+
if (!(requestUrl && requestToken)) {
|
|
460
|
+
throw new CeError("OIDC refresh requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN");
|
|
461
|
+
}
|
|
462
|
+
const [host, port] = urlAuthority(requestUrl);
|
|
463
|
+
if (!Array.isArray(authority) || authority.length !== 2 || authority[0] !== host || authority[1] !== port) {
|
|
464
|
+
throw new CeError("OIDC token-request host does not match the registry");
|
|
465
|
+
}
|
|
466
|
+
const refreshed = await forgejoOidcJwt(audience, requestUrl, requestToken);
|
|
467
|
+
const refreshedExpiry = jwtExpiry(refreshed);
|
|
468
|
+
if (needsRefresh(refreshedExpiry)) {
|
|
469
|
+
throw new CeError("refreshed OIDC token expires within the five-minute window");
|
|
470
|
+
}
|
|
471
|
+
entry.token = refreshed;
|
|
472
|
+
entry.expires_at = refreshedExpiry;
|
|
473
|
+
return { Username: username, Secret: refreshed };
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async function helperStore(input) {
|
|
477
|
+
const payload = JSON.parse(input);
|
|
478
|
+
if (!isObject(payload))
|
|
479
|
+
throw new CeError("credential-helper store payload is invalid");
|
|
480
|
+
const { ServerURL: registry, Username: username, Secret: secret } = payload;
|
|
481
|
+
const filled = (value) => typeof value === "string" && value !== "";
|
|
482
|
+
if (!(filled(registry) && filled(username) && filled(secret))) {
|
|
483
|
+
throw new CeError("credential-helper store fields are incomplete");
|
|
484
|
+
}
|
|
485
|
+
const expiry = jwtExpiry(secret);
|
|
486
|
+
await withHelperState((state) => {
|
|
487
|
+
const entry = state.registries[registryKey(registry)] ??= {};
|
|
488
|
+
Object.assign(entry, { expires_at: expiry, token: secret, username });
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
async function credentialHelper(argv) {
|
|
492
|
+
const [command] = argv;
|
|
493
|
+
if (argv.length !== 1 || !["get", "store", "erase", "list"].includes(command)) {
|
|
494
|
+
logError("ce: credential-helper requires get|store|erase|list");
|
|
495
|
+
return 1;
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
if (command === "get") {
|
|
499
|
+
io.stdout(`${JSON.stringify(await helperGet(io.stdin().trim()))}
|
|
500
|
+
`);
|
|
501
|
+
} else if (command === "store") {
|
|
502
|
+
await helperStore(io.stdin());
|
|
503
|
+
} else if (command === "erase") {
|
|
504
|
+
const key = registryKey(io.stdin().trim());
|
|
505
|
+
await withHelperState((state) => void delete state.registries[key]);
|
|
506
|
+
} else {
|
|
507
|
+
const entries = await withHelperState((state) => Object.fromEntries(Object.entries(state.registries).filter(([, entry]) => isObject(entry)).map(([key, entry]) => [key, entry.username ?? ""])));
|
|
508
|
+
io.stdout(`${JSON.stringify(entries)}
|
|
509
|
+
`);
|
|
510
|
+
}
|
|
511
|
+
return 0;
|
|
512
|
+
} catch (exc) {
|
|
513
|
+
if (!(exc instanceof CeError || exc instanceof SyntaxError || isErrno(exc)))
|
|
514
|
+
throw exc;
|
|
515
|
+
logError(`ce: Forgejo credential helper: ${errorMessage(exc)}`);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
510
519
|
|
|
511
|
-
// src/
|
|
512
|
-
var
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
520
|
+
// src/command.ts
|
|
521
|
+
var VALUE_OPTIONS = {
|
|
522
|
+
docker: {
|
|
523
|
+
long: new Set([
|
|
524
|
+
"config",
|
|
525
|
+
"context",
|
|
526
|
+
"host",
|
|
527
|
+
"log-level",
|
|
528
|
+
"tlscacert",
|
|
529
|
+
"tlscert",
|
|
530
|
+
"tlskey",
|
|
531
|
+
"builder"
|
|
532
|
+
]),
|
|
533
|
+
short: "cHl"
|
|
534
|
+
},
|
|
535
|
+
podman: {
|
|
536
|
+
long: new Set([
|
|
537
|
+
"cdi-spec-dir",
|
|
538
|
+
"cgroup-manager",
|
|
539
|
+
"cni-config-dir",
|
|
540
|
+
"config",
|
|
541
|
+
"conmon",
|
|
542
|
+
"connection",
|
|
543
|
+
"context",
|
|
544
|
+
"cpu-profile",
|
|
545
|
+
"db-backend",
|
|
546
|
+
"default-mounts-file",
|
|
547
|
+
"events-backend",
|
|
548
|
+
"hooks-dir",
|
|
549
|
+
"host",
|
|
550
|
+
"identity",
|
|
551
|
+
"imagestore",
|
|
552
|
+
"log-level",
|
|
553
|
+
"max-workers",
|
|
554
|
+
"memory-profile",
|
|
555
|
+
"module",
|
|
556
|
+
"namespace",
|
|
557
|
+
"network-backend",
|
|
558
|
+
"network-cmd-path",
|
|
559
|
+
"network-config-dir",
|
|
560
|
+
"out",
|
|
561
|
+
"pull-option",
|
|
562
|
+
"registries-conf",
|
|
563
|
+
"root",
|
|
564
|
+
"runroot",
|
|
565
|
+
"runtime",
|
|
566
|
+
"runtime-flag",
|
|
567
|
+
"ssh",
|
|
568
|
+
"storage-driver",
|
|
569
|
+
"storage-opt",
|
|
570
|
+
"tmpdir",
|
|
571
|
+
"url",
|
|
572
|
+
"volumepath"
|
|
573
|
+
]),
|
|
574
|
+
short: "cH"
|
|
575
|
+
},
|
|
576
|
+
buildah: {
|
|
577
|
+
long: new Set([
|
|
578
|
+
"cgroup-manager",
|
|
579
|
+
"cpu-profile",
|
|
580
|
+
"default-mounts-file",
|
|
581
|
+
"experimental-timeout",
|
|
582
|
+
"imagestore",
|
|
583
|
+
"log-level",
|
|
584
|
+
"memory-profile",
|
|
585
|
+
"registries-conf",
|
|
586
|
+
"registries-conf-dir",
|
|
587
|
+
"root",
|
|
588
|
+
"runroot",
|
|
589
|
+
"short-name-alias-conf",
|
|
590
|
+
"storage-driver",
|
|
591
|
+
"storage-opt",
|
|
592
|
+
"userns-gid-map",
|
|
593
|
+
"userns-uid-map"
|
|
594
|
+
]),
|
|
595
|
+
short: ""
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function takesNext(engine, arg) {
|
|
599
|
+
const options = VALUE_OPTIONS[engine];
|
|
600
|
+
if (arg.startsWith("--"))
|
|
601
|
+
return options.long.has(arg.slice(2));
|
|
602
|
+
for (let j = 1;j < arg.length; j++) {
|
|
603
|
+
if (options.short.includes(arg[j]))
|
|
604
|
+
return j === arg.length - 1;
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
519
607
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
608
|
+
var COMMANDS = {
|
|
609
|
+
docker: [
|
|
610
|
+
{ words: ["build"] },
|
|
611
|
+
{ words: ["image", "build"] },
|
|
612
|
+
{ words: ["builder", "build"] },
|
|
613
|
+
{ words: ["buildx", "build"] },
|
|
614
|
+
{ words: ["buildx", "b"] },
|
|
615
|
+
{ words: ["buildx", "imagetools", "create"], tagsOnly: true }
|
|
616
|
+
],
|
|
617
|
+
podman: [
|
|
618
|
+
{ words: ["build"] },
|
|
619
|
+
{ words: ["image", "build"] },
|
|
620
|
+
{ words: ["buildx", "build"] }
|
|
621
|
+
],
|
|
622
|
+
buildah: [{ words: ["build"] }, { words: ["bud"] }]
|
|
623
|
+
};
|
|
624
|
+
function findBuildCommand(engine, argv) {
|
|
625
|
+
const words = [];
|
|
626
|
+
const options = [];
|
|
627
|
+
let global = 0;
|
|
628
|
+
for (let i = 0;i < argv.length; i++) {
|
|
629
|
+
const arg = argv[i];
|
|
630
|
+
if (arg === "--")
|
|
631
|
+
return null;
|
|
632
|
+
if (arg.startsWith("-")) {
|
|
633
|
+
options.push(arg);
|
|
634
|
+
if (takesNext(engine, arg) && i + 1 < argv.length)
|
|
635
|
+
options.push(argv[++i]);
|
|
636
|
+
continue;
|
|
526
637
|
}
|
|
638
|
+
if (!words.length)
|
|
639
|
+
global = options.length;
|
|
640
|
+
words.push(arg);
|
|
641
|
+
const candidates = COMMANDS[engine].filter((command) => words.every((word, j) => command.words[j] === word));
|
|
642
|
+
const match = candidates.find((command) => command.words.length === words.length);
|
|
643
|
+
if (match)
|
|
644
|
+
return {
|
|
645
|
+
start: i + 1,
|
|
646
|
+
name: arg,
|
|
647
|
+
tagsOnly: match.tagsOnly ?? false,
|
|
648
|
+
engineOptions: engine === "docker" ? options.slice(0, global) : options,
|
|
649
|
+
buildx: words[0] === "buildx"
|
|
650
|
+
};
|
|
651
|
+
if (!candidates.length)
|
|
652
|
+
return null;
|
|
527
653
|
}
|
|
654
|
+
return null;
|
|
528
655
|
}
|
|
529
|
-
|
|
530
|
-
|
|
656
|
+
|
|
657
|
+
// src/containerfile.ts
|
|
658
|
+
import fs5 from "node:fs";
|
|
659
|
+
import path4 from "node:path";
|
|
660
|
+
var engineEscapes = (value) => /[^\p{L}\p{M}\p{N}\p{P}\p{S} ]/u.test(value);
|
|
661
|
+
|
|
662
|
+
class Unreadable extends Error {
|
|
663
|
+
located = false;
|
|
664
|
+
hint = "";
|
|
531
665
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
666
|
+
var fail = (message) => {
|
|
667
|
+
throw new Unreadable(message);
|
|
668
|
+
};
|
|
669
|
+
var STANDARD_SYNTAX = /^(?:docker\.io\/)?docker\/dockerfile(?:-upstream)?(?::(?:1|latest|labs|master|1-labs|master-labs|1\.(?:2[1-9]|[3-9]\d|\d{3,})(?:\.\d+)?(?:-[\w.]+)?(?:@sha256:[0-9a-f]{64})?))?$/u;
|
|
670
|
+
var isRemote = (value) => value === "-" || value.includes("://") || /^(?:git@|github\.com\/)/u.test(value);
|
|
671
|
+
var familiar = (name) => name.replace(/^(?:(?:index\.)?docker\.io\/)?(?:library\/(?=[^/]+$))?/u, "").replace(/:latest$/u, "");
|
|
672
|
+
function findContainerfile(files, cwd = process.cwd()) {
|
|
673
|
+
try {
|
|
674
|
+
const file = locate(files, cwd);
|
|
675
|
+
if (files.engine !== "docker" && file.endsWith(".in"))
|
|
676
|
+
fail(`${files.engine} runs ${JSON.stringify(file)} through cpp, which ce does not`);
|
|
677
|
+
return file;
|
|
678
|
+
} catch (exc) {
|
|
679
|
+
if (exc instanceof Unreadable)
|
|
680
|
+
throw exc;
|
|
681
|
+
return fail(`ce cannot look for the Containerfile: ${errorMessage(exc)}`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function locate({ engine, files, positionals }, cwd) {
|
|
685
|
+
const docker = engine === "docker";
|
|
686
|
+
if (positionals.length > 1)
|
|
687
|
+
fail(`ce cannot tell which of ${positionals.map((p) => JSON.stringify(p)).join(", ")} is the build context`);
|
|
688
|
+
const context = positionals[0] ?? (docker ? fail("the command has no build context") : ".");
|
|
689
|
+
if (isRemote(context))
|
|
690
|
+
fail(`the build context ${JSON.stringify(context)} is not a local directory`);
|
|
691
|
+
const contextDir = path4.resolve(cwd, context);
|
|
692
|
+
if (!docker && (files.length > 1 || files.some((f) => f.includes(","))))
|
|
693
|
+
fail(`ce does not read ${engine}'s several -f files`);
|
|
694
|
+
const file = files.at(-1) || undefined;
|
|
695
|
+
if (file === undefined && files.length && !docker)
|
|
696
|
+
fail("the -f is empty");
|
|
697
|
+
if (docker) {
|
|
698
|
+
if (file !== undefined && isRemote(file))
|
|
699
|
+
fail(`-f ${JSON.stringify(file)} is not a local file`);
|
|
700
|
+
return lowercaseDockerfile(file === undefined ? path4.join(contextDir, "Dockerfile") : path4.resolve(cwd, file));
|
|
701
|
+
}
|
|
702
|
+
if (file !== undefined) {
|
|
703
|
+
if (isRemote(file))
|
|
704
|
+
fail(`-f ${JSON.stringify(file)} is not a local file`);
|
|
705
|
+
const given = path4.resolve(cwd, file);
|
|
706
|
+
return fs5.existsSync(given) ? given : path4.resolve(contextDir, file);
|
|
707
|
+
}
|
|
708
|
+
if (fs5.statSync(contextDir, { throwIfNoEntry: false })?.isFile())
|
|
709
|
+
return contextDir;
|
|
710
|
+
const containerfile = path4.join(contextDir, "Containerfile");
|
|
711
|
+
return fs5.existsSync(containerfile) ? containerfile : path4.join(contextDir, "Dockerfile");
|
|
712
|
+
}
|
|
713
|
+
function lowercaseDockerfile(file) {
|
|
714
|
+
if (path4.basename(file) !== "Dockerfile")
|
|
715
|
+
return file;
|
|
716
|
+
let names;
|
|
717
|
+
try {
|
|
718
|
+
names = fs5.readdirSync(path4.dirname(file));
|
|
719
|
+
} catch {
|
|
720
|
+
return file;
|
|
721
|
+
}
|
|
722
|
+
return !names.includes("Dockerfile") && names.includes("dockerfile") ? path4.join(path4.dirname(file), "dockerfile") : file;
|
|
723
|
+
}
|
|
724
|
+
var SPACE = "\\t\\n\\v\\f\\r \\u0085\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000";
|
|
725
|
+
var IS_SPACE = new RegExp(`[${SPACE}]`, "u");
|
|
726
|
+
var TRIM_START = new RegExp(`^[${SPACE}]+`, "u");
|
|
727
|
+
var UNUSUAL_SPACE = /[\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/u;
|
|
728
|
+
var NON_ASCII = /[^\0-\x7f]/u;
|
|
729
|
+
function instructionOf(word, line) {
|
|
730
|
+
if (NON_ASCII.test(word))
|
|
731
|
+
fail(`ce cannot read the instruction ${JSON.stringify(word)} on line ${line}`);
|
|
732
|
+
return word.toUpperCase();
|
|
733
|
+
}
|
|
734
|
+
var stageKey = (name) => NON_ASCII.test(name) ? fail(`ce reads only ASCII stage names, not ${JSON.stringify(name)}`) : name.toLowerCase();
|
|
735
|
+
function trimEnd(text, space) {
|
|
736
|
+
let end = text.length;
|
|
737
|
+
while (end > 0 && space.test(text[end - 1]))
|
|
738
|
+
end--;
|
|
739
|
+
return text.slice(0, end);
|
|
740
|
+
}
|
|
741
|
+
var trim = (text) => trimEnd(text.replace(TRIM_START, ""), IS_SPACE);
|
|
742
|
+
var COMMAND = /^([^\t\v\f\r ]+)[\t\v\f\r ]*([\s\S]*)$/u;
|
|
743
|
+
function words(text, escape, heredoc = false) {
|
|
744
|
+
const out = [];
|
|
745
|
+
let word = "";
|
|
746
|
+
let quote = "";
|
|
747
|
+
let inWord = false;
|
|
748
|
+
for (let i = 0;i < text.length; i++) {
|
|
749
|
+
const ch = text[i];
|
|
750
|
+
if (!quote && IS_SPACE.test(ch)) {
|
|
751
|
+
if (inWord)
|
|
752
|
+
out.push(word);
|
|
753
|
+
[word, inWord] = ["", false];
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
inWord = true;
|
|
757
|
+
if (ch === escape && quote !== "'" && i + 1 === text.length)
|
|
758
|
+
continue;
|
|
759
|
+
word += ch;
|
|
760
|
+
if (heredoc && !quote && ch === "<" && text[i + 1] === "<") {
|
|
761
|
+
word += text[++i];
|
|
762
|
+
while (/[ \t\r]/u.test(text[i + 1] ?? ""))
|
|
763
|
+
word += text[++i];
|
|
764
|
+
} else if (ch === escape && quote !== "'" && i + 1 < text.length)
|
|
765
|
+
word += text[++i];
|
|
766
|
+
else if (ch === quote)
|
|
767
|
+
quote = "";
|
|
768
|
+
else if (!quote && (ch === '"' || ch === "'"))
|
|
769
|
+
quote = ch;
|
|
770
|
+
}
|
|
771
|
+
if (quote)
|
|
772
|
+
fail(`unterminated ${quote} in ${JSON.stringify(text)}`);
|
|
773
|
+
if (inWord)
|
|
774
|
+
out.push(word);
|
|
775
|
+
return out;
|
|
535
776
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
777
|
+
var HEREDOC_INSTRUCTIONS = ["RUN", "COPY", "ADD"];
|
|
778
|
+
function heredocs(keyword, rest, line, buildkit) {
|
|
779
|
+
while (keyword === "ONBUILD") {
|
|
780
|
+
const [, inner = "", innerRest = ""] = COMMAND.exec(trim(rest)) ?? [];
|
|
781
|
+
[keyword, rest] = [instructionOf(inner, line), innerRest];
|
|
782
|
+
}
|
|
783
|
+
if (!HEREDOC_INSTRUCTIONS.includes(keyword) || !rest.includes("<<"))
|
|
784
|
+
return [];
|
|
785
|
+
const body = rest.replace(/^(?:--\S+\s+)*/u, "").replace(TRIM_START, "");
|
|
786
|
+
if (body.startsWith("[")) {
|
|
787
|
+
const json = buildkit ? body : jsonPrefix(body);
|
|
788
|
+
try {
|
|
789
|
+
const array = json === undefined ? undefined : JSON.parse(json);
|
|
790
|
+
if (Array.isArray(array) && array.every((v) => typeof v === "string"))
|
|
791
|
+
return [];
|
|
792
|
+
} catch {}
|
|
793
|
+
}
|
|
794
|
+
const unsure = () => fail(`ce cannot tell which heredocs the << on line ${line} starts`);
|
|
795
|
+
if (UNUSUAL_SPACE.test(rest) || !plainBraces(rest))
|
|
796
|
+
unsure();
|
|
797
|
+
const found = [];
|
|
798
|
+
let split;
|
|
799
|
+
try {
|
|
800
|
+
split = words(rest, "\\", true);
|
|
801
|
+
} catch {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
for (const word of split) {
|
|
805
|
+
const match = /^\d*<<(-?)[\t\n\f\r ]*([^<]*)$/u.exec(word);
|
|
806
|
+
if (!match?.[2])
|
|
807
|
+
continue;
|
|
808
|
+
const delimiter = /^(["']?)([^\s"'\\`$]+)\1$/u.exec(match[2]);
|
|
809
|
+
found.push({ name: delimiter?.[2] ?? unsure(), tabs: match[1] === "-" });
|
|
810
|
+
}
|
|
811
|
+
return found;
|
|
812
|
+
}
|
|
813
|
+
var PLAIN_BRACES = /\$\{[A-Za-z_][A-Za-z0-9_]*(?:(?::[-+?]|[-+?#%])[^\s"'\\`<${}]*)?\}/uy;
|
|
814
|
+
var QUOTED_BRACES = /\$\{[A-Za-z_][A-Za-z0-9_]*(?:(?::[-+?]|[-+?#%])[^\n"'\\`<${}]*)?\}/uy;
|
|
815
|
+
function plainBraces(text) {
|
|
816
|
+
let quote = "";
|
|
817
|
+
for (let i = 0;i < text.length; i++) {
|
|
818
|
+
const ch = text[i];
|
|
819
|
+
if (ch === "\\" && quote !== "'")
|
|
820
|
+
i++;
|
|
821
|
+
else if (quote && ch === quote)
|
|
822
|
+
quote = "";
|
|
823
|
+
else if (!quote && (ch === '"' || ch === "'"))
|
|
824
|
+
quote = ch;
|
|
825
|
+
else if (quote !== "'" && text.startsWith("${", i)) {
|
|
826
|
+
const braces = quote ? QUOTED_BRACES : PLAIN_BRACES;
|
|
827
|
+
braces.lastIndex = i;
|
|
828
|
+
const match = braces.exec(text);
|
|
829
|
+
if (!match)
|
|
830
|
+
return false;
|
|
831
|
+
i += match[0].length - 1;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
function jsonPrefix(text) {
|
|
837
|
+
let depth = 0;
|
|
838
|
+
for (let i = 0;i < text.length; i++) {
|
|
839
|
+
const ch = text[i];
|
|
840
|
+
if (ch === '"') {
|
|
841
|
+
for (i++;i < text.length && text[i] !== '"'; i++)
|
|
842
|
+
if (text[i] === "\\")
|
|
843
|
+
i++;
|
|
844
|
+
} else if (ch === "[" || ch === "{")
|
|
845
|
+
depth++;
|
|
846
|
+
else if ((ch === "]" || ch === "}") && --depth === 0)
|
|
847
|
+
return text.slice(0, i + 1);
|
|
848
|
+
}
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
var DIRECTIVE_SPACE = /[\t\n\f\r ]/u;
|
|
852
|
+
function directive(text) {
|
|
853
|
+
const match = /^([a-zA-Z][a-zA-Z0-9]*)[\t\n\f\r ]*=[\t\n\f\r ]*([^\n]+)$/u.exec(trimEnd(text, DIRECTIVE_SPACE));
|
|
854
|
+
return match ? { key: match[1].toLowerCase(), value: match[2] } : undefined;
|
|
855
|
+
}
|
|
856
|
+
var DIRECTIVES = ["syntax", "escape", "check"];
|
|
857
|
+
function detectSyntax(lines) {
|
|
858
|
+
if (lines[0]?.startsWith("#!"))
|
|
859
|
+
lines = lines.slice(1);
|
|
860
|
+
for (const prefix of ["#", "//"]) {
|
|
861
|
+
const seen = new Set;
|
|
862
|
+
for (const line of lines) {
|
|
863
|
+
if (!line.startsWith(prefix))
|
|
864
|
+
break;
|
|
865
|
+
const match = directive(line.slice(prefix.length).replace(TRIM_START, ""));
|
|
866
|
+
if (!match || !DIRECTIVES.includes(match.key) || seen.has(match.key))
|
|
867
|
+
break;
|
|
868
|
+
seen.add(match.key);
|
|
869
|
+
if (match.key === "syntax")
|
|
870
|
+
return match.value.split(" ")[0];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
542
874
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
875
|
+
function pair(word) {
|
|
876
|
+
const eq = word.indexOf("=");
|
|
877
|
+
return eq < 0 ? { name: word, value: undefined } : { name: word.slice(0, eq), value: word.slice(eq + 1) };
|
|
878
|
+
}
|
|
879
|
+
function parseContainerfile(text, model = "buildkit") {
|
|
880
|
+
const buildkit = model === "buildkit";
|
|
881
|
+
const lines = text.replace(/^\ufeff/u, "").split(`
|
|
882
|
+
`).map((line, at) => line.endsWith("\r\r") ? fail(`ce reads only LF and CRLF line ends, not line ${at + 1}'s`) : line.replace(/\r$/u, ""));
|
|
883
|
+
let escape = "\\";
|
|
884
|
+
let i = 0;
|
|
885
|
+
for (let seen = false;!buildkit && i < lines.length; i++) {
|
|
886
|
+
const directive = /^#[ \t]*escape[ \t]*=[ \t]*([^\n])/u.exec(lines[i].replace(TRIM_START, "").toLowerCase());
|
|
887
|
+
if (!directive)
|
|
888
|
+
break;
|
|
889
|
+
if (seen)
|
|
890
|
+
fail("there is more than one escape directive");
|
|
891
|
+
seen = true;
|
|
892
|
+
escape = directive[1];
|
|
893
|
+
if (escape !== "\\" && escape !== "`")
|
|
894
|
+
fail(`the escape directive ${JSON.stringify(escape)} is not \\ or \``);
|
|
895
|
+
}
|
|
896
|
+
for (const seen = new Set;buildkit && i < lines.length; i++) {
|
|
897
|
+
const line = lines[i].replace(TRIM_START, "");
|
|
898
|
+
const found = line.startsWith("#") ? directive(line.slice(1).replace(TRIM_START, "")) : undefined;
|
|
899
|
+
if (!found || !DIRECTIVES.includes(found.key) || seen.has(found.key))
|
|
900
|
+
break;
|
|
901
|
+
seen.add(found.key);
|
|
902
|
+
if (found.key === "escape") {
|
|
903
|
+
const { value } = found;
|
|
904
|
+
if (value !== "\\" && value !== "`")
|
|
905
|
+
fail(`the escape directive ${JSON.stringify(value)} is not \\ or \``);
|
|
906
|
+
escape = value;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const e = escape === "\\" ? "\\\\" : escape;
|
|
910
|
+
const continued = buildkit ? new RegExp(`([^${e}])${e}[ \\t]*$|^${e}[ \\t]*$`, "u") : new RegExp(`()${e}[ \\t]*$`, "u");
|
|
911
|
+
const parsed = {
|
|
912
|
+
escape,
|
|
913
|
+
syntax: detectSyntax(lines),
|
|
914
|
+
globals: [],
|
|
915
|
+
stages: []
|
|
916
|
+
};
|
|
917
|
+
while (i < lines.length) {
|
|
918
|
+
let start = 0;
|
|
919
|
+
let line = "";
|
|
920
|
+
for (;i < lines.length; i++) {
|
|
921
|
+
const physical = lines[i];
|
|
922
|
+
if (/^(?:#|$)/u.test(trim(physical)))
|
|
923
|
+
continue;
|
|
924
|
+
start ||= i + 1;
|
|
925
|
+
if (continued.test(physical)) {
|
|
926
|
+
line += physical.replace(continued, "$1");
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
line += physical;
|
|
930
|
+
i++;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
line = trim(line);
|
|
934
|
+
if (!line)
|
|
935
|
+
continue;
|
|
936
|
+
const [instruction = "", rest = ""] = COMMAND.exec(line).slice(1);
|
|
937
|
+
const keyword = instructionOf(instruction, start);
|
|
938
|
+
for (const { name, tabs } of heredocs(keyword, rest, start, buildkit)) {
|
|
939
|
+
for (;; i++) {
|
|
940
|
+
if (i >= lines.length)
|
|
941
|
+
fail(`the heredoc ${name} from line ${start} never ends`);
|
|
942
|
+
const end = lines[i];
|
|
943
|
+
if ((tabs ? end.replace(/^\t+/u, "") : end) === name) {
|
|
944
|
+
i++;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const [, onbuild = "", onbuildRest = ""] = keyword === "ONBUILD" ? COMMAND.exec(trim(rest)) ?? [] : [];
|
|
950
|
+
const kind = onbuild ? instructionOf(onbuild, start) : keyword;
|
|
951
|
+
const args = onbuild ? onbuildRest : rest;
|
|
952
|
+
if (keyword !== "FROM" && kind !== "ARG" && kind !== "ENV")
|
|
953
|
+
continue;
|
|
954
|
+
if (UNUSUAL_SPACE.test(line))
|
|
955
|
+
fail(`ce reads only ASCII space in the ${kind} on line ${start}`);
|
|
956
|
+
const split = onbuild && buildkit ? "\\" : escape;
|
|
957
|
+
const stage = parsed.stages.at(-1);
|
|
958
|
+
if (keyword === "FROM") {
|
|
959
|
+
const all = words(rest, buildkit ? escape : "\\");
|
|
960
|
+
let flags = 0;
|
|
961
|
+
while (all[flags]?.startsWith("--"))
|
|
962
|
+
if (all[flags++] === "--")
|
|
963
|
+
break;
|
|
964
|
+
const parts = all.slice(flags);
|
|
965
|
+
if (!(parts.length === 1 || parts.length === 3 && /^[Aa][Ss]$/u.test(parts[1])) || parts.some((part) => IS_SPACE.test(part)))
|
|
966
|
+
fail(`ce cannot read the FROM on line ${start}`);
|
|
967
|
+
parsed.stages.push({
|
|
968
|
+
name: parts[2] ?? "",
|
|
969
|
+
line: start,
|
|
970
|
+
base: parts[0],
|
|
971
|
+
instructions: []
|
|
972
|
+
});
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const set = words(args, split);
|
|
976
|
+
if (set[0]?.startsWith("--"))
|
|
977
|
+
fail(`ce does not read the flags of the ${kind} on line ${start}`);
|
|
978
|
+
const from = {
|
|
979
|
+
line: start,
|
|
980
|
+
...onbuild !== "" && { onbuild: true }
|
|
981
|
+
};
|
|
982
|
+
if (kind === "ARG" && (stage || !onbuild)) {
|
|
983
|
+
if (!set.length)
|
|
984
|
+
fail(`the ARG on line ${start} names nothing`);
|
|
985
|
+
(stage?.instructions ?? parsed.globals).push({
|
|
986
|
+
kind: "ARG",
|
|
987
|
+
pairs: set.map(pair),
|
|
988
|
+
...from
|
|
989
|
+
});
|
|
990
|
+
} else if (kind === "ENV" && stage) {
|
|
991
|
+
const legacy = set.length > 0 && !set[0].includes("=");
|
|
992
|
+
stage.instructions.push({
|
|
993
|
+
kind: "ENV",
|
|
994
|
+
pairs: legacy ? [{ name: set[0], value: undefined }] : set.map(pair),
|
|
995
|
+
...from
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return parsed;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
class Unknown {
|
|
1003
|
+
why;
|
|
1004
|
+
reason;
|
|
1005
|
+
constructor(why, reason = "") {
|
|
1006
|
+
this.why = why;
|
|
1007
|
+
this.reason = reason;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
var BASE_ENV = new Unknown("base");
|
|
1011
|
+
var SCRATCH_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
1012
|
+
var BUILTINS = [
|
|
1013
|
+
"BUILDPLATFORM",
|
|
1014
|
+
"BUILDOS",
|
|
1015
|
+
"BUILDOSVERSION",
|
|
1016
|
+
"BUILDARCH",
|
|
1017
|
+
"BUILDVARIANT",
|
|
1018
|
+
"TARGETPLATFORM",
|
|
1019
|
+
"TARGETOS",
|
|
1020
|
+
"TARGETOSVERSION",
|
|
1021
|
+
"TARGETARCH",
|
|
1022
|
+
"TARGETVARIANT",
|
|
1023
|
+
"TARGETSTAGE"
|
|
1024
|
+
];
|
|
1025
|
+
var INJECTED = /^(?:SOURCE_DATE_EPOCH|(?:HTTPS?|FTP|NO|ALL)_PROXY)$/iu;
|
|
1026
|
+
var INJECTED_IB = /^SOURCE_DATE_EPOCH$/iu;
|
|
1027
|
+
var PROXIES = /^(?:(?:HTTPS?|FTP|NO)_PROXY|(?:https?|ftp|no)_proxy)$/u;
|
|
1028
|
+
var NON_ENV = ["BUILDKIT_SBOM_SCAN_CONTEXT", "BUILDKIT_SBOM_SCAN_STAGE"];
|
|
1029
|
+
function expandWord(word, escape, model, lookup) {
|
|
1030
|
+
const buildkit = model === "buildkit";
|
|
1031
|
+
let i = 0;
|
|
1032
|
+
let depth = 0;
|
|
1033
|
+
let unused = 0;
|
|
1034
|
+
const value = (name) => {
|
|
1035
|
+
if (unused)
|
|
1036
|
+
return;
|
|
1037
|
+
const v = lookup(name);
|
|
1038
|
+
if (v instanceof Unknown) {
|
|
1039
|
+
if (v.why !== "error") {
|
|
1040
|
+
const exc = new Unreadable(`ce cannot tell what $${name} is there`);
|
|
1041
|
+
exc.hint = v.reason;
|
|
1042
|
+
throw exc;
|
|
1043
|
+
}
|
|
1044
|
+
const exc = new Unreadable(v.reason);
|
|
1045
|
+
exc.located = true;
|
|
1046
|
+
throw exc;
|
|
1047
|
+
}
|
|
1048
|
+
return v;
|
|
1049
|
+
};
|
|
1050
|
+
const readName = () => {
|
|
1051
|
+
const rest = word.slice(i);
|
|
1052
|
+
const name = (buildkit ? /^(?:[0-9]+|[@*#?\-$!]|[A-Za-z_][A-Za-z0-9_]*)/u.exec(rest)?.[0] : /^(?:[0-9]|[A-Za-z0-9_]+)/u.exec(rest)?.[0]) ?? "";
|
|
1053
|
+
const after = rest.slice(name.length);
|
|
1054
|
+
if (NON_ASCII.test(after[0] ?? "") && /^[\p{L}\p{Nd}]/u.test(after))
|
|
1055
|
+
fail(`ce reads only ASCII names after a $, not in ${JSON.stringify(word)}`);
|
|
1056
|
+
i += name.length;
|
|
1057
|
+
return name;
|
|
1058
|
+
};
|
|
1059
|
+
const unsupported = () => fail(`ce does not evaluate the \${} in ${JSON.stringify(word)}`);
|
|
1060
|
+
const dollar = () => {
|
|
1061
|
+
i++;
|
|
1062
|
+
if (word[i] !== "{") {
|
|
1063
|
+
const name = readName();
|
|
1064
|
+
return name ? value(name) ?? "" : "$";
|
|
1065
|
+
}
|
|
1066
|
+
i++;
|
|
1067
|
+
const name = readName();
|
|
1068
|
+
if (!name)
|
|
1069
|
+
unsupported();
|
|
1070
|
+
if (word[i] === "}") {
|
|
1071
|
+
i++;
|
|
1072
|
+
return value(name) ?? "";
|
|
1073
|
+
}
|
|
1074
|
+
const op = (buildkit ? /^:?[-+]/u : /^:[-+]/u).exec(word.slice(i))?.[0];
|
|
1075
|
+
if (!op)
|
|
1076
|
+
unsupported();
|
|
1077
|
+
i += op.length;
|
|
1078
|
+
const v = value(name);
|
|
1079
|
+
const set = op.startsWith(":") ? v !== undefined && v !== "" : v !== undefined;
|
|
1080
|
+
const used = op.endsWith("-") ? !set : set;
|
|
1081
|
+
if (++depth > 100)
|
|
1082
|
+
fail("ce does not evaluate ${} nested this deeply");
|
|
1083
|
+
if (!used)
|
|
1084
|
+
unused++;
|
|
1085
|
+
const alternative = until("}");
|
|
1086
|
+
if (!used)
|
|
1087
|
+
unused--;
|
|
1088
|
+
depth--;
|
|
1089
|
+
return used ? alternative : set ? v : "";
|
|
1090
|
+
};
|
|
1091
|
+
const until = (end) => {
|
|
1092
|
+
let out = "";
|
|
1093
|
+
while (i < word.length) {
|
|
1094
|
+
const ch = word[i];
|
|
1095
|
+
if (end && ch === end) {
|
|
1096
|
+
i++;
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
if (ch === escape) {
|
|
1100
|
+
out += word[i + 1] ?? "";
|
|
1101
|
+
i += 2;
|
|
1102
|
+
} else if (ch === "'") {
|
|
1103
|
+
const close = word.indexOf("'", i + 1);
|
|
1104
|
+
if (close < 0)
|
|
1105
|
+
fail(`unterminated ' in ${JSON.stringify(word)}`);
|
|
1106
|
+
out += word.slice(i + 1, close);
|
|
1107
|
+
i = close + 1;
|
|
1108
|
+
} else if (ch === '"') {
|
|
1109
|
+
i++;
|
|
1110
|
+
for (;; ) {
|
|
1111
|
+
if (i >= word.length)
|
|
1112
|
+
fail(`unterminated " in ${JSON.stringify(word)}`);
|
|
1113
|
+
const c = word[i];
|
|
1114
|
+
if (c === '"') {
|
|
1115
|
+
i++;
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
if (c === escape && ['"', "$", escape].includes(word[i + 1] ?? "")) {
|
|
1119
|
+
out += word[i + 1];
|
|
1120
|
+
i += 2;
|
|
1121
|
+
} else if (c === "$")
|
|
1122
|
+
out += dollar();
|
|
1123
|
+
else {
|
|
1124
|
+
out += c;
|
|
1125
|
+
i++;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
} else if (ch === "$")
|
|
1129
|
+
out += dollar();
|
|
1130
|
+
else {
|
|
1131
|
+
out += ch;
|
|
1132
|
+
i++;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (end)
|
|
1136
|
+
fail(`missing ${end} in ${JSON.stringify(word)}`);
|
|
1137
|
+
return out;
|
|
1138
|
+
};
|
|
1139
|
+
return until("");
|
|
1140
|
+
}
|
|
1141
|
+
var VARIANTS = new Map(Object.entries({
|
|
1142
|
+
amd64: { "": "", v1: "", v2: "v2", v3: "v3", v4: "v4" },
|
|
1143
|
+
arm64: { "": "", "8": "v8", v8: "v8", "v8.0": "v8" },
|
|
1144
|
+
arm: {
|
|
1145
|
+
"": "v7",
|
|
1146
|
+
"5": "v5",
|
|
1147
|
+
"6": "v6",
|
|
1148
|
+
"7": "v7",
|
|
1149
|
+
"8": "v8",
|
|
1150
|
+
v5: "v5",
|
|
1151
|
+
v6: "v6",
|
|
1152
|
+
v7: "v7",
|
|
1153
|
+
v8: "v8"
|
|
1154
|
+
},
|
|
1155
|
+
"386": { "": "" },
|
|
1156
|
+
ppc64le: { "": "" },
|
|
1157
|
+
riscv64: { "": "" },
|
|
1158
|
+
s390x: { "": "" }
|
|
1159
|
+
}).map(([arch, variants]) => [arch, new Map(Object.entries(variants))]));
|
|
1160
|
+
var ARCH_NAMES = new Map([
|
|
1161
|
+
["aarch64", ["arm64"]],
|
|
1162
|
+
["x86_64", ["amd64"]],
|
|
1163
|
+
["x86-64", ["amd64"]],
|
|
1164
|
+
["i386", ["386", ""]],
|
|
1165
|
+
["armhf", ["arm", "v7"]],
|
|
1166
|
+
["armel", ["arm", "v6"]]
|
|
1167
|
+
]);
|
|
1168
|
+
function platformArgs(platform, model) {
|
|
1169
|
+
const parts = platform.toLowerCase().split("/");
|
|
1170
|
+
const isArch = (name) => VARIANTS.has(name) || ARCH_NAMES.has(name);
|
|
1171
|
+
if (parts.length === 1 && isArch(parts[0]) && process.platform === "linux")
|
|
1172
|
+
parts.unshift("linux");
|
|
1173
|
+
const [os, written = "", given = "", ...rest] = parts;
|
|
1174
|
+
if (os !== "linux" || rest.length)
|
|
1175
|
+
return;
|
|
1176
|
+
const [arch = written, fixed] = ARCH_NAMES.get(written) ?? [];
|
|
1177
|
+
let variant = fixed === undefined ? VARIANTS.get(arch)?.get(given) : given ? undefined : fixed;
|
|
1178
|
+
if (variant === undefined)
|
|
1179
|
+
return;
|
|
1180
|
+
if (arch === "arm64" && model === "buildkit")
|
|
1181
|
+
variant = "";
|
|
1182
|
+
return new Map([
|
|
1183
|
+
["TARGETPLATFORM", [os, arch, variant].filter(Boolean).join("/")],
|
|
1184
|
+
["TARGETOS", os],
|
|
1185
|
+
["TARGETARCH", arch],
|
|
1186
|
+
["TARGETVARIANT", variant],
|
|
1187
|
+
["TARGETOSVERSION", ""]
|
|
1188
|
+
]);
|
|
1189
|
+
}
|
|
1190
|
+
function normalizePlatform(platform, engine) {
|
|
1191
|
+
return platformArgs(platform, engine === "docker" ? "buildkit" : "imagebuilder")?.get("TARGETPLATFORM");
|
|
1192
|
+
}
|
|
1193
|
+
function argDefault(parsed, name, target, buildArg, model = "buildkit", {
|
|
1194
|
+
contexts = [],
|
|
1195
|
+
envs = [],
|
|
1196
|
+
env: hostEnv = process.env,
|
|
1197
|
+
platform,
|
|
1198
|
+
platformIgnored,
|
|
1199
|
+
format,
|
|
1200
|
+
from: fromImage
|
|
1201
|
+
} = {}) {
|
|
1202
|
+
const buildkit = model === "buildkit";
|
|
1203
|
+
const escape = buildkit ? parsed.escape : "\\";
|
|
1204
|
+
const evaluate = (word, line, lookup) => {
|
|
1205
|
+
try {
|
|
1206
|
+
return expandWord(word, escape, model, lookup);
|
|
1207
|
+
} catch (exc) {
|
|
1208
|
+
if (!(exc instanceof Unreadable))
|
|
1209
|
+
throw exc;
|
|
1210
|
+
return new Unknown("error", exc.located ? exc.message : `${exc.message}, on line ${line}${exc.hint && `: ${exc.hint}`}`);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
const isBuiltin = (arg) => BUILTINS.includes(arg);
|
|
1214
|
+
let targets;
|
|
1215
|
+
const builtin = (arg, global) => {
|
|
1216
|
+
if (arg.startsWith("BUILD"))
|
|
1217
|
+
return new Unknown("builtin", "it is the platform of the machine that builds");
|
|
1218
|
+
if (arg === "TARGETSTAGE")
|
|
1219
|
+
return new Unknown("builtin");
|
|
1220
|
+
if (global && !buildkit)
|
|
1221
|
+
return new Unknown("builtin", "podman and buildah do not set it from --platform before the first FROM");
|
|
1222
|
+
targets ??= platformIgnored?.() ?? (platform === undefined ? "it comes from --platform, which the build does not have" : platformArgs(platform, model) ?? `ce does not read the --platform ${JSON.stringify(platform)}`);
|
|
1223
|
+
return typeof targets === "string" ? new Unknown("builtin", targets) : targets.get(arg);
|
|
1224
|
+
};
|
|
1225
|
+
const injected = (arg) => (buildkit ? INJECTED : INJECTED_IB).test(arg);
|
|
1226
|
+
const plain = (name) => ![...`$'"`, escape].some((ch) => name.includes(ch));
|
|
1227
|
+
const named = (kind, name, line) => plain(name) ? name : fail(`ce cannot tell what name the ${kind} on line ${line} sets`);
|
|
1228
|
+
const globals = new Map;
|
|
1229
|
+
for (const arg of BUILTINS)
|
|
1230
|
+
globals.set(arg, buildArg(arg) ?? builtin(arg, true));
|
|
1231
|
+
for (const { pairs, line } of parsed.globals) {
|
|
1232
|
+
const before = new Map(globals);
|
|
1233
|
+
const seen = (ref) => {
|
|
1234
|
+
const known = buildkit ? globals : before;
|
|
1235
|
+
if (known.has(ref) || buildkit || !PROXIES.test(ref))
|
|
1236
|
+
return known.get(ref);
|
|
1237
|
+
return buildArg(ref);
|
|
1238
|
+
};
|
|
1239
|
+
for (const { name, value } of pairs) {
|
|
1240
|
+
const arg = named("ARG", name, line);
|
|
1241
|
+
const given = buildArg(arg);
|
|
1242
|
+
if (given !== undefined)
|
|
1243
|
+
globals.set(arg, given);
|
|
1244
|
+
else if (injected(arg))
|
|
1245
|
+
globals.set(arg, new Unknown("injected"));
|
|
1246
|
+
else if (value !== undefined && !(isBuiltin(arg) && !buildkit))
|
|
1247
|
+
globals.set(arg, evaluate(value, line, seen));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const findStage = (wanted, before, target = false) => {
|
|
1251
|
+
const at = parsed.stages.findLastIndex((s, i) => i < before && (buildkit ? stageKey(s.name) === stageKey(wanted) : (s.name || String(i)) === wanted || !target && String(i) === wanted));
|
|
1252
|
+
if (at >= 0 || buildkit || !target || !/^\d+$/u.test(wanted))
|
|
1253
|
+
return at;
|
|
1254
|
+
const position = Number(wanted);
|
|
1255
|
+
return position < before ? position : -1;
|
|
1256
|
+
};
|
|
1257
|
+
const replaced = (index) => {
|
|
1258
|
+
const stage = parsed.stages[index].name;
|
|
1259
|
+
return buildkit && stage ? contexts.find((c) => stageKey(familiar(c)) === stageKey(stage)) : undefined;
|
|
1260
|
+
};
|
|
1261
|
+
for (const stage of parsed.stages) {
|
|
1262
|
+
if (!buildkit && !plain(stage.name))
|
|
1263
|
+
fail(`ce cannot tell what the stage ${JSON.stringify(stage.name)} is named`);
|
|
1264
|
+
}
|
|
1265
|
+
const source = (index) => {
|
|
1266
|
+
const stage = parsed.stages[index];
|
|
1267
|
+
const base = index === 0 && fromImage !== undefined && !buildkit ? fromImage : evaluate(stage.base, stage.line, (ref) => globals.get(ref));
|
|
1268
|
+
let parent = typeof base === "string" ? findStage(base, index) : -1;
|
|
1269
|
+
const swapped = buildkit ? parent >= 0 ? replaced(parent) : undefined : contexts.find((c) => c === stage.base || c === base);
|
|
1270
|
+
if (swapped !== undefined)
|
|
1271
|
+
parent = -1;
|
|
1272
|
+
return { base, parent, swapped };
|
|
1273
|
+
};
|
|
1274
|
+
const states = new Map;
|
|
1275
|
+
const run = (index) => {
|
|
1276
|
+
const known = states.get(index);
|
|
1277
|
+
if (known)
|
|
1278
|
+
return known;
|
|
1279
|
+
const stage = parsed.stages[index];
|
|
1280
|
+
const { base, parent, swapped } = source(index);
|
|
1281
|
+
const from = parent >= 0 ? run(parent) : undefined;
|
|
1282
|
+
const state = {
|
|
1283
|
+
env: new Map,
|
|
1284
|
+
args: new Map,
|
|
1285
|
+
image: new Map(from?.image),
|
|
1286
|
+
opaque: from?.opaque ?? (swapped !== undefined || base !== "scratch")
|
|
1287
|
+
};
|
|
1288
|
+
const lost = (name) => new Unknown("error", `--build-context ${name} replaces what the FROM on line ${stage.line} builds from, so ce cannot tell what it sets`);
|
|
1289
|
+
let triggers = [];
|
|
1290
|
+
if (buildkit) {
|
|
1291
|
+
if (base instanceof Unknown)
|
|
1292
|
+
state.undeclared = new Unknown("error", `ce cannot tell what the FROM on line ${stage.line} is built from: ${base.reason}`);
|
|
1293
|
+
else if (swapped !== undefined)
|
|
1294
|
+
state.undeclared = lost(swapped);
|
|
1295
|
+
else if (from) {
|
|
1296
|
+
state.args = new Map(from.args);
|
|
1297
|
+
state.undeclared = from.undeclared;
|
|
1298
|
+
for (const [k, v] of from.env)
|
|
1299
|
+
state.env.set(k, from.opaque ? BASE_ENV : k === "PATH" ? SCRATCH_PATH : v);
|
|
1300
|
+
for (const [k, v] of from.image)
|
|
1301
|
+
state.env.set(k, v);
|
|
1302
|
+
triggers = parsed.stages[parent].instructions.filter((i) => i.onbuild);
|
|
1303
|
+
}
|
|
1304
|
+
} else {
|
|
1305
|
+
if (from)
|
|
1306
|
+
state.env = new Map(from.env);
|
|
1307
|
+
const unset = envs.filter((flag) => flag.startsWith("--unsetenv "));
|
|
1308
|
+
for (const flag of unset) {
|
|
1309
|
+
const spec = flag.slice(flag.indexOf(" ") + 1);
|
|
1310
|
+
if (/["\r\n]/u.test(spec))
|
|
1311
|
+
for (const k of state.env.keys())
|
|
1312
|
+
state.env.set(k, new Unknown("error", `${k} may be changed by ${/[\r\n]/u.test(spec) ? `--unsetenv ${JSON.stringify(spec)}` : flag}`));
|
|
1313
|
+
else
|
|
1314
|
+
for (const k of spec.split(","))
|
|
1315
|
+
state.env.delete(k);
|
|
1316
|
+
}
|
|
1317
|
+
for (const flag of envs.filter((f) => f.startsWith("--env "))) {
|
|
1318
|
+
const spec = flag.slice(flag.indexOf(" ") + 1);
|
|
1319
|
+
const copied = (k, v) => {
|
|
1320
|
+
const what = v.includes("$") ? "expand the $" : engineEscapes(v) ? "escape the characters podman and buildah escape" : undefined;
|
|
1321
|
+
if (what === undefined)
|
|
1322
|
+
return v;
|
|
1323
|
+
return new Unknown("error", spec.includes("=") ? `ce does not ${what} in --env ${engineEscapes(spec) ? JSON.stringify(spec) : spec}` : `ce does not ${what} in the ${k} that ${flag} copies`);
|
|
1324
|
+
};
|
|
1325
|
+
if (spec.includes("=")) {
|
|
1326
|
+
const k = spec.slice(0, spec.indexOf("="));
|
|
1327
|
+
state.env.set(k, copied(k, spec.slice(k.length + 1)));
|
|
1328
|
+
} else if (spec.endsWith("*") && !spec.slice(0, -1).includes("*")) {
|
|
1329
|
+
for (const [k, v] of Object.entries(hostEnv))
|
|
1330
|
+
if (k.startsWith(spec.slice(0, -1)) && v !== undefined)
|
|
1331
|
+
state.env.set(k, copied(k, v));
|
|
1332
|
+
} else if (spec.includes("*"))
|
|
1333
|
+
fail(`ce does not read ${flag}`);
|
|
1334
|
+
else if (!spec.endsWith("-") && hostEnv[spec] !== undefined)
|
|
1335
|
+
state.env.set(spec, copied(spec, hostEnv[spec]));
|
|
1336
|
+
}
|
|
1337
|
+
const written = findStage(stage.base, index);
|
|
1338
|
+
if (swapped !== undefined)
|
|
1339
|
+
state.undeclared = lost(swapped);
|
|
1340
|
+
else if (written >= 0)
|
|
1341
|
+
state.args = inheritedArgs(written);
|
|
1342
|
+
const onbuilds = parent >= 0 ? parsed.stages[parent].instructions.filter((i) => i.onbuild) : [];
|
|
1343
|
+
if (format === "docker")
|
|
1344
|
+
triggers = onbuilds;
|
|
1345
|
+
for (const { kind, pairs, line } of format ? [] : onbuilds) {
|
|
1346
|
+
for (const { name: arg } of pairs) {
|
|
1347
|
+
if (!plain(arg))
|
|
1348
|
+
fail(`ce cannot tell what name the ONBUILD on line ${line} sets`);
|
|
1349
|
+
(kind === "ENV" ? state.env : state.args).set(arg, new Unknown("error", `${arg} may be set by the ONBUILD on line ${line}, which ${model} runs only for some image formats`));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const see = (env, args) => (ref) => env.has(ref) ? env.get(ref) : state.opaque ? BASE_ENV : ref === "PATH" ? SCRATCH_PATH : buildkit ? undefined : args.has(ref) ? args.get(ref) : PROXIES.test(ref) ? buildArg(ref) : undefined;
|
|
1354
|
+
const own = stage.instructions.filter((i) => !i.onbuild);
|
|
1355
|
+
for (const { kind, pairs, line } of [...triggers, ...own]) {
|
|
1356
|
+
const before = new Map(state.env);
|
|
1357
|
+
const argsBefore = new Map(state.args);
|
|
1358
|
+
for (const { name, value } of pairs) {
|
|
1359
|
+
const arg = named(kind, name, line);
|
|
1360
|
+
const seen = buildkit && kind === "ARG" ? state.env : before;
|
|
1361
|
+
const evaluated = (word) => evaluate(word, line, see(seen, argsBefore));
|
|
1362
|
+
if (kind === "ENV") {
|
|
1363
|
+
const v = value === undefined ? new Unknown("error", `ce does not read the ENV ${arg} on line ${line}`) : evaluated(value);
|
|
1364
|
+
state.env.set(arg, v);
|
|
1365
|
+
state.image.set(arg, v);
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const given = buildArg(arg);
|
|
1369
|
+
let v;
|
|
1370
|
+
if (given !== undefined)
|
|
1371
|
+
v = given;
|
|
1372
|
+
else if (injected(arg))
|
|
1373
|
+
v = new Unknown("injected");
|
|
1374
|
+
else if (isBuiltin(arg) && (!buildkit || value === undefined))
|
|
1375
|
+
v = buildkit ? globals.get(arg) : builtin(arg, false);
|
|
1376
|
+
else if (value !== undefined)
|
|
1377
|
+
v = evaluated(value);
|
|
1378
|
+
else if (globals.get(arg) !== undefined)
|
|
1379
|
+
v = globals.get(arg);
|
|
1380
|
+
else {
|
|
1381
|
+
if (!state.args.has(arg))
|
|
1382
|
+
state.args.set(arg, state.undeclared);
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
state.args.set(arg, v);
|
|
1386
|
+
if (buildkit && !NON_ENV.includes(arg))
|
|
1387
|
+
state.env.set(arg, v);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
states.set(index, state);
|
|
1391
|
+
return state;
|
|
1392
|
+
};
|
|
1393
|
+
const inherited = new Map;
|
|
1394
|
+
const inheritedArgs = (index) => {
|
|
1395
|
+
const todo = [];
|
|
1396
|
+
let i = index;
|
|
1397
|
+
while (i >= 0 && !inherited.has(i)) {
|
|
1398
|
+
todo.unshift(i);
|
|
1399
|
+
i = findStage(parsed.stages[i].base, i);
|
|
1400
|
+
}
|
|
1401
|
+
let args = new Map(inherited.get(i));
|
|
1402
|
+
for (const at of todo) {
|
|
1403
|
+
for (const { kind, pairs, line, onbuild } of parsed.stages[at].instructions) {
|
|
1404
|
+
if (kind !== "ARG" || onbuild)
|
|
1405
|
+
continue;
|
|
1406
|
+
for (const { name: arg, value } of pairs) {
|
|
1407
|
+
named("ARG", arg, line);
|
|
1408
|
+
const given = buildArg(arg);
|
|
1409
|
+
if (given !== undefined)
|
|
1410
|
+
args.set(arg, given);
|
|
1411
|
+
else if (injected(arg))
|
|
1412
|
+
args.set(arg, new Unknown("injected"));
|
|
1413
|
+
else if (isBuiltin(arg))
|
|
1414
|
+
args.set(arg, globals.get(arg));
|
|
1415
|
+
else if (value?.includes("$"))
|
|
1416
|
+
args.set(arg, new Unknown("error", `ce does not work out the $ in the ARG ${arg} on line ${line}, which a stage built from it gets`));
|
|
1417
|
+
else if (value !== undefined)
|
|
1418
|
+
args.set(arg, evaluate(value, line, () => {
|
|
1419
|
+
return;
|
|
1420
|
+
}));
|
|
1421
|
+
else if (globals.get(arg) !== undefined)
|
|
1422
|
+
args.set(arg, globals.get(arg));
|
|
1423
|
+
else if (!args.has(arg))
|
|
1424
|
+
args.set(arg, undefined);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
inherited.set(at, args);
|
|
1428
|
+
args = new Map(args);
|
|
1429
|
+
}
|
|
1430
|
+
return args;
|
|
1431
|
+
};
|
|
1432
|
+
let index = parsed.stages.length - 1;
|
|
1433
|
+
if (index < 0)
|
|
1434
|
+
fail("it has no FROM");
|
|
1435
|
+
if (target) {
|
|
1436
|
+
index = findStage(target, parsed.stages.length, true);
|
|
1437
|
+
if (index < 0)
|
|
1438
|
+
fail(`it has no stage named ${JSON.stringify(target)}`);
|
|
1439
|
+
}
|
|
1440
|
+
const swapped = replaced(index);
|
|
1441
|
+
if (swapped !== undefined)
|
|
1442
|
+
fail(`--build-context ${swapped} replaces the stage built`);
|
|
1443
|
+
const ancestors = [];
|
|
1444
|
+
for (let i = index;i >= 0; i = source(i).parent)
|
|
1445
|
+
ancestors.unshift(i);
|
|
1446
|
+
for (const i of ancestors)
|
|
1447
|
+
run(i);
|
|
1448
|
+
const state = run(index);
|
|
1449
|
+
const v = state.args.has(name) ? state.args.get(name) : state.undeclared ?? (!globals.has(name) && injected(name) && buildArg(name) === undefined ? new Unknown("injected") : globals.get(name));
|
|
1450
|
+
if (v instanceof Unknown) {
|
|
1451
|
+
if (v.why === "builtin")
|
|
1452
|
+
fail(`${name} is set by the builder${v.reason && `: ${v.reason}`}`);
|
|
1453
|
+
if (v.why === "injected")
|
|
1454
|
+
fail(`${name} may be set by the builder`);
|
|
1455
|
+
fail(v.why === "error" ? v.reason : `ce cannot tell what ${name} is`);
|
|
1456
|
+
}
|
|
1457
|
+
return v;
|
|
1458
|
+
}
|
|
1459
|
+
function readContainerfile(file, model) {
|
|
1460
|
+
let text;
|
|
1461
|
+
try {
|
|
1462
|
+
text = fs5.readFileSync(file, "utf8");
|
|
1463
|
+
} catch (exc) {
|
|
1464
|
+
return fail(`ce cannot read ${JSON.stringify(file)}: ${errorMessage(exc)}`);
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
return parseContainerfile(text, model);
|
|
1468
|
+
} catch (exc) {
|
|
1469
|
+
if (exc instanceof Unreadable)
|
|
1470
|
+
fail(`${JSON.stringify(file)}: ${exc.message}`);
|
|
1471
|
+
throw exc;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function loadContainerfile(files, buildArg) {
|
|
1475
|
+
const docker = files.engine === "docker";
|
|
1476
|
+
let file;
|
|
1477
|
+
let parsed;
|
|
1478
|
+
try {
|
|
1479
|
+
file = findContainerfile(files);
|
|
1480
|
+
parsed = readContainerfile(file, docker ? "buildkit" : "imagebuilder");
|
|
1481
|
+
} catch (exc) {
|
|
1482
|
+
if (exc instanceof Unreadable)
|
|
1483
|
+
return { error: exc };
|
|
1484
|
+
throw exc;
|
|
1485
|
+
}
|
|
1486
|
+
const frontend = docker ? buildArg("BUILDKIT_SYNTAX")?.trim().split(" ")[0] ?? parsed.syntax : undefined;
|
|
1487
|
+
if (frontend !== undefined && !STANDARD_SYNTAX.test(frontend))
|
|
1488
|
+
return {
|
|
1489
|
+
file,
|
|
1490
|
+
error: new Unreadable(`it is read by the frontend ${JSON.stringify(frontend)}, which may read it differently`)
|
|
1491
|
+
};
|
|
1492
|
+
return { file, parsed };
|
|
1493
|
+
}
|
|
1494
|
+
function dockerfileEpoch(files, buildArg) {
|
|
1495
|
+
let read;
|
|
1496
|
+
try {
|
|
1497
|
+
read = loadContainerfile(files, buildArg);
|
|
1498
|
+
} catch {
|
|
1499
|
+
return "";
|
|
1500
|
+
}
|
|
1501
|
+
if (!read.parsed)
|
|
1502
|
+
return "";
|
|
1503
|
+
const defaults = read.parsed.globals.flatMap(({ pairs }) => pairs).filter(({ name, value }) => name === "SOURCE_DATE_EPOCH" && value !== undefined);
|
|
1504
|
+
if (!defaults.length)
|
|
1505
|
+
return;
|
|
1506
|
+
const value = defaults[0].value;
|
|
1507
|
+
return defaults.length === 1 && !value.includes("$") ? value : "";
|
|
1508
|
+
}
|
|
1509
|
+
function containerfileDefaults(files, buildArg) {
|
|
1510
|
+
const model = files.engine === "docker" ? "buildkit" : "imagebuilder";
|
|
1511
|
+
let read;
|
|
1512
|
+
const load = () => loadContainerfile(files, buildArg);
|
|
1513
|
+
const platforms = files.platforms?.length ? [...new Set(files.platforms)] : [undefined];
|
|
1514
|
+
return (name) => {
|
|
1515
|
+
read ??= load();
|
|
1516
|
+
try {
|
|
1517
|
+
if (read.error)
|
|
1518
|
+
throw read.error;
|
|
1519
|
+
const values = platforms.map((platform) => argDefault(read.parsed, name, files.target, buildArg, model, {
|
|
1520
|
+
contexts: files.contexts ?? [],
|
|
1521
|
+
envs: files.envs ?? [],
|
|
1522
|
+
env: files.env ?? process.env,
|
|
1523
|
+
platform,
|
|
1524
|
+
platformIgnored: files.platformIgnored,
|
|
1525
|
+
format: files.format,
|
|
1526
|
+
from: files.from
|
|
1527
|
+
}));
|
|
1528
|
+
const other = values.findIndex((v) => (v ?? "") !== (values[0] ?? ""));
|
|
1529
|
+
if (other !== -1)
|
|
1530
|
+
fail(`it is ${JSON.stringify(values[0] ?? "")} for ${platforms[0]} but ${JSON.stringify(values[other] ?? "")} for ${platforms[other]}, and every platform gets the same tags`);
|
|
1531
|
+
return values[0];
|
|
1532
|
+
} catch (exc) {
|
|
1533
|
+
if (!(exc instanceof Unreadable))
|
|
1534
|
+
throw exc;
|
|
1535
|
+
const where = read.file ? ` from ${JSON.stringify(read.file)}` : "";
|
|
1536
|
+
throw new CeError(`ce: {arg.${name}} has no value from --build-arg, and ce cannot work out its default${where}: ${exc.message}`);
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/version.ts
|
|
1542
|
+
var SEMVER = /^v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?)$/u;
|
|
1543
|
+
|
|
1544
|
+
// src/metadata.ts
|
|
1545
|
+
var CI_VARIABLES = [
|
|
1546
|
+
"image",
|
|
1547
|
+
"ref",
|
|
1548
|
+
"branch",
|
|
1549
|
+
"pr",
|
|
1550
|
+
"tag",
|
|
1551
|
+
"default",
|
|
1552
|
+
"revision",
|
|
1553
|
+
"sha",
|
|
1554
|
+
"version",
|
|
1555
|
+
"major",
|
|
1556
|
+
"minor",
|
|
1557
|
+
"patch",
|
|
1558
|
+
"stable"
|
|
1559
|
+
];
|
|
1560
|
+
var ENV_VARIABLE = /^env\.([A-Za-z_][A-Za-z0-9_]*)$/u;
|
|
1561
|
+
var nonEmpty = (value) => value ? value : undefined;
|
|
1562
|
+
function epochDate(epoch, base = 10) {
|
|
1563
|
+
const match = base === 10 ? /^([+-]?)(\d+)$/u.exec(epoch) : /^([+-]?)(0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][01]+|0[0-7]+|0|[1-9]\d*)$/u.exec(epoch);
|
|
1564
|
+
if (!match)
|
|
1565
|
+
return;
|
|
1566
|
+
const digits = match[2];
|
|
1567
|
+
const magnitude = base === 0 && /^0[0-7]/u.test(digits) ? BigInt(`0o${digits.slice(1)}`) : BigInt(digits);
|
|
1568
|
+
const seconds = match[1] === "-" ? -magnitude : magnitude;
|
|
1569
|
+
if (seconds < -(2n ** 63n) || seconds >= 2n ** 63n)
|
|
1570
|
+
return;
|
|
1571
|
+
const date = new Date(Number(seconds) * 1000);
|
|
1572
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
1573
|
+
}
|
|
1574
|
+
var rfc3339 = (date) => date.toISOString().replace(/\.\d{3}Z$/u, "Z");
|
|
1575
|
+
|
|
1576
|
+
class CiMetadata {
|
|
1577
|
+
#env;
|
|
1578
|
+
#cache = new Map;
|
|
1579
|
+
#ref;
|
|
1580
|
+
#event;
|
|
1581
|
+
constructor(env = process.env) {
|
|
1582
|
+
this.#env = env;
|
|
1583
|
+
}
|
|
1584
|
+
known(name) {
|
|
1585
|
+
return CI_VARIABLES.includes(name) || ENV_VARIABLE.test(name);
|
|
1586
|
+
}
|
|
1587
|
+
get(name) {
|
|
1588
|
+
const envName = ENV_VARIABLE.exec(name)?.[1];
|
|
1589
|
+
if (envName)
|
|
1590
|
+
return Object.hasOwn(this.#env, envName) ? nonEmpty(this.#env[envName]) : undefined;
|
|
1591
|
+
if (!this.#cache.has(name))
|
|
1592
|
+
this.#cache.set(name, this.#resolve(name));
|
|
1593
|
+
return this.#cache.get(name);
|
|
1594
|
+
}
|
|
1595
|
+
labels(now = new Date) {
|
|
1596
|
+
const labels = {};
|
|
1597
|
+
const add = (key, value) => {
|
|
1598
|
+
try {
|
|
1599
|
+
const v = value();
|
|
1600
|
+
if (v)
|
|
1601
|
+
labels[`org.opencontainers.image.${key}`] = v;
|
|
1602
|
+
} catch (exc) {
|
|
1603
|
+
if (!(exc instanceof CeError))
|
|
1604
|
+
throw exc;
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
const server = nonEmpty(this.#ciEnv("SERVER_URL"))?.replace(/\/+$/u, "");
|
|
1608
|
+
const repository = nonEmpty(this.#ciEnv("REPOSITORY"));
|
|
1609
|
+
const page = server && repository ? `${server}/${repository}` : undefined;
|
|
1610
|
+
const repo = (key) => {
|
|
1611
|
+
const value = this.#payload().repository;
|
|
1612
|
+
return isObject(value) && typeof value[key] === "string" ? value[key] : undefined;
|
|
1613
|
+
};
|
|
1614
|
+
add("source", () => page);
|
|
1615
|
+
add("url", () => {
|
|
1616
|
+
try {
|
|
1617
|
+
return repo("website") || page;
|
|
1618
|
+
} catch (exc) {
|
|
1619
|
+
if (exc instanceof CeError)
|
|
1620
|
+
return page;
|
|
1621
|
+
throw exc;
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
add("revision", () => this.#ciEnv("SHA"));
|
|
1625
|
+
add("version", () => this.get("version") ?? this.#rawRef());
|
|
1626
|
+
add("title", () => repository?.split("/").at(-1));
|
|
1627
|
+
add("description", () => repo("description"));
|
|
1628
|
+
const epoch = this.#env.SOURCE_DATE_EPOCH;
|
|
1629
|
+
const time = epoch ? epochDate(epoch) : now;
|
|
1630
|
+
if (Object.keys(labels).length && time)
|
|
1631
|
+
add("created", () => rfc3339(time));
|
|
1632
|
+
return labels;
|
|
1633
|
+
}
|
|
1634
|
+
#resolve(name) {
|
|
1635
|
+
switch (name) {
|
|
1636
|
+
case "image": {
|
|
1637
|
+
const server = this.#require("SERVER_URL", "{image}");
|
|
1638
|
+
const repository = this.#require("REPOSITORY", "{image}");
|
|
1639
|
+
return `${registryServer(server)}/${repository}`.toLowerCase();
|
|
1640
|
+
}
|
|
1641
|
+
case "ref":
|
|
1642
|
+
return this.#rawRef();
|
|
1643
|
+
case "branch": {
|
|
1644
|
+
const ref = this.#parsedRef();
|
|
1645
|
+
return ref.kind === "branch" ? ref.name : undefined;
|
|
1646
|
+
}
|
|
1647
|
+
case "tag": {
|
|
1648
|
+
const ref = this.#parsedRef();
|
|
1649
|
+
return ref.kind === "tag" ? ref.name : undefined;
|
|
1650
|
+
}
|
|
1651
|
+
case "pr": {
|
|
1652
|
+
const ref = this.#parsedRef();
|
|
1653
|
+
return ref.kind === "pr" ? ref.number : undefined;
|
|
1654
|
+
}
|
|
1655
|
+
case "default": {
|
|
1656
|
+
const ref = this.#parsedRef();
|
|
1657
|
+
if (ref.kind !== "branch")
|
|
1658
|
+
return;
|
|
1659
|
+
return ref.name === this.defaultBranch() ? ref.name : undefined;
|
|
1660
|
+
}
|
|
1661
|
+
case "revision":
|
|
1662
|
+
return this.#require("SHA", `{${name}}`);
|
|
1663
|
+
case "sha":
|
|
1664
|
+
return this.#require("SHA", `{${name}}`).slice(0, 7);
|
|
1665
|
+
case "version":
|
|
1666
|
+
return this.#semver()?.[1];
|
|
1667
|
+
case "major":
|
|
1668
|
+
return this.#semver()?.[2];
|
|
1669
|
+
case "minor":
|
|
1670
|
+
return this.#semver()?.[3];
|
|
1671
|
+
case "patch":
|
|
1672
|
+
return this.#semver()?.[4];
|
|
1673
|
+
case "stable": {
|
|
1674
|
+
const semver = this.#semver();
|
|
1675
|
+
return semver && semver[5] === undefined ? semver[1] : undefined;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
throw new CeError(`ce: unknown placeholder {${name}}`);
|
|
1679
|
+
}
|
|
1680
|
+
#ciEnv(name) {
|
|
1681
|
+
return nonEmpty(this.#env[`FORGEJO_${name}`]) ?? nonEmpty(this.#env[`GITHUB_${name}`]);
|
|
1682
|
+
}
|
|
1683
|
+
#require(name, user) {
|
|
1684
|
+
const value = this.#ciEnv(name);
|
|
1685
|
+
if (!value) {
|
|
1686
|
+
throw new CeError(`ce: ${user} needs GITHUB_${name} (or FORGEJO_${name}), which is not set; is this a Forgejo Actions run?`);
|
|
1687
|
+
}
|
|
1688
|
+
return value;
|
|
1689
|
+
}
|
|
1690
|
+
#rawRef() {
|
|
1691
|
+
const ref = this.#parsedRef();
|
|
1692
|
+
return ref.kind === "pr" ? `pr-${ref.number}` : ref.name;
|
|
1693
|
+
}
|
|
1694
|
+
#parsedRef() {
|
|
1695
|
+
if (this.#ref)
|
|
1696
|
+
return this.#ref;
|
|
1697
|
+
const ref = this.#require("REF", "{ref}, {branch}, {tag} or {pr}");
|
|
1698
|
+
const event = this.#ciEnv("EVENT_NAME") ?? "";
|
|
1699
|
+
const pull = /^refs\/pull\/(\d+)\//u.exec(ref);
|
|
1700
|
+
if (event.startsWith("pull_request") || pull) {
|
|
1701
|
+
const payload = pull ? {} : this.#payload();
|
|
1702
|
+
const fromPayload = [
|
|
1703
|
+
isObject(payload.pull_request) && payload.pull_request.number,
|
|
1704
|
+
payload.number
|
|
1705
|
+
].find(Number.isInteger);
|
|
1706
|
+
const number = pull?.[1] ?? (fromPayload === undefined ? undefined : String(fromPayload));
|
|
1707
|
+
if (!number)
|
|
1708
|
+
throw new CeError(`ce: cannot find the pull request number for ${event} on ${ref}`);
|
|
1709
|
+
this.#ref = { kind: "pr", number };
|
|
1710
|
+
} else if (ref.startsWith("refs/heads/")) {
|
|
1711
|
+
this.#ref = { kind: "branch", name: ref.slice("refs/heads/".length) };
|
|
1712
|
+
} else if (ref.startsWith("refs/tags/")) {
|
|
1713
|
+
this.#ref = { kind: "tag", name: ref.slice("refs/tags/".length) };
|
|
1714
|
+
} else {
|
|
1715
|
+
throw new CeError(`ce: cannot tell what ${JSON.stringify(ref)} is: not a branch, tag or pull request`);
|
|
1716
|
+
}
|
|
1717
|
+
return this.#ref;
|
|
1718
|
+
}
|
|
1719
|
+
#semver() {
|
|
1720
|
+
const ref = this.#parsedRef();
|
|
1721
|
+
return ref.kind === "tag" ? SEMVER.exec(ref.name) : null;
|
|
1722
|
+
}
|
|
1723
|
+
defaultBranch() {
|
|
1724
|
+
const repository = this.#ciEnv("EVENT_PATH") ? this.#payload().repository : undefined;
|
|
1725
|
+
const fromEvent = isObject(repository) ? repository.default_branch : undefined;
|
|
1726
|
+
const branch = typeof fromEvent === "string" && fromEvent || nonEmpty(this.#env.DEFAULT_BRANCH);
|
|
1727
|
+
if (!branch) {
|
|
1728
|
+
throw new CeError("ce: {default} needs repository.default_branch in the event file or DEFAULT_BRANCH, and neither is set");
|
|
1729
|
+
}
|
|
1730
|
+
return branch;
|
|
546
1731
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (!/^(?:[^/:]+:\d+\/)?[^:]*$/u.test(image)) {
|
|
562
|
-
throw new CeError(`ce: image name ${JSON.stringify(image)} from ${JSON.stringify(template)} has a ":" that is not a registry port`);
|
|
1732
|
+
#payload() {
|
|
1733
|
+
if (this.#event)
|
|
1734
|
+
return this.#event;
|
|
1735
|
+
const file = this.#require("EVENT_PATH", "reading the event file");
|
|
1736
|
+
let payload;
|
|
1737
|
+
try {
|
|
1738
|
+
payload = readJsonFile(file, undefined);
|
|
1739
|
+
} catch (exc) {
|
|
1740
|
+
throw new CeError(`ce: ${errorMessage(exc)}`);
|
|
1741
|
+
}
|
|
1742
|
+
if (!isObject(payload))
|
|
1743
|
+
throw new CeError(`ce: the event file ${JSON.stringify(file)} is missing or not a JSON object`);
|
|
1744
|
+
this.#event = payload;
|
|
1745
|
+
return payload;
|
|
563
1746
|
}
|
|
564
|
-
return image;
|
|
565
1747
|
}
|
|
566
|
-
|
|
567
|
-
|
|
1748
|
+
var ON_DEFAULT_BRANCH = new Set(["ref", "branch", "default"]);
|
|
1749
|
+
var UNKNOWN_ON_DEFAULT_BRANCH = new Set(["revision", "sha"]);
|
|
1750
|
+
function onDefaultBranch(vars, branch) {
|
|
1751
|
+
return {
|
|
1752
|
+
known: (name) => vars.known(name),
|
|
1753
|
+
get(name) {
|
|
1754
|
+
if (ON_DEFAULT_BRANCH.has(name))
|
|
1755
|
+
return branch;
|
|
1756
|
+
if (UNKNOWN_ON_DEFAULT_BRANCH.has(name)) {
|
|
1757
|
+
throw new CeError(`ce: the default branch's cache scope cannot use {${name}}: this run does not know its commit`);
|
|
1758
|
+
}
|
|
1759
|
+
return name === "image" || name.startsWith("env.") ? vars.get(name) : undefined;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
568
1762
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1763
|
+
|
|
1764
|
+
// src/tag-lists.ts
|
|
1765
|
+
var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
1766
|
+
var REFERENCE = /^\{tags\.([A-Za-z_][A-Za-z0-9_]*)\}$/u;
|
|
1767
|
+
function defineTagList(spec, lists) {
|
|
1768
|
+
const eq = spec.indexOf("=");
|
|
1769
|
+
const name = eq < 0 ? spec : spec.slice(0, eq);
|
|
1770
|
+
if (eq < 0 || !NAME.test(name))
|
|
1771
|
+
throw new CeError("ce: --tag-list requires NAME=TEMPLATES, with an identifier as NAME");
|
|
1772
|
+
const template = spec.slice(eq + 1);
|
|
1773
|
+
if (!splitTagTemplates(template).length)
|
|
1774
|
+
throw new CeError(`ce: --tag-list ${name} has no tags`);
|
|
1775
|
+
if (lists.has(name))
|
|
1776
|
+
throw new CeError(`ce: --tag-list ${name} is defined more than once`);
|
|
1777
|
+
lists.set(name, template);
|
|
1778
|
+
}
|
|
1779
|
+
function splitTagTemplates(value) {
|
|
1780
|
+
return value.match(/(?:\{[^{}]*\}|[^,\s{}]|[{}])+/gu) ?? [];
|
|
1781
|
+
}
|
|
1782
|
+
function expandTagLists(value, lists) {
|
|
1783
|
+
const tags = [];
|
|
1784
|
+
const active = new Set;
|
|
1785
|
+
const stack = [{ name: "", tags: splitTagTemplates(value), index: 0 }];
|
|
1786
|
+
while (stack.length) {
|
|
1787
|
+
const frame = stack.at(-1);
|
|
1788
|
+
if (frame.index === frame.tags.length) {
|
|
1789
|
+
active.delete(frame.name);
|
|
1790
|
+
stack.pop();
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const tag = frame.tags[frame.index++];
|
|
1794
|
+
const name = REFERENCE.exec(tag)?.[1];
|
|
1795
|
+
if (name === undefined) {
|
|
1796
|
+
if (tag.includes("{tags."))
|
|
1797
|
+
throw new CeError(`ce: a {tags.NAME} reference must be a whole tag-list entry: ${JSON.stringify(tag)}`);
|
|
1798
|
+
tags.push(tag);
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
const template = lists.get(name);
|
|
1802
|
+
if (template === undefined)
|
|
1803
|
+
throw new CeError(`ce: unknown tag list ${JSON.stringify(name)}; define it with --tag-list`);
|
|
1804
|
+
if (active.has(name))
|
|
1805
|
+
throw new CeError(`ce: cyclic tag list reference: ${[...active, name].join(" -> ")}`);
|
|
1806
|
+
active.add(name);
|
|
1807
|
+
stack.push({ name, tags: splitTagTemplates(template), index: 0 });
|
|
573
1808
|
}
|
|
574
|
-
return
|
|
1809
|
+
return tags;
|
|
575
1810
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1811
|
+
|
|
1812
|
+
// src/template.ts
|
|
1813
|
+
var PLACEHOLDER = /\{([^{}]*)\}/gu;
|
|
1814
|
+
function hasPlaceholders(value) {
|
|
1815
|
+
return value.includes("{") || value.includes("}");
|
|
580
1816
|
}
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1817
|
+
function parse(template, vars) {
|
|
1818
|
+
const parts = [];
|
|
1819
|
+
const text = (value) => {
|
|
1820
|
+
if (hasPlaceholders(value))
|
|
1821
|
+
throw new CeError(`ce: unmatched brace in ${JSON.stringify(template)}`);
|
|
1822
|
+
if (value)
|
|
1823
|
+
parts.push({ text: value });
|
|
1824
|
+
};
|
|
1825
|
+
let last = 0;
|
|
1826
|
+
for (const match of template.matchAll(PLACEHOLDER)) {
|
|
1827
|
+
text(template.slice(last, match.index));
|
|
1828
|
+
last = match.index + match[0].length;
|
|
1829
|
+
const expression = match[1];
|
|
1830
|
+
if (expression.startsWith("?") || expression.startsWith("!")) {
|
|
1831
|
+
const alternatives = expression.slice(1).split("|").map((term) => {
|
|
1832
|
+
const eq = term.indexOf("=");
|
|
1833
|
+
const name = eq < 0 ? term : term.slice(0, eq);
|
|
1834
|
+
if (!vars.known(name))
|
|
1835
|
+
throw new CeError(`ce: unknown placeholder ${match[0]} in ${JSON.stringify(template)}`);
|
|
1836
|
+
return eq < 0 ? { name } : { name, expected: term.slice(eq + 1) };
|
|
1837
|
+
});
|
|
1838
|
+
parts.push({
|
|
1839
|
+
guard: match[0],
|
|
1840
|
+
negate: expression.startsWith("!"),
|
|
1841
|
+
alternatives
|
|
1842
|
+
});
|
|
607
1843
|
} else {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1844
|
+
if (!vars.known(expression))
|
|
1845
|
+
throw new CeError(`ce: unknown placeholder ${match[0]} in ${JSON.stringify(template)}`);
|
|
1846
|
+
parts.push({ name: expression });
|
|
611
1847
|
}
|
|
612
1848
|
}
|
|
613
|
-
|
|
1849
|
+
text(template.slice(last));
|
|
1850
|
+
return parts;
|
|
614
1851
|
}
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const fields = [];
|
|
620
|
-
let i = 0;
|
|
621
|
-
for (;; ) {
|
|
622
|
-
if (value[i] !== '"') {
|
|
623
|
-
const end = value.indexOf(",", i);
|
|
624
|
-
const field = end < 0 ? value.slice(i) : value.slice(i, end);
|
|
625
|
-
if (field.includes('"'))
|
|
626
|
-
return null;
|
|
627
|
-
fields.push(field);
|
|
628
|
-
if (end < 0)
|
|
629
|
-
return fields;
|
|
630
|
-
i = end + 1;
|
|
1852
|
+
function failedGuard(parts, vars) {
|
|
1853
|
+
let unknown;
|
|
1854
|
+
for (const part of parts) {
|
|
1855
|
+
if (!("guard" in part))
|
|
631
1856
|
continue;
|
|
1857
|
+
try {
|
|
1858
|
+
let unresolved;
|
|
1859
|
+
let matched = false;
|
|
1860
|
+
for (const condition of part.alternatives) {
|
|
1861
|
+
try {
|
|
1862
|
+
const value = vars.get(condition.name);
|
|
1863
|
+
if (value !== undefined && (condition.expected === undefined || value === condition.expected)) {
|
|
1864
|
+
matched = true;
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
} catch (exc) {
|
|
1868
|
+
unresolved ??= exc;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (!matched && unresolved !== undefined)
|
|
1872
|
+
throw unresolved;
|
|
1873
|
+
if (matched === part.negate) {
|
|
1874
|
+
const only = part.alternatives.length === 1 ? part.alternatives[0] : undefined;
|
|
1875
|
+
return {
|
|
1876
|
+
guard: part.guard,
|
|
1877
|
+
reason: only ? only.expected === undefined ? `${only.name} has ${part.negate ? "a" : "no"} value` : `${only.name} ${part.negate ? "matched" : "did not match"} the required value` : part.negate ? "an alternative matched" : "no alternative matched"
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
} catch (exc) {
|
|
1881
|
+
unknown ??= exc;
|
|
632
1882
|
}
|
|
633
|
-
let field = "";
|
|
634
|
-
for (i++;; i++) {
|
|
635
|
-
const quote = value.indexOf('"', i);
|
|
636
|
-
if (quote < 0)
|
|
637
|
-
return null;
|
|
638
|
-
field += value.slice(i, quote);
|
|
639
|
-
i = quote + 1;
|
|
640
|
-
if (value[i] !== '"')
|
|
641
|
-
break;
|
|
642
|
-
field += '"';
|
|
643
|
-
}
|
|
644
|
-
fields.push(field);
|
|
645
|
-
if (i === value.length)
|
|
646
|
-
return fields;
|
|
647
|
-
if (value[i] !== ",")
|
|
648
|
-
return null;
|
|
649
|
-
i++;
|
|
650
1883
|
}
|
|
1884
|
+
if (unknown !== undefined)
|
|
1885
|
+
throw unknown;
|
|
1886
|
+
return;
|
|
651
1887
|
}
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
var fieldKey = (field) => field.split("=", 1)[0].toLowerCase();
|
|
656
|
-
function ghaFields(value) {
|
|
657
|
-
const fields = csvFields(value);
|
|
658
|
-
const type = fields?.findLast((field) => fieldKey(field) === "type");
|
|
659
|
-
return type?.slice("type=".length) === "gha" ? fields : null;
|
|
660
|
-
}
|
|
661
|
-
var scopeIndex = (fields) => fields.findLastIndex((field) => fieldKey(field) === "scope");
|
|
662
|
-
function cacheScopeName(value) {
|
|
663
|
-
const fields = ghaFields(value);
|
|
664
|
-
if (!fields)
|
|
665
|
-
return value;
|
|
666
|
-
const i = scopeIndex(fields);
|
|
667
|
-
if (i < 0)
|
|
668
|
-
return "";
|
|
669
|
-
const scope = fields[i].slice("scope=".length);
|
|
670
|
-
assertAscii("cache scope", scope);
|
|
671
|
-
return sanitizeChars(scope, INVALID_SCOPE_CHARS);
|
|
1888
|
+
function checkTemplate(template, vars) {
|
|
1889
|
+
parse(template, vars);
|
|
672
1890
|
}
|
|
673
|
-
function
|
|
674
|
-
const
|
|
675
|
-
if (!
|
|
676
|
-
return
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
assertAscii("cache prefix", cachePrefix);
|
|
680
|
-
const prefix = sanitizeChars(cachePrefix, INVALID_TAG_CHARS);
|
|
681
|
-
let scope = cacheScopeName(value);
|
|
682
|
-
if (!scope) {
|
|
683
|
-
scope = prefix.replace(/[-_.]+$/u, "");
|
|
684
|
-
} else if (prefix) {
|
|
685
|
-
scope = SEPARATORS.includes(prefix.at(-1)) ? `${prefix}${scope}` : `${prefix}-${scope}`;
|
|
686
|
-
}
|
|
687
|
-
const i = scopeIndex(fields);
|
|
688
|
-
if (i < 0)
|
|
689
|
-
fields.push(`scope=${scope}`);
|
|
690
|
-
else
|
|
691
|
-
fields[i] = `scope=${scope}`;
|
|
692
|
-
return csvJoin(fields);
|
|
1891
|
+
function guardsPass(template, vars, onSkip) {
|
|
1892
|
+
const failure = failedGuard(parse(template, vars), vars);
|
|
1893
|
+
if (!failure)
|
|
1894
|
+
return true;
|
|
1895
|
+
onSkip?.(failure);
|
|
1896
|
+
return false;
|
|
693
1897
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if (v.length > 1 && v.startsWith("-")) {
|
|
701
|
-
throw new CeError(`ce: ${flag} requires a value but the next token ${JSON.stringify(v)} looks like a flag.`);
|
|
1898
|
+
function expand(template, vars, encode, onSkip) {
|
|
1899
|
+
const parts = parse(template, vars);
|
|
1900
|
+
const failure = failedGuard(parts, vars);
|
|
1901
|
+
if (failure) {
|
|
1902
|
+
onSkip?.(failure);
|
|
1903
|
+
return null;
|
|
702
1904
|
}
|
|
703
|
-
return
|
|
1905
|
+
return render(parts, vars, encode, (name) => `ce: {${name}} has no value here, in ${JSON.stringify(template)}. ` + `To leave the tag out instead, guard it with {?${name}}.`);
|
|
704
1906
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const rawCacheFrom = [];
|
|
716
|
-
const rawCacheTo = [];
|
|
717
|
-
const summary = {
|
|
718
|
-
tags: [],
|
|
719
|
-
cacheFrom: [],
|
|
720
|
-
cacheTo: [],
|
|
721
|
-
buildArgs: [],
|
|
722
|
-
labels: [],
|
|
723
|
-
dockerfile: "",
|
|
724
|
-
target: "",
|
|
725
|
-
platform: "",
|
|
726
|
-
context: ""
|
|
727
|
-
};
|
|
728
|
-
let templated = false;
|
|
729
|
-
let rest = [];
|
|
730
|
-
const addTags = (value, flag) => {
|
|
731
|
-
templated ||= hasPlaceholders(value);
|
|
732
|
-
const refs = rewriteTagValue(value, tagPrefix, vars);
|
|
733
|
-
for (const ref of refs)
|
|
734
|
-
args.push(`${flag}${ref}`);
|
|
735
|
-
summary.tags.push(...refs);
|
|
736
|
-
};
|
|
737
|
-
const summarize = (flag, value) => {
|
|
738
|
-
const field = SUMMARIZED[flag];
|
|
739
|
-
if (field === "buildArgs" || field === "labels")
|
|
740
|
-
summary[field].push(value);
|
|
741
|
-
else
|
|
742
|
-
summary[field] = value;
|
|
743
|
-
};
|
|
744
|
-
for (let i = 0;i < argv.length; i++) {
|
|
745
|
-
const arg = argv[i];
|
|
746
|
-
const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
|
|
747
|
-
const next = () => consumeValue(argv.slice(++i), arg)[0];
|
|
748
|
-
if (arg === "--") {
|
|
749
|
-
rest = argv.slice(i);
|
|
750
|
-
if (rest.length > 1)
|
|
751
|
-
summary.context = rest.at(-1);
|
|
752
|
-
break;
|
|
753
|
-
} else if (arg === "-t")
|
|
754
|
-
addTags(next(), "-t");
|
|
755
|
-
else if (arg === "--tag")
|
|
756
|
-
addTags(next(), "--tag=");
|
|
757
|
-
else if (arg.startsWith("-t"))
|
|
758
|
-
addTags(arg.slice(2), "-t");
|
|
759
|
-
else if (name === "--tag")
|
|
760
|
-
addTags(attached, "--tag=");
|
|
761
|
-
else if (name === "--cache-from")
|
|
762
|
-
rawCacheFrom.push(attached ?? next());
|
|
763
|
-
else if (name === "--cache-to")
|
|
764
|
-
rawCacheTo.push(attached ?? next());
|
|
765
|
-
else if (name === "--cache") {
|
|
766
|
-
const v = attached ?? next();
|
|
767
|
-
rawCacheFrom.push(v);
|
|
768
|
-
rawCacheTo.push(v);
|
|
769
|
-
} else if (Object.hasOwn(SUMMARIZED, name)) {
|
|
770
|
-
const flag = name;
|
|
771
|
-
if (attached === undefined) {
|
|
772
|
-
const v = next();
|
|
773
|
-
args.push(arg, v);
|
|
774
|
-
summarize(flag, v);
|
|
775
|
-
} else {
|
|
776
|
-
args.push(arg);
|
|
777
|
-
summarize(flag, attached);
|
|
778
|
-
}
|
|
779
|
-
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
780
|
-
args.push(arg);
|
|
781
|
-
summary.dockerfile = arg.slice(2);
|
|
782
|
-
} else {
|
|
783
|
-
args.push(arg);
|
|
784
|
-
if (!arg.startsWith("-"))
|
|
785
|
-
summary.context = arg;
|
|
1907
|
+
function render(parts, vars, encode, missing) {
|
|
1908
|
+
let out = "";
|
|
1909
|
+
for (const part of parts) {
|
|
1910
|
+
if ("text" in part)
|
|
1911
|
+
out += part.text;
|
|
1912
|
+
else if ("name" in part) {
|
|
1913
|
+
const value = vars.get(part.name);
|
|
1914
|
+
if (value === undefined)
|
|
1915
|
+
throw new CeError(missing(part.name));
|
|
1916
|
+
out += encode(value);
|
|
786
1917
|
}
|
|
787
1918
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
summary.labels.unshift(...added);
|
|
795
|
-
if (rawCacheFrom.length || rawCacheTo.length)
|
|
796
|
-
cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
|
|
797
|
-
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
798
|
-
for (const c of rawCacheFrom) {
|
|
799
|
-
const rewritten = toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c;
|
|
800
|
-
args.push(`--cache-from=${rewritten}`);
|
|
801
|
-
summary.cacheFrom.push(rewritten);
|
|
802
|
-
}
|
|
803
|
-
for (const c of rawCacheTo) {
|
|
804
|
-
const rewritten = normalizeCacheScope(c, cachePrefix);
|
|
805
|
-
args.push(`--cache-to=${rewritten}`);
|
|
806
|
-
summary.cacheTo.push(rewritten);
|
|
1919
|
+
return out;
|
|
1920
|
+
}
|
|
1921
|
+
function checkValue(template, vars, what, guards = false) {
|
|
1922
|
+
const parts = parse(template, vars);
|
|
1923
|
+
if (!guards && parts.some((part) => ("guard" in part) && part.guard)) {
|
|
1924
|
+
throw new CeError(`ce: ${what} ${JSON.stringify(template)} cannot use guards here`);
|
|
807
1925
|
}
|
|
808
|
-
|
|
809
|
-
return { args, summary };
|
|
1926
|
+
return parts;
|
|
810
1927
|
}
|
|
811
|
-
function
|
|
812
|
-
const
|
|
813
|
-
|
|
1928
|
+
function expandValue(template, vars, encode, what, guards = false) {
|
|
1929
|
+
const parts = checkValue(template, vars, what, guards);
|
|
1930
|
+
if (failedGuard(parts, vars))
|
|
1931
|
+
return "";
|
|
1932
|
+
const hint = (name) => guards ? ` To leave the ${what} empty instead, guard it with {?${name}}.` : "";
|
|
1933
|
+
return render(parts, vars, encode, (name) => `ce: {${name}} has no value here, in the ${what} ${JSON.stringify(template)}.${hint(name)}`);
|
|
814
1934
|
}
|
|
815
|
-
function printBuildSummary(engine, args, summary) {
|
|
816
|
-
const command = findBuildCommand(engine, args);
|
|
817
|
-
if (!command || command.tagsOnly)
|
|
818
|
-
return;
|
|
819
|
-
const subcommand = command.name;
|
|
820
|
-
const out = ["[ce] build summary:", ` engine: ${engine}`, ` subcommand: ${subcommand}`];
|
|
821
|
-
const list = (title, items) => {
|
|
822
|
-
if (items.length)
|
|
823
|
-
out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
|
|
824
|
-
};
|
|
825
|
-
list("tags", summary.tags);
|
|
826
|
-
list("cache-from", summary.cacheFrom);
|
|
827
|
-
list("cache-to", summary.cacheTo);
|
|
828
|
-
if (summary.dockerfile)
|
|
829
|
-
out.push(` dockerfile: ${summary.dockerfile}`);
|
|
830
|
-
if (summary.target)
|
|
831
|
-
out.push(` target: ${summary.target}`);
|
|
832
|
-
if (summary.platform)
|
|
833
|
-
out.push(` platform: ${summary.platform}`);
|
|
834
|
-
list("build args", summary.buildArgs);
|
|
835
|
-
list("labels", summary.labels);
|
|
836
|
-
if (summary.context)
|
|
837
|
-
out.push(` context: ${summary.context}`);
|
|
838
|
-
io.stderr(`${out.join(`
|
|
839
|
-
`)}
|
|
840
1935
|
|
|
841
|
-
|
|
1936
|
+
// src/tags.ts
|
|
1937
|
+
function describeSkippedTag({
|
|
1938
|
+
scope,
|
|
1939
|
+
template,
|
|
1940
|
+
guard,
|
|
1941
|
+
reason
|
|
1942
|
+
}) {
|
|
1943
|
+
return `${scope} ${JSON.stringify(template)}: ${guard} did not pass (${reason})`;
|
|
842
1944
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
var
|
|
846
|
-
var
|
|
847
|
-
var
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
headers = { Accept: "application/json", Authorization: `bearer ${requestToken}` };
|
|
859
|
-
new Headers(headers);
|
|
860
|
-
} catch {
|
|
861
|
-
throw new CeError("token request URL or headers are invalid");
|
|
862
|
-
}
|
|
863
|
-
let body;
|
|
864
|
-
try {
|
|
865
|
-
const response = await fetch(tokenUrl, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
866
|
-
if (!response.ok)
|
|
867
|
-
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
868
|
-
body = await response.arrayBuffer();
|
|
869
|
-
} catch (exc) {
|
|
870
|
-
throw new CeError(`token request failed: ${errorMessage(exc)}`);
|
|
871
|
-
}
|
|
872
|
-
let payload;
|
|
873
|
-
try {
|
|
874
|
-
payload = JSON.parse(decodeUtf8(body));
|
|
875
|
-
} catch {
|
|
876
|
-
throw new CeError("token response is not valid UTF-8 JSON");
|
|
877
|
-
}
|
|
878
|
-
if (!isObject(payload))
|
|
879
|
-
throw new CeError("token response is not a JSON object");
|
|
880
|
-
const jwt = payload.value;
|
|
881
|
-
if (typeof jwt !== "string" || !jwt.trim()) {
|
|
882
|
-
throw new CeError("token response has no non-empty string 'value'");
|
|
1945
|
+
var INVALID_TAG_CHARS = /[^A-Za-z0-9_.\-]/gu;
|
|
1946
|
+
var INVALID_SCOPE_CHARS = /[^A-Za-z0-9_.:/\-]/gu;
|
|
1947
|
+
var VALID_TAG = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/u;
|
|
1948
|
+
var HOST_PORT = /^([^/:]+):(?:\d|\{[^{}]*\})+\//u;
|
|
1949
|
+
var SEPARATORS = "-_.";
|
|
1950
|
+
function sanitizeChars(value, charRegex) {
|
|
1951
|
+
return value.replace(charRegex, "_");
|
|
1952
|
+
}
|
|
1953
|
+
function assertAscii(label, value) {
|
|
1954
|
+
for (const ch of value) {
|
|
1955
|
+
const code = ch.codePointAt(0);
|
|
1956
|
+
if (code >= 128) {
|
|
1957
|
+
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
1958
|
+
throw new CeError(`ce: refusing to sanitize non-ASCII character ${JSON.stringify(ch)} ` + `(U+${hex}) in ${label} ${JSON.stringify(value)} — rename the ref ` + `or pass an explicit ASCII tag.`);
|
|
1959
|
+
}
|
|
883
1960
|
}
|
|
884
|
-
return jwt.trim();
|
|
885
1961
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1962
|
+
function stripProto(value) {
|
|
1963
|
+
return value.replace(/^https?:\/\//u, "");
|
|
1964
|
+
}
|
|
1965
|
+
function splitOnce(value, sep) {
|
|
1966
|
+
const i = value.indexOf(sep);
|
|
1967
|
+
return i < 0 ? [value, ""] : [value.slice(0, i), value.slice(i + sep.length)];
|
|
1968
|
+
}
|
|
1969
|
+
function splitRef(value) {
|
|
1970
|
+
const host = HOST_PORT.exec(value);
|
|
1971
|
+
const isHost = host && (host[1].includes(".") || host[1] === "localhost" || value.includes(":", host[0].length));
|
|
1972
|
+
const offset = isHost ? host[0].length : 0;
|
|
1973
|
+
const i = value.indexOf(":", offset);
|
|
1974
|
+
return i < 0 ? null : [value.slice(0, i), value.slice(i + 1)];
|
|
1975
|
+
}
|
|
1976
|
+
function assertValidTag(repo, tag) {
|
|
1977
|
+
if (!VALID_TAG.test(tag)) {
|
|
1978
|
+
throw new CeError(`ce: invalid tag ${JSON.stringify(tag)} in ${JSON.stringify(`${repo}:${tag}`)}: a tag is up to 128 letters, ` + `digits, "_", "." and "-", and cannot start with "." or "-".`);
|
|
895
1979
|
}
|
|
896
|
-
|
|
897
|
-
if (typeof expiry !== "number")
|
|
898
|
-
throw new CeError("OIDC token has no numeric 'exp' claim");
|
|
899
|
-
return expiry;
|
|
1980
|
+
return `${repo}:${tag}`;
|
|
900
1981
|
}
|
|
901
|
-
function
|
|
902
|
-
|
|
1982
|
+
function encodeTagValue(value) {
|
|
1983
|
+
assertAscii("placeholder value", value);
|
|
1984
|
+
return value.replace(INVALID_TAG_CHARS, "-");
|
|
903
1985
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
var STALE_AFTER_MS = 120000;
|
|
915
|
-
var POLL_MS = 50;
|
|
916
|
-
function snapshot(lockPath) {
|
|
917
|
-
try {
|
|
918
|
-
const { ino, mtimeMs } = fs3.statSync(lockPath);
|
|
919
|
-
return { ino, mtimeMs, owner: fs3.readFileSync(lockPath, "utf8").trim() };
|
|
920
|
-
} catch (exc) {
|
|
921
|
-
if (isErrno(exc, "ENOENT"))
|
|
922
|
-
return null;
|
|
923
|
-
throw exc;
|
|
1986
|
+
function encodeRepoValue(value) {
|
|
1987
|
+
assertAscii("placeholder value", value);
|
|
1988
|
+
if (/[^A-Za-z0-9._:/-]/u.test(value))
|
|
1989
|
+
throw new CeError(`ce: ${JSON.stringify(value)} cannot go in an image name`);
|
|
1990
|
+
return value;
|
|
1991
|
+
}
|
|
1992
|
+
function expandImage(template, vars) {
|
|
1993
|
+
const image = expandValue(template, vars, encodeRepoValue, "image name");
|
|
1994
|
+
if (!/^(?:[^/:]+:\d+\/)?[^:]*$/u.test(image)) {
|
|
1995
|
+
throw new CeError(`ce: image name ${JSON.stringify(image)} from ${JSON.stringify(template)} has a ":" that is not a registry port`);
|
|
924
1996
|
}
|
|
1997
|
+
return image;
|
|
925
1998
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
process.kill(Number(match[1]), 0);
|
|
934
|
-
return false;
|
|
935
|
-
} catch (exc) {
|
|
936
|
-
return !isErrno(exc, "EPERM");
|
|
1999
|
+
function expandPrefix(prefix, vars, what) {
|
|
2000
|
+
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what, true) : prefix;
|
|
2001
|
+
}
|
|
2002
|
+
function literalRef(repo, tag) {
|
|
2003
|
+
if (tag.includes("/")) {
|
|
2004
|
+
const [first, rest] = splitOnce(tag, "/");
|
|
2005
|
+
return `${repo}/${sanitizeChars(first, INVALID_TAG_CHARS)}:${sanitizeChars(rest, INVALID_TAG_CHARS)}`;
|
|
937
2006
|
}
|
|
2007
|
+
return `${repo}:${sanitizeChars(tag, INVALID_TAG_CHARS)}`;
|
|
938
2008
|
}
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
2009
|
+
function computeSafePrefix(prefix) {
|
|
2010
|
+
assertAscii("tag prefix", prefix);
|
|
2011
|
+
const p = sanitizeChars(prefix, INVALID_TAG_CHARS);
|
|
2012
|
+
return p && !SEPARATORS.includes(p.at(-1)) ? `${p}-` : p;
|
|
2013
|
+
}
|
|
2014
|
+
function rewriteTagValue(value, tagPrefix, vars = new CiMetadata, { tagLists = new Map, onSkip } = {}) {
|
|
2015
|
+
assertAscii("tag", value);
|
|
2016
|
+
const guards = /^(?:\{[?!][^{}]*\})+/u.exec(value)?.[0] ?? "";
|
|
2017
|
+
const v = stripProto(value.slice(guards.length));
|
|
2018
|
+
if (!v)
|
|
2019
|
+
throw new CeError("ce: empty -t/--tag value");
|
|
2020
|
+
checkTemplate(guards, vars);
|
|
2021
|
+
const [repoTemplate, tagList] = splitRef(v) ?? [v, undefined];
|
|
2022
|
+
if (hasPlaceholders(repoTemplate))
|
|
2023
|
+
checkValue(repoTemplate, vars, "image name");
|
|
2024
|
+
if (hasPlaceholders(tagPrefix))
|
|
2025
|
+
checkValue(tagPrefix, vars, "tag prefix", true);
|
|
2026
|
+
let repo;
|
|
2027
|
+
const expandRepo = () => repo ??= hasPlaceholders(repoTemplate) ? expandImage(repoTemplate, vars) : repoTemplate;
|
|
2028
|
+
if (tagList === undefined) {
|
|
2029
|
+
if (!guardsPass(guards, vars, (failure) => onSkip?.({ ...failure, template: value, scope: "destination" })))
|
|
2030
|
+
return [];
|
|
2031
|
+
return [expandRepo()];
|
|
954
2032
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
2033
|
+
const tags = expandTagLists(tagList, tagLists);
|
|
2034
|
+
if (!tags.length)
|
|
2035
|
+
throw new CeError(`ce: no tag after the ":" in ${JSON.stringify(value)}`);
|
|
2036
|
+
for (const tag of tags) {
|
|
2037
|
+
assertAscii("tag", tag);
|
|
2038
|
+
checkTemplate(tag, vars);
|
|
2039
|
+
}
|
|
2040
|
+
let safePrefix;
|
|
2041
|
+
const refs = [];
|
|
2042
|
+
for (const tag of tags) {
|
|
2043
|
+
let destinationSkipped = false;
|
|
2044
|
+
const expanded = guards || hasPlaceholders(tag) ? expand(guards + tag, vars, encodeTagValue, (failure) => {
|
|
2045
|
+
destinationSkipped = guards.includes(failure.guard);
|
|
2046
|
+
onSkip?.({
|
|
2047
|
+
...failure,
|
|
2048
|
+
template: destinationSkipped ? value : `${repoTemplate}:${tag}`,
|
|
2049
|
+
scope: destinationSkipped ? "destination" : "tag"
|
|
2050
|
+
});
|
|
2051
|
+
}) : tag;
|
|
2052
|
+
if (destinationSkipped)
|
|
2053
|
+
return [];
|
|
2054
|
+
if (expanded === null)
|
|
2055
|
+
continue;
|
|
2056
|
+
safePrefix ??= computeSafePrefix(expandPrefix(tagPrefix, vars, "tag prefix"));
|
|
2057
|
+
if (hasPlaceholders(tag)) {
|
|
2058
|
+
refs.push(assertValidTag(expandRepo(), `${safePrefix}${sanitizeChars(expanded, INVALID_TAG_CHARS)}`));
|
|
2059
|
+
} else {
|
|
2060
|
+
const ref = literalRef(expandRepo(), `${safePrefix}${tag}`);
|
|
2061
|
+
const i = ref.lastIndexOf(":");
|
|
2062
|
+
refs.push(assertValidTag(ref.slice(0, i), ref.slice(i + 1)));
|
|
959
2063
|
}
|
|
960
|
-
} finally {
|
|
961
|
-
fs3.rmSync(guard, { force: true });
|
|
962
2064
|
}
|
|
2065
|
+
return refs;
|
|
963
2066
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
2067
|
+
function csvFields(value) {
|
|
2068
|
+
value = value.replace(/\r?\n$/u, "");
|
|
2069
|
+
if (!value)
|
|
2070
|
+
return null;
|
|
2071
|
+
const fields = [];
|
|
2072
|
+
let i = 0;
|
|
967
2073
|
for (;; ) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
2074
|
+
if (value[i] !== '"') {
|
|
2075
|
+
const end = value.indexOf(",", i);
|
|
2076
|
+
const field = end < 0 ? value.slice(i) : value.slice(i, end);
|
|
2077
|
+
if (field.includes('"'))
|
|
2078
|
+
return null;
|
|
2079
|
+
fields.push(field);
|
|
2080
|
+
if (end < 0)
|
|
2081
|
+
return fields;
|
|
2082
|
+
i = end + 1;
|
|
976
2083
|
continue;
|
|
977
2084
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
2085
|
+
let field = "";
|
|
2086
|
+
for (i++;; i++) {
|
|
2087
|
+
const quote = value.indexOf('"', i);
|
|
2088
|
+
if (quote < 0)
|
|
2089
|
+
return null;
|
|
2090
|
+
field += value.slice(i, quote);
|
|
2091
|
+
i = quote + 1;
|
|
2092
|
+
if (value[i] !== '"')
|
|
2093
|
+
break;
|
|
2094
|
+
field += '"';
|
|
984
2095
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
if (current?.ino === ino && current.owner === owner)
|
|
992
|
-
fs3.rmSync(lockPath, { force: true });
|
|
2096
|
+
fields.push(field);
|
|
2097
|
+
if (i === value.length)
|
|
2098
|
+
return fields;
|
|
2099
|
+
if (value[i] !== ",")
|
|
2100
|
+
return null;
|
|
2101
|
+
i++;
|
|
993
2102
|
}
|
|
994
2103
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
var HELPER_SUFFIX = "ce-forgejo";
|
|
998
|
-
var STATE_FILE = "ce-forgejo-credentials.json";
|
|
999
|
-
function dockerConfigDir() {
|
|
1000
|
-
const configured = process.env.DOCKER_CONFIG;
|
|
1001
|
-
if (!configured)
|
|
1002
|
-
return path3.join(homedir(), ".docker");
|
|
1003
|
-
const expanded = configured === "~" || configured.startsWith("~/") ? path3.join(homedir(), configured.slice(1)) : configured;
|
|
1004
|
-
return path3.resolve(expanded);
|
|
1005
|
-
}
|
|
1006
|
-
function helperStatePath() {
|
|
1007
|
-
return path3.join(dockerConfigDir(), STATE_FILE);
|
|
2104
|
+
function csvJoin(fields) {
|
|
2105
|
+
return fields.map((field) => /^[ \t]|[",\r\n]/u.test(field) ? `"${field.replaceAll('"', '""')}"` : field).join(",");
|
|
1008
2106
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
if (!isObject(state) || !isObject(state.registries)) {
|
|
1015
|
-
throw new CeError("Forgejo credential-helper state is invalid");
|
|
1016
|
-
}
|
|
1017
|
-
const before = JSON.stringify(state);
|
|
1018
|
-
const result = await fn(state);
|
|
1019
|
-
if (JSON.stringify(state) !== before)
|
|
1020
|
-
writePrivateJson(statePath, state);
|
|
1021
|
-
return result;
|
|
1022
|
-
});
|
|
2107
|
+
var fieldKey = (field) => field.split("=", 1)[0].toLowerCase();
|
|
2108
|
+
function ghaFields(value) {
|
|
2109
|
+
const fields = csvFields(value);
|
|
2110
|
+
const type = fields?.findLast((field) => fieldKey(field) === "type");
|
|
2111
|
+
return type?.slice("type=".length) === "gha" ? fields : null;
|
|
1023
2112
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
});
|
|
2113
|
+
var isGhaCache = (value) => ghaFields(value) !== null;
|
|
2114
|
+
var scopeIndex = (fields) => fields.findLastIndex((field) => fieldKey(field) === "scope");
|
|
2115
|
+
function cacheScopeName(value) {
|
|
2116
|
+
const fields = ghaFields(value);
|
|
2117
|
+
if (!fields)
|
|
2118
|
+
return value;
|
|
2119
|
+
const i = scopeIndex(fields);
|
|
2120
|
+
if (i < 0)
|
|
2121
|
+
return "";
|
|
2122
|
+
const scope = fields[i].slice("scope=".length);
|
|
2123
|
+
assertAscii("cache scope", scope);
|
|
2124
|
+
return sanitizeChars(scope, INVALID_SCOPE_CHARS);
|
|
1037
2125
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
for (const key of Object.keys(auths)) {
|
|
1052
|
-
if (registryKey(key) === registryKey(registry))
|
|
1053
|
-
delete auths[key];
|
|
1054
|
-
}
|
|
2126
|
+
function normalizeCacheScope(value, cachePrefix) {
|
|
2127
|
+
const fields = ghaFields(value);
|
|
2128
|
+
if (!fields)
|
|
2129
|
+
return value;
|
|
2130
|
+
if (!cachePrefix)
|
|
2131
|
+
return value;
|
|
2132
|
+
assertAscii("cache prefix", cachePrefix);
|
|
2133
|
+
const prefix = sanitizeChars(cachePrefix, INVALID_TAG_CHARS);
|
|
2134
|
+
let scope = cacheScopeName(value);
|
|
2135
|
+
if (!scope) {
|
|
2136
|
+
scope = prefix.replace(/[-_.]+$/u, "");
|
|
2137
|
+
} else if (prefix) {
|
|
2138
|
+
scope = SEPARATORS.includes(prefix.at(-1)) ? `${prefix}${scope}` : `${prefix}-${scope}`;
|
|
1055
2139
|
}
|
|
1056
|
-
|
|
2140
|
+
const i = scopeIndex(fields);
|
|
2141
|
+
if (i < 0)
|
|
2142
|
+
fields.push(`scope=${scope}`);
|
|
2143
|
+
else
|
|
2144
|
+
fields[i] = `scope=${scope}`;
|
|
2145
|
+
return csvJoin(fields);
|
|
2146
|
+
}
|
|
2147
|
+
function withoutCacheMode(value) {
|
|
2148
|
+
const fields = ghaFields(value);
|
|
2149
|
+
if (!fields?.some((field) => fieldKey(field) === "mode"))
|
|
2150
|
+
return value;
|
|
2151
|
+
return csvJoin(fields.filter((field) => fieldKey(field) !== "mode"));
|
|
1057
2152
|
}
|
|
1058
2153
|
|
|
1059
|
-
// src/
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
2154
|
+
// src/build-args.ts
|
|
2155
|
+
function consumeValue(argv, flag) {
|
|
2156
|
+
if (argv.length === 0)
|
|
2157
|
+
throw new CeError(`ce: ${flag} requires a value but none was provided.`);
|
|
2158
|
+
const v = argv[0];
|
|
2159
|
+
if (v.length > 1 && v.startsWith("-")) {
|
|
2160
|
+
throw new CeError(`ce: ${flag} requires a value but the next token ${JSON.stringify(v)} looks like a flag.`);
|
|
2161
|
+
}
|
|
2162
|
+
return [v, argv.slice(1)];
|
|
2163
|
+
}
|
|
2164
|
+
var SUMMARIZED = {
|
|
2165
|
+
"-f": "dockerfile",
|
|
2166
|
+
"--file": "dockerfile",
|
|
2167
|
+
"--target": "target",
|
|
2168
|
+
"--platform": "platform",
|
|
2169
|
+
"--build-arg": "buildArgs",
|
|
2170
|
+
"--label": "labels",
|
|
2171
|
+
"--annotation": "annotations"
|
|
2172
|
+
};
|
|
2173
|
+
var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
|
|
2174
|
+
var PLATFORM_FROM = {
|
|
2175
|
+
podman: [5, 2],
|
|
2176
|
+
buildah: [1, 37]
|
|
2177
|
+
};
|
|
2178
|
+
var PLATFORM_PARTS = [
|
|
2179
|
+
"--arch",
|
|
2180
|
+
"--os",
|
|
2181
|
+
"--variant",
|
|
2182
|
+
"--override-arch",
|
|
2183
|
+
"--override-os",
|
|
2184
|
+
"--os-version",
|
|
2185
|
+
"--os-feature"
|
|
2186
|
+
];
|
|
2187
|
+
function withBuildArgs(vars, values, argFile, fromFile) {
|
|
2188
|
+
return {
|
|
2189
|
+
known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
|
|
2190
|
+
get(name) {
|
|
2191
|
+
const arg = ARG_VARIABLE.exec(name)?.[1];
|
|
2192
|
+
if (arg === undefined)
|
|
2193
|
+
return vars.get(name);
|
|
2194
|
+
const value = values.get(arg);
|
|
2195
|
+
if (typeof value === "string")
|
|
2196
|
+
return value || undefined;
|
|
2197
|
+
if (argFile) {
|
|
2198
|
+
throw new CeError(`ce: {${name}} has no value from --build-arg, and may get one from --build-arg-file, which ce does not read`);
|
|
2199
|
+
}
|
|
2200
|
+
return fromFile(arg) || undefined;
|
|
1081
2201
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
var SHORT_BOOLS = {
|
|
2205
|
+
docker: "qD",
|
|
2206
|
+
podman: "qD",
|
|
2207
|
+
buildah: "qD"
|
|
2208
|
+
};
|
|
2209
|
+
var BUILDX_VALUES = "add-host allow annotation attest build-arg build-context builder cache-from cache-to call cgroup-parent cpu-period cpu-quota cpu-shares cpuset-cpus cpuset-mems file iidfile isolation label memory memory-swap metadata-file network no-cache-filter output platform policy print progress provenance resource sbom secret security-opt shm-size ssh tag target ulimit";
|
|
2210
|
+
var BUILDAH_VALUES = "add-host annotation arch authfile blob-cache blob-info-cache-dir build-arg build-arg-file build-context cache-from cache-to cache-ttl cap-add cap-drop cdi-config-dir cert-dir cgroup-parent cgroupns compression-format compression-level cpp-flag cpu-period cpu-quota cpu-shares cpuset-cpus cpuset-mems creds cw decryption-key device dns dns-option dns-search env file format from group-add hooks-dir ignorefile iidfile iidfile-raw ipc isolation jobs label layer-label logfile loglevel manifest memory memory-swap metadata-file mount net network os os-feature os-version output override-arch override-os pid platform progress raw-iidfile retry retry-delay runtime-flag rusage-logfile sbom sbom-image-output sbom-image-purl-output sbom-merge-strategy sbom-output sbom-purl-output sbom-scanner-command sbom-scanner-image secret security-opt shm-size sign-by signature-policy source-date-epoch source-policy-file ssh tag target timestamp ulimit unsetannotation unsetenv unsetlabel userns userns-gid-map userns-gid-map-group userns-uid-map userns-uid-map-user uts variant volume";
|
|
2211
|
+
var PODMAN_ROOT_VALUES = "cdi-spec-dir cgroup-manager conmon cpu-profile db-backend default-mounts-file events-backend hooks-dir imagestore log-level max-workers memory-profile namespace network-backend network-cmd-path network-config-dir pull-option registries-conf root runroot runtime runtime-flag storage-driver storage-opt tmpdir volumepath";
|
|
2212
|
+
var BUILDAH_ROOT_VALUES = "cgroup-manager cpu-profile default-mounts-file imagestore log-level memory-profile registries-conf registries-conf-dir root runroot short-name-alias-conf storage-driver storage-opt userns-gid-map userns-uid-map";
|
|
2213
|
+
var VALUE_FLAGS = {
|
|
2214
|
+
docker: new Set(BUILDX_VALUES.split(" ")),
|
|
2215
|
+
podman: new Set(`${BUILDAH_VALUES} ${PODMAN_ROOT_VALUES}`.split(" ")),
|
|
2216
|
+
buildah: new Set(`${BUILDAH_VALUES} runtime ${BUILDAH_ROOT_VALUES}`.split(" "))
|
|
2217
|
+
};
|
|
2218
|
+
var SHORT_VALUES = {
|
|
2219
|
+
docker: "cfmot",
|
|
2220
|
+
podman: "cfmotv",
|
|
2221
|
+
buildah: "cfmotv"
|
|
2222
|
+
};
|
|
2223
|
+
var takesValue = (arg, engine) => arg.startsWith("--") ? VALUE_FLAGS[engine].has(arg.slice(2)) : arg.length === 2 && SHORT_VALUES[engine].includes(arg[1]);
|
|
2224
|
+
function ungroup(argv, engine) {
|
|
2225
|
+
const out = [];
|
|
2226
|
+
for (let at = 0;at < argv.length; at++) {
|
|
2227
|
+
const arg = argv[at];
|
|
2228
|
+
if (arg === "--") {
|
|
2229
|
+
out.push(...argv.slice(at));
|
|
2230
|
+
break;
|
|
1084
2231
|
}
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
2232
|
+
const split = [];
|
|
2233
|
+
let i = 1;
|
|
2234
|
+
if (/^-[^-]/u.test(arg))
|
|
2235
|
+
while (i < arg.length - 1 && SHORT_BOOLS[engine].includes(arg[i]) && arg[i + 1] !== "=")
|
|
2236
|
+
split.push(`-${arg[i++]}`);
|
|
2237
|
+
split.push(i === 1 ? arg : `-${arg.slice(i)}`);
|
|
2238
|
+
out.push(...split);
|
|
2239
|
+
if (takesValue(split.at(-1), engine) && at + 1 < argv.length)
|
|
2240
|
+
out.push(argv[++at]);
|
|
2241
|
+
}
|
|
2242
|
+
return out;
|
|
2243
|
+
}
|
|
2244
|
+
function parseBuildArgs(given, tagPrefix, cachePrefix, {
|
|
2245
|
+
vars = new CiMetadata,
|
|
2246
|
+
tagLists,
|
|
2247
|
+
labels = {},
|
|
2248
|
+
defaultBranch,
|
|
2249
|
+
env = process.env,
|
|
2250
|
+
engine = "docker",
|
|
2251
|
+
buildx = false,
|
|
2252
|
+
engineVersion,
|
|
2253
|
+
sbom = false
|
|
2254
|
+
} = {}) {
|
|
2255
|
+
const argv = ungroup(given, engine);
|
|
2256
|
+
const parts = [];
|
|
2257
|
+
const rawCacheFrom = [];
|
|
2258
|
+
const rawCacheTo = [];
|
|
2259
|
+
const summary = {
|
|
2260
|
+
tags: [],
|
|
2261
|
+
skippedTags: [],
|
|
2262
|
+
cacheFrom: [],
|
|
2263
|
+
cacheTo: [],
|
|
2264
|
+
buildArgs: [],
|
|
2265
|
+
labels: [],
|
|
2266
|
+
annotations: [],
|
|
2267
|
+
dockerfile: "",
|
|
2268
|
+
target: "",
|
|
2269
|
+
platform: "",
|
|
2270
|
+
context: ""
|
|
2271
|
+
};
|
|
2272
|
+
let templated = false;
|
|
2273
|
+
const buildArgs = new Map;
|
|
2274
|
+
let argFile = false;
|
|
2275
|
+
const files = [];
|
|
2276
|
+
const positionals = [];
|
|
2277
|
+
const contexts = [];
|
|
2278
|
+
const envs = [];
|
|
2279
|
+
const platforms = [];
|
|
2280
|
+
let platformParts = false;
|
|
2281
|
+
let fromImage;
|
|
2282
|
+
let format = env["BUILDAH_FORMAT"];
|
|
2283
|
+
const sbomFlags = [];
|
|
2284
|
+
const times = new Map;
|
|
2285
|
+
let epochArg;
|
|
2286
|
+
let allPlatforms = false;
|
|
2287
|
+
let rest = [];
|
|
2288
|
+
const addTags = (value, flag) => {
|
|
2289
|
+
templated ||= hasPlaceholders(value);
|
|
2290
|
+
parts.push({ tags: value, flag });
|
|
2291
|
+
};
|
|
2292
|
+
const summarize = (flag, value) => {
|
|
2293
|
+
const field = SUMMARIZED[flag];
|
|
2294
|
+
if (field === "buildArgs") {
|
|
2295
|
+
const [key, given] = value.includes("=") ? splitAt(value, "=") : [value, Object.hasOwn(env, value) ? env[value] : undefined];
|
|
2296
|
+
if (given !== undefined)
|
|
2297
|
+
buildArgs.set(key, given);
|
|
2298
|
+
else if (engine !== "docker")
|
|
2299
|
+
buildArgs.set(value, null);
|
|
2300
|
+
if (key === "SOURCE_DATE_EPOCH" && given !== undefined)
|
|
2301
|
+
epochArg = given;
|
|
2302
|
+
else if (key === "SOURCE_DATE_EPOCH" && engine !== "docker")
|
|
2303
|
+
epochArg = undefined;
|
|
2304
|
+
if (engine !== "docker" && buildArgs.has("TARGETPLATFORM"))
|
|
2305
|
+
buildArgs.set("TARGETPLATFORM", null);
|
|
2306
|
+
const epoch = buildArgs.get("SOURCE_DATE_EPOCH");
|
|
2307
|
+
if (engine === "docker" && typeof epoch === "string" && !(/^(?:0|-?[1-9][0-9]*)$/u.test(epoch) && BigInt(epoch) >= -(2n ** 63n) && BigInt(epoch) < 2n ** 63n))
|
|
2308
|
+
buildArgs.set("SOURCE_DATE_EPOCH", null);
|
|
1088
2309
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
if (
|
|
1092
|
-
|
|
2310
|
+
if (field === "dockerfile")
|
|
2311
|
+
files.push(value);
|
|
2312
|
+
if (field === "platform") {
|
|
2313
|
+
platforms.push(...value.split(","));
|
|
2314
|
+
summary.platform = platforms.join(",");
|
|
2315
|
+
} else if (field === "buildArgs" || field === "labels" || field === "annotations")
|
|
2316
|
+
summary[field].push(value);
|
|
2317
|
+
else
|
|
2318
|
+
summary[field] = value;
|
|
2319
|
+
};
|
|
2320
|
+
for (let i = 0;i < argv.length; i++) {
|
|
2321
|
+
const arg = argv[i];
|
|
2322
|
+
const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
|
|
2323
|
+
const next = () => consumeValue(argv.slice(++i), arg)[0];
|
|
2324
|
+
const short = () => arg.slice(arg[2] === "=" ? 3 : 2);
|
|
2325
|
+
if (engine !== "docker" && PLATFORM_PARTS.includes(name))
|
|
2326
|
+
platformParts = true;
|
|
2327
|
+
if (engine !== "docker" && name === "--format")
|
|
2328
|
+
format = attached ?? argv[i + 1];
|
|
2329
|
+
if (engine !== "docker" && name === "--all-platforms")
|
|
2330
|
+
allPlatforms = attached === undefined || !/^(?:0|f|false)$/iu.test(attached);
|
|
2331
|
+
if (engine !== "docker" && name === "--from")
|
|
2332
|
+
fromImage = attached ?? argv[i + 1] ?? "";
|
|
2333
|
+
if (name.startsWith("--sbom") || name === "--attest")
|
|
2334
|
+
sbomFlags.push([name, attached ?? argv[i + 1] ?? ""]);
|
|
2335
|
+
if (engine !== "docker" && (name === "--timestamp" || name === "--source-date-epoch"))
|
|
2336
|
+
times.set(name, attached ?? argv[i + 1] ?? "");
|
|
2337
|
+
if (arg === "--") {
|
|
2338
|
+
rest = argv.slice(i);
|
|
2339
|
+
positionals.push(...rest.slice(1));
|
|
2340
|
+
if (rest.length > 1)
|
|
2341
|
+
summary.context = rest.at(-1);
|
|
2342
|
+
break;
|
|
2343
|
+
} else if (arg === "-t")
|
|
2344
|
+
addTags(next(), "-t");
|
|
2345
|
+
else if (arg === "--tag")
|
|
2346
|
+
addTags(next(), "--tag=");
|
|
2347
|
+
else if (arg.startsWith("-t"))
|
|
2348
|
+
addTags(short(), "-t");
|
|
2349
|
+
else if (name === "--tag")
|
|
2350
|
+
addTags(attached, "--tag=");
|
|
2351
|
+
else if (name === "--cache-from")
|
|
2352
|
+
rawCacheFrom.push(attached ?? next());
|
|
2353
|
+
else if (name === "--cache-to")
|
|
2354
|
+
rawCacheTo.push(attached ?? next());
|
|
2355
|
+
else if (name === "--cache") {
|
|
2356
|
+
const v = attached ?? next();
|
|
2357
|
+
rawCacheFrom.push(withoutCacheMode(v));
|
|
2358
|
+
rawCacheTo.push(v);
|
|
2359
|
+
} else if (Object.hasOwn(SUMMARIZED, name)) {
|
|
2360
|
+
const flag = name;
|
|
2361
|
+
if (attached === undefined) {
|
|
2362
|
+
const v = next();
|
|
2363
|
+
parts.push(arg, v);
|
|
2364
|
+
summarize(flag, v);
|
|
2365
|
+
} else {
|
|
2366
|
+
parts.push(arg);
|
|
2367
|
+
summarize(flag, attached);
|
|
2368
|
+
}
|
|
2369
|
+
} else if (name === "--build-arg-file") {
|
|
2370
|
+
argFile = true;
|
|
2371
|
+
parts.push(arg);
|
|
2372
|
+
if (attached === undefined)
|
|
2373
|
+
parts.push(next());
|
|
2374
|
+
} else if (name === "--build-context") {
|
|
2375
|
+
const v = attached ?? next();
|
|
2376
|
+
parts.push(arg);
|
|
2377
|
+
if (attached === undefined)
|
|
2378
|
+
parts.push(v);
|
|
2379
|
+
contexts.push(v.split("=")[0]);
|
|
2380
|
+
} else if ((name === "--env" || name === "--unsetenv") && engine !== "docker") {
|
|
2381
|
+
const v = attached ?? next();
|
|
2382
|
+
parts.push(arg);
|
|
2383
|
+
if (attached === undefined)
|
|
2384
|
+
parts.push(v);
|
|
2385
|
+
envs.push(`${name} ${v}`);
|
|
2386
|
+
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
2387
|
+
parts.push(arg);
|
|
2388
|
+
summary.dockerfile = short();
|
|
2389
|
+
files.push(summary.dockerfile);
|
|
2390
|
+
} else if (takesValue(arg, engine) && i + 1 < argv.length) {
|
|
2391
|
+
parts.push(arg, argv[++i]);
|
|
2392
|
+
} else {
|
|
2393
|
+
parts.push(arg);
|
|
2394
|
+
if (!arg.startsWith("-")) {
|
|
2395
|
+
positionals.push(arg);
|
|
2396
|
+
summary.context = arg;
|
|
2397
|
+
}
|
|
1093
2398
|
}
|
|
1094
|
-
entry.token = refreshed;
|
|
1095
|
-
entry.expires_at = refreshedExpiry;
|
|
1096
|
-
return { Username: username, Secret: refreshed };
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
async function helperStore(input) {
|
|
1100
|
-
const payload = JSON.parse(input);
|
|
1101
|
-
if (!isObject(payload))
|
|
1102
|
-
throw new CeError("credential-helper store payload is invalid");
|
|
1103
|
-
const { ServerURL: registry, Username: username, Secret: secret } = payload;
|
|
1104
|
-
const filled = (value) => typeof value === "string" && value !== "";
|
|
1105
|
-
if (!(filled(registry) && filled(username) && filled(secret))) {
|
|
1106
|
-
throw new CeError("credential-helper store fields are incomplete");
|
|
1107
2399
|
}
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
2400
|
+
const whyIgnored = () => {
|
|
2401
|
+
if (allPlatforms)
|
|
2402
|
+
return `--all-platforms builds for the platforms of the base images, which ce does not read`;
|
|
2403
|
+
if (platformParts)
|
|
2404
|
+
return `${engine}'s --arch, --os or --variant changes the platform, which ce does not follow`;
|
|
2405
|
+
const from = PLATFORM_FROM[engine];
|
|
2406
|
+
if (!from || !platforms.length)
|
|
2407
|
+
return;
|
|
2408
|
+
const since = `ce follows how ${engine} sets it from --platform only from ${from.join(".")}`;
|
|
2409
|
+
const version = engineVersion?.();
|
|
2410
|
+
if (version === undefined)
|
|
2411
|
+
return `${since}, and cannot tell which version builds`;
|
|
2412
|
+
return older(version, from) ? `${since}, and this is ${version}` : undefined;
|
|
2413
|
+
};
|
|
2414
|
+
let ignored;
|
|
2415
|
+
const platformIgnored = () => (ignored ??= { why: whyIgnored() }).why;
|
|
2416
|
+
const baseVars = vars;
|
|
2417
|
+
const buildFiles = {
|
|
2418
|
+
engine,
|
|
2419
|
+
files,
|
|
2420
|
+
positionals,
|
|
2421
|
+
target: summary.target,
|
|
2422
|
+
contexts,
|
|
2423
|
+
envs,
|
|
2424
|
+
env,
|
|
2425
|
+
platforms,
|
|
2426
|
+
platformIgnored,
|
|
2427
|
+
format: format === "oci" || format === "docker" ? format : undefined,
|
|
2428
|
+
from: fromImage
|
|
2429
|
+
};
|
|
2430
|
+
const buildArg = (arg) => buildArgs.get(arg) ?? undefined;
|
|
2431
|
+
const fromFile = containerfileDefaults(buildFiles, buildArg);
|
|
2432
|
+
vars = withBuildArgs(baseVars, buildArgs, argFile, fromFile);
|
|
2433
|
+
const args = parts.flatMap((part) => {
|
|
2434
|
+
if (typeof part === "string")
|
|
2435
|
+
return [part];
|
|
2436
|
+
const refs = [
|
|
2437
|
+
...new Set(rewriteTagValue(part.tags, tagPrefix, vars, {
|
|
2438
|
+
tagLists,
|
|
2439
|
+
onSkip: (skipped) => summary.skippedTags.push(skipped)
|
|
2440
|
+
}))
|
|
2441
|
+
].filter((ref) => !summary.tags.includes(ref));
|
|
2442
|
+
summary.tags.push(...refs);
|
|
2443
|
+
return refs.map((ref) => `${part.flag}${ref}`);
|
|
1112
2444
|
});
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (argv.length !== 1 || !["get", "store", "erase", "list"].includes(command)) {
|
|
1117
|
-
logError("ce: credential-helper requires get|store|erase|list");
|
|
1118
|
-
return 1;
|
|
2445
|
+
if (templated && !summary.tags.length) {
|
|
2446
|
+
throw new CeError("ce: the guards in the -t templates left out every tag, so the image would have none" + summary.skippedTags.map((skipped) => `
|
|
2447
|
+
- ${describeSkippedTag(skipped)}`).join(""));
|
|
1119
2448
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
2449
|
+
const CREATED = "org.opencontainers.image.created";
|
|
2450
|
+
const timestamp = times.get("--timestamp");
|
|
2451
|
+
const flagEpoch = timestamp ?? times.get("--source-date-epoch");
|
|
2452
|
+
let epoch;
|
|
2453
|
+
if (engine === "docker")
|
|
2454
|
+
epoch = epochArg ?? (env["SOURCE_DATE_EPOCH"] || !labels[CREATED] ? undefined : dockerfileEpoch(buildFiles, buildArg));
|
|
2455
|
+
else if (flagEpoch !== undefined || env["SOURCE_DATE_EPOCH"])
|
|
2456
|
+
epoch = flagEpoch;
|
|
2457
|
+
else
|
|
2458
|
+
epoch = epochArg ?? (argFile ? "" : undefined);
|
|
2459
|
+
const time = epoch === undefined ? undefined : epochDate(epoch, timestamp === undefined ? 10 : 0);
|
|
2460
|
+
const metadata = Object.entries(labels).filter(([key]) => key !== CREATED || epoch === undefined || time).map(([key, value]) => [
|
|
2461
|
+
key,
|
|
2462
|
+
time && key === CREATED ? rfc3339(time) : value
|
|
2463
|
+
]);
|
|
2464
|
+
const ownLabels = new Set(summary.labels.map((label) => label.split("=", 1)[0]));
|
|
2465
|
+
const ownAnnotations = new Set(summary.annotations.map((annotation) => annotationKey(annotation, engine)));
|
|
2466
|
+
const addedLabels = metadata.filter(([key, value]) => !ownLabels.has(key) && (engine === "docker" || !(value.includes("$") || engineEscapes(value)))).map(([key, value]) => `${key}=${value}`);
|
|
2467
|
+
const level = engine === "docker" && indexed(platforms) ? "manifest,index:" : "";
|
|
2468
|
+
const addedAnnotations = annotates(engine, env, buildx) ? metadata.filter(([key]) => !ownAnnotations.has(key)).map(([key, value]) => `${level}${key}=${value}`) : [];
|
|
2469
|
+
args.unshift(...addedLabels.map((label) => `--label=${label}`), ...addedAnnotations.map((annotation) => `--annotation=${annotation}`), ...sbomArgs(engine, sbom, sbomFlags));
|
|
2470
|
+
summary.labels.unshift(...addedLabels);
|
|
2471
|
+
summary.annotations.unshift(...addedAnnotations);
|
|
2472
|
+
const prefixTemplate = cachePrefix;
|
|
2473
|
+
if (rawCacheFrom.length || rawCacheTo.length)
|
|
2474
|
+
cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
|
|
2475
|
+
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
2476
|
+
const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
|
|
2477
|
+
const ghaTo = rawCacheTo.filter(isGhaCache);
|
|
2478
|
+
if (defaultBranch && ghaTo.length) {
|
|
2479
|
+
const fileArg = (arg) => {
|
|
2480
|
+
throw new CeError(`ce: the default branch's cache scope cannot use {arg.${arg}} from the Containerfile, which may differ there`);
|
|
2481
|
+
};
|
|
2482
|
+
const onDefault = withBuildArgs(onDefaultBranch(baseVars, defaultBranch()), buildArgs, argFile, fileArg);
|
|
2483
|
+
const prefix = expandPrefix(prefixTemplate, onDefault, "cache prefix");
|
|
2484
|
+
cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
|
|
2485
|
+
}
|
|
2486
|
+
for (const rewritten of new Set(cacheFrom)) {
|
|
2487
|
+
args.push(`--cache-from=${rewritten}`);
|
|
2488
|
+
summary.cacheFrom.push(rewritten);
|
|
2489
|
+
}
|
|
2490
|
+
for (const c of rawCacheTo) {
|
|
2491
|
+
const rewritten = normalizeCacheScope(c, cachePrefix);
|
|
2492
|
+
args.push(`--cache-to=${rewritten}`);
|
|
2493
|
+
summary.cacheTo.push(rewritten);
|
|
1140
2494
|
}
|
|
2495
|
+
args.push(...rest);
|
|
2496
|
+
return { args, summary, vars };
|
|
2497
|
+
}
|
|
2498
|
+
function annotationKey(annotation, engine) {
|
|
2499
|
+
const key = annotation.split("=", 1)[0];
|
|
2500
|
+
return engine === "docker" ? key.replace(/^[a-z-]+(?:\[[^\]]*\])?(?:,[a-z-]+(?:\[[^\]]*\])?)*:/u, "") : key;
|
|
2501
|
+
}
|
|
2502
|
+
var annotates = (engine, env, buildx) => engine !== "docker" || buildx || !["0", "f", "false"].includes((env["DOCKER_BUILDKIT"] ?? "").toLowerCase());
|
|
2503
|
+
function indexed(platforms) {
|
|
2504
|
+
const normalized = platforms.map((p) => normalizePlatform(p, "docker"));
|
|
2505
|
+
return normalized.every((p) => p !== undefined) && new Set(normalized).size > 1;
|
|
2506
|
+
}
|
|
2507
|
+
var SBOM_PATH = "/root/buildinfo/content_manifests/sbom";
|
|
2508
|
+
function sbomArgs(engine, sbom, flags) {
|
|
2509
|
+
if (engine === "docker") {
|
|
2510
|
+
const asked = flags.some(([name, value]) => name === "--sbom" || name === "--attest" && csvFields(value)?.findLast((field) => fieldKey(field) === "type")?.slice("type=".length) === "sbom");
|
|
2511
|
+
if (!sbom || asked)
|
|
2512
|
+
return [];
|
|
2513
|
+
if (sbom === "cyclonedx")
|
|
2514
|
+
throw new CeError("ce: SBOM=cyclonedx is not supported by Docker's BuildKit SBOM attestation; use SBOM=spdx or SBOM=true");
|
|
2515
|
+
return ["--sbom=true"];
|
|
2516
|
+
}
|
|
2517
|
+
const preset = flags.findLast(([name]) => name === "--sbom")?.[1];
|
|
2518
|
+
const output = flags.some(([name]) => [
|
|
2519
|
+
"--sbom-output",
|
|
2520
|
+
"--sbom-image-output",
|
|
2521
|
+
"--sbom-purl-output",
|
|
2522
|
+
"--sbom-image-purl-output"
|
|
2523
|
+
].includes(name));
|
|
2524
|
+
const used = preset ?? (sbom ? `syft-${sbom === "cyclonedx" ? "cyclonedx" : "spdx"}` : undefined);
|
|
2525
|
+
if (used === undefined)
|
|
2526
|
+
return [];
|
|
2527
|
+
const args = preset === undefined ? [`--sbom=${used}`] : [];
|
|
2528
|
+
if (!output)
|
|
2529
|
+
args.push(`--sbom-image-output=${SBOM_PATH}${used.endsWith("-spdx") ? ".spdx.json" : ".cdx.json"}`);
|
|
2530
|
+
return args;
|
|
2531
|
+
}
|
|
2532
|
+
function older(version, than) {
|
|
2533
|
+
const parts = /^(\d+)\.(\d+)/u.exec(version)?.slice(1).map(Number);
|
|
2534
|
+
if (!parts)
|
|
2535
|
+
return true;
|
|
2536
|
+
const i = parts.findIndex((n, j) => n !== than[j]);
|
|
2537
|
+
return i !== -1 && parts[i] < than[i];
|
|
2538
|
+
}
|
|
2539
|
+
function splitAt(value, sep) {
|
|
2540
|
+
const i = value.indexOf(sep);
|
|
2541
|
+
return [value.slice(0, i), value.slice(i + 1)];
|
|
2542
|
+
}
|
|
2543
|
+
function printBuildSummary(engine, args, summary) {
|
|
2544
|
+
const command = findBuildCommand(engine, args);
|
|
2545
|
+
if (!command || command.tagsOnly)
|
|
2546
|
+
return;
|
|
2547
|
+
const subcommand = command.name;
|
|
2548
|
+
const out = [
|
|
2549
|
+
"[ce] build summary:",
|
|
2550
|
+
` engine: ${engine}`,
|
|
2551
|
+
` subcommand: ${subcommand}`
|
|
2552
|
+
];
|
|
2553
|
+
const list = (title, items) => {
|
|
2554
|
+
if (items.length)
|
|
2555
|
+
out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
|
|
2556
|
+
};
|
|
2557
|
+
list("tags", summary.tags);
|
|
2558
|
+
list("cache-from", summary.cacheFrom);
|
|
2559
|
+
list("cache-to", summary.cacheTo);
|
|
2560
|
+
if (summary.dockerfile)
|
|
2561
|
+
out.push(` dockerfile: ${summary.dockerfile}`);
|
|
2562
|
+
if (summary.target)
|
|
2563
|
+
out.push(` target: ${summary.target}`);
|
|
2564
|
+
if (summary.platform)
|
|
2565
|
+
out.push(` platform: ${summary.platform}`);
|
|
2566
|
+
list("build args", summary.buildArgs);
|
|
2567
|
+
list("labels", summary.labels);
|
|
2568
|
+
list("annotations", summary.annotations);
|
|
2569
|
+
if (summary.context)
|
|
2570
|
+
out.push(` context: ${summary.context}`);
|
|
2571
|
+
io.stderr(`${out.join(`
|
|
2572
|
+
`)}
|
|
2573
|
+
|
|
2574
|
+
`);
|
|
1141
2575
|
}
|
|
1142
2576
|
|
|
1143
2577
|
// src/engine.ts
|
|
1144
2578
|
var isEngine = (value) => ["docker", "podman", "buildah"].includes(value);
|
|
1145
2579
|
var WAIT_ATTEMPTS = 30;
|
|
1146
|
-
async function waitForEngine(engine) {
|
|
2580
|
+
async function waitForEngine(engine, options = []) {
|
|
1147
2581
|
if (engine === "buildah")
|
|
1148
2582
|
return true;
|
|
2583
|
+
const argv = [engine, ...options, "info"];
|
|
2584
|
+
let stderr = "";
|
|
1149
2585
|
for (let attempt = 1;attempt <= WAIT_ATTEMPTS; attempt++) {
|
|
1150
|
-
|
|
2586
|
+
const result = system.run(argv);
|
|
2587
|
+
if (result.status === 0)
|
|
1151
2588
|
return true;
|
|
2589
|
+
stderr = result.stderr.trim();
|
|
1152
2590
|
logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
|
|
1153
2591
|
await system.sleep(1000);
|
|
1154
2592
|
}
|
|
1155
2593
|
logError(`no engine available: ${engine}`);
|
|
2594
|
+
if (stderr)
|
|
2595
|
+
logError(`${argv.map(shellQuote).join(" ")} last said:
|
|
2596
|
+
${stderr}`);
|
|
1156
2597
|
return false;
|
|
1157
2598
|
}
|
|
1158
|
-
function
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
2599
|
+
function engineVersion(engine, options = []) {
|
|
2600
|
+
const argv = engine === "podman" ? ["podman", ...options, "info", "--format", "{{.Version.Version}}"] : ["buildah", "--version"];
|
|
2601
|
+
for (let attempt = 1;; attempt++) {
|
|
2602
|
+
let result;
|
|
2603
|
+
try {
|
|
2604
|
+
result = system.output(argv);
|
|
2605
|
+
} catch {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (result.status === 0)
|
|
2609
|
+
return /^(?:buildah version )?(\d+\.\d+\.\d+)\b/u.exec(result.stdout.trim())?.[1];
|
|
2610
|
+
if (engine === "buildah" || attempt === WAIT_ATTEMPTS)
|
|
2611
|
+
return;
|
|
2612
|
+
logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
|
|
2613
|
+
system.sleepSync(1000);
|
|
2614
|
+
}
|
|
1165
2615
|
}
|
|
1166
|
-
|
|
2616
|
+
function sbomOption() {
|
|
2617
|
+
const value = (process.env["SBOM"] ?? "").toLowerCase();
|
|
2618
|
+
if (value === "spdx" || value === "cyclonedx")
|
|
2619
|
+
return value;
|
|
2620
|
+
return envFlag("SBOM", false, "true, false, spdx or cyclonedx");
|
|
2621
|
+
}
|
|
2622
|
+
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
2623
|
+
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
2624
|
+
ensureHelperOnPath();
|
|
1167
2625
|
const command = findBuildCommand(engine, argv);
|
|
1168
2626
|
let args = argv;
|
|
1169
2627
|
let summary;
|
|
1170
2628
|
if (command) {
|
|
1171
2629
|
const vars = new CiMetadata;
|
|
1172
|
-
const labels = !command.tagsOnly && (ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1173
|
-
const
|
|
2630
|
+
const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS", true)) ? vars.labels() : {};
|
|
2631
|
+
const sbom = !command.tagsOnly && sbomOption();
|
|
2632
|
+
const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
|
|
2633
|
+
const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
|
|
2634
|
+
engine,
|
|
2635
|
+
buildx: command.buildx,
|
|
2636
|
+
vars,
|
|
2637
|
+
tagLists: options.tagLists,
|
|
2638
|
+
labels,
|
|
2639
|
+
sbom,
|
|
2640
|
+
defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined,
|
|
2641
|
+
engineVersion: engine === "docker" ? undefined : () => engineVersion(engine, command.engineOptions)
|
|
2642
|
+
});
|
|
1174
2643
|
args = [...argv.slice(0, command.start), ...parsed.args];
|
|
1175
2644
|
summary = parsed.summary;
|
|
1176
2645
|
}
|
|
1177
|
-
|
|
2646
|
+
if (dryRun) {
|
|
2647
|
+
if (summary)
|
|
2648
|
+
printBuildSummary(engine, args, summary);
|
|
2649
|
+
if (summary?.skippedTags.length) {
|
|
2650
|
+
const skipped = summary.skippedTags.map((skipped) => ` - ${describeSkippedTag(skipped)}`);
|
|
2651
|
+
io.stderr(`[ce] skipped tags:
|
|
2652
|
+
${skipped.join(`
|
|
2653
|
+
`)}
|
|
2654
|
+
|
|
2655
|
+
`);
|
|
2656
|
+
}
|
|
2657
|
+
if (!system.which(engine))
|
|
2658
|
+
logError(`ce: warning: ${engine} is not on PATH, so running this here would fail`);
|
|
2659
|
+
io.stdout(`${[engine, ...args].map(shellQuote).join(" ")}
|
|
2660
|
+
`);
|
|
2661
|
+
return 0;
|
|
2662
|
+
}
|
|
1178
2663
|
if (!system.which(engine)) {
|
|
1179
2664
|
logError(`Engine not found: ${engine}`);
|
|
1180
2665
|
return 1;
|
|
1181
2666
|
}
|
|
1182
|
-
if (!await waitForEngine(engine))
|
|
2667
|
+
if (!await waitForEngine(engine, command?.engineOptions))
|
|
1183
2668
|
return 1;
|
|
1184
2669
|
if (summary)
|
|
1185
2670
|
printBuildSummary(engine, args, summary);
|
|
1186
|
-
if (engine === "docker")
|
|
1187
|
-
return system.exec("docker", ["docker", ...args]);
|
|
1188
|
-
if (system.which("gosu")) {
|
|
1189
|
-
const user = process.env.USER ?? "";
|
|
1190
|
-
if (!user) {
|
|
1191
|
-
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1192
|
-
return 1;
|
|
1193
|
-
}
|
|
1194
|
-
return system.exec("gosu", ["gosu", user, engine, ...args]);
|
|
1195
|
-
}
|
|
1196
2671
|
return system.exec(engine, [engine, ...args]);
|
|
1197
2672
|
}
|
|
1198
2673
|
|
|
@@ -1203,31 +2678,50 @@ function registryGroups() {
|
|
|
1203
2678
|
const suffixed = Object.keys(process.env).filter((name) => name.startsWith(prefix) && name.length > prefix.length).sort().map((name) => name.slice(prefix.length));
|
|
1204
2679
|
return [...process.env.REGISTRY_HOST ? [""] : [], ...suffixed];
|
|
1205
2680
|
}
|
|
2681
|
+
function unsetOrEmpty(name) {
|
|
2682
|
+
return `${name} is ${process.env[name] === undefined ? "unset" : "empty"}`;
|
|
2683
|
+
}
|
|
1206
2684
|
function oidcConfig() {
|
|
1207
2685
|
const audience = process.env.AUD ?? "";
|
|
1208
2686
|
if (!audience)
|
|
1209
2687
|
return null;
|
|
1210
2688
|
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
1211
2689
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
1212
|
-
|
|
1213
|
-
|
|
2690
|
+
const missing = [
|
|
2691
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
2692
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN"
|
|
2693
|
+
].filter((name) => !process.env[name]);
|
|
2694
|
+
if (missing.length) {
|
|
2695
|
+
return `Forgejo OIDC login config is incomplete: AUD is set, but ${missing.map(unsetOrEmpty).join("; ")}`;
|
|
1214
2696
|
}
|
|
1215
2697
|
try {
|
|
1216
2698
|
assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
|
|
1217
|
-
return {
|
|
2699
|
+
return {
|
|
2700
|
+
audience,
|
|
2701
|
+
requestUrl,
|
|
2702
|
+
requestToken,
|
|
2703
|
+
authority: urlAuthority(requestUrl)
|
|
2704
|
+
};
|
|
1218
2705
|
} catch (exc) {
|
|
1219
2706
|
if (!(exc instanceof CeError))
|
|
1220
2707
|
throw exc;
|
|
1221
2708
|
return `Invalid ACTIONS_ID_TOKEN_REQUEST_URL: ${exc.message}`;
|
|
1222
2709
|
}
|
|
1223
2710
|
}
|
|
1224
|
-
function loginWithEngines(target, password) {
|
|
1225
|
-
for (const engine of
|
|
2711
|
+
function loginWithEngines(target, password, engines) {
|
|
2712
|
+
for (const engine of engines) {
|
|
1226
2713
|
if (!system.which(engine))
|
|
1227
2714
|
continue;
|
|
1228
2715
|
let result;
|
|
1229
2716
|
try {
|
|
1230
|
-
result = system.run([
|
|
2717
|
+
result = system.run([
|
|
2718
|
+
engine,
|
|
2719
|
+
"login",
|
|
2720
|
+
"-u",
|
|
2721
|
+
target.user,
|
|
2722
|
+
"--password-stdin",
|
|
2723
|
+
target.registry
|
|
2724
|
+
], `${password}
|
|
1231
2725
|
`);
|
|
1232
2726
|
} catch (exc) {
|
|
1233
2727
|
if (isErrno(exc, "ENOENT"))
|
|
@@ -1241,12 +2735,13 @@ function loginWithEngines(target, password) {
|
|
|
1241
2735
|
}
|
|
1242
2736
|
return null;
|
|
1243
2737
|
}
|
|
1244
|
-
async function login() {
|
|
2738
|
+
async function login(engine, dryRun = false) {
|
|
1245
2739
|
ensureHelperOnPath();
|
|
2740
|
+
const engines = engine ? [engine] : ENGINES;
|
|
1246
2741
|
const groups = registryGroups();
|
|
1247
2742
|
if (groups.length === 0) {
|
|
1248
|
-
|
|
1249
|
-
return
|
|
2743
|
+
logError("ce: no registries to log in to; set REGISTRY_HOST, REGISTRY_USER and REGISTRY_PASSWORD");
|
|
2744
|
+
return 1;
|
|
1250
2745
|
}
|
|
1251
2746
|
const oidc = oidcConfig();
|
|
1252
2747
|
if (typeof oidc === "string") {
|
|
@@ -1257,37 +2752,61 @@ async function login() {
|
|
|
1257
2752
|
let failed = false;
|
|
1258
2753
|
for (const group of groups) {
|
|
1259
2754
|
const suffix = group ? `__${group}` : "";
|
|
1260
|
-
const
|
|
1261
|
-
const
|
|
1262
|
-
const
|
|
2755
|
+
const hostName = `REGISTRY_HOST${suffix}`;
|
|
2756
|
+
const userName = `REGISTRY_USER${suffix}`;
|
|
2757
|
+
const passwordName = `REGISTRY_PASSWORD${suffix}`;
|
|
2758
|
+
const host = process.env[hostName] ?? "";
|
|
2759
|
+
const user = process.env[userName] ?? "";
|
|
2760
|
+
const password = process.env[passwordName] ?? "";
|
|
1263
2761
|
const label = group || "default";
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
failed = true;
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
for (const rawRegistry of host.split(/\s+/u).filter(Boolean)) {
|
|
1271
|
-
let authority;
|
|
1272
|
-
let registry;
|
|
2762
|
+
const hosts = host.split(/\s+/u).filter(Boolean);
|
|
2763
|
+
const registries = [];
|
|
2764
|
+
for (const rawRegistry of hosts) {
|
|
1273
2765
|
try {
|
|
1274
|
-
authority = urlAuthority(rawRegistry);
|
|
1275
|
-
registry = registryServer(rawRegistry);
|
|
2766
|
+
const authority = urlAuthority(rawRegistry);
|
|
2767
|
+
const registry = registryServer(rawRegistry);
|
|
2768
|
+
registries.push({
|
|
2769
|
+
registry,
|
|
2770
|
+
useOidc: oidc !== null && sameAuthority(authority, oidc.authority)
|
|
2771
|
+
});
|
|
1276
2772
|
} catch (exc) {
|
|
1277
2773
|
if (!(exc instanceof CeError))
|
|
1278
2774
|
throw exc;
|
|
1279
|
-
logError(`Invalid registry host for '${label}' (${rawRegistry}): ${exc.message}`);
|
|
1280
|
-
failed = true;
|
|
1281
|
-
continue;
|
|
1282
|
-
}
|
|
1283
|
-
const useOidc = oidc !== null && sameAuthority(authority, oidc.authority);
|
|
1284
|
-
if (!useOidc && !password) {
|
|
1285
|
-
logError(incomplete);
|
|
2775
|
+
logError(rawRegistry.includes("@") ? `Invalid registry host for '${label}' in ${hostName}, not shown as it has an "@" and may hold a password` : `Invalid registry host for '${label}' (${rawRegistry}): ${exc.message}`);
|
|
1286
2776
|
failed = true;
|
|
1287
|
-
continue;
|
|
1288
2777
|
}
|
|
1289
|
-
targets.push({ registry, label, user, password, useOidc });
|
|
1290
2778
|
}
|
|
2779
|
+
const missing = [];
|
|
2780
|
+
if (!host)
|
|
2781
|
+
missing.push(unsetOrEmpty(hostName));
|
|
2782
|
+
else if (!hosts.length)
|
|
2783
|
+
missing.push(`${hostName} has no registry in it`);
|
|
2784
|
+
if (!user)
|
|
2785
|
+
missing.push(unsetOrEmpty(userName));
|
|
2786
|
+
const needPassword = registries.filter((r) => !r.useOidc).map((r) => r.registry);
|
|
2787
|
+
if (!password && needPassword.length)
|
|
2788
|
+
missing.push(`${unsetOrEmpty(passwordName)}, which ${needPassword.join(", ")} needs`);
|
|
2789
|
+
if (missing.length) {
|
|
2790
|
+
logError(`Login config for '${label}' is incomplete: ${missing.join("; ")}`);
|
|
2791
|
+
failed = true;
|
|
2792
|
+
}
|
|
2793
|
+
if (!user)
|
|
2794
|
+
continue;
|
|
2795
|
+
for (const { registry, useOidc } of registries) {
|
|
2796
|
+
if (useOidc || password)
|
|
2797
|
+
targets.push({ registry, label, user, password, useOidc });
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (dryRun) {
|
|
2801
|
+
const available = engines.find((candidate) => system.which(candidate));
|
|
2802
|
+
const selected = available ?? engines[0];
|
|
2803
|
+
if (!available)
|
|
2804
|
+
logError(`ce: warning: ${selected} is not on PATH, so running this here would fail`);
|
|
2805
|
+
for (const target of targets) {
|
|
2806
|
+
io.stdout(`${[selected, "login", "-u", target.user, "--password-stdin", target.registry].map(shellQuote).join(" ")}
|
|
2807
|
+
`);
|
|
2808
|
+
}
|
|
2809
|
+
return failed ? 1 : 0;
|
|
1291
2810
|
}
|
|
1292
2811
|
let jwt = null;
|
|
1293
2812
|
if (oidc && targets.some((target) => target.useOidc)) {
|
|
@@ -1309,14 +2828,14 @@ async function login() {
|
|
|
1309
2828
|
continue;
|
|
1310
2829
|
}
|
|
1311
2830
|
log(`Logging in to ${registry} (${label})...`);
|
|
1312
|
-
const
|
|
1313
|
-
if (!
|
|
2831
|
+
const used = loginWithEngines(target, password, engines);
|
|
2832
|
+
if (!used) {
|
|
1314
2833
|
logError(`Login failed for '${label}' (${registry})`);
|
|
1315
2834
|
failed = true;
|
|
1316
2835
|
continue;
|
|
1317
2836
|
}
|
|
1318
|
-
log(`Logged in to ${registry} (${label}) via ${
|
|
1319
|
-
if (target.useOidc && oidc && jwt) {
|
|
2837
|
+
log(`Logged in to ${registry} (${label}) via ${used}`);
|
|
2838
|
+
if (target.useOidc && oidc && jwt && (!engine || engine === "docker")) {
|
|
1320
2839
|
try {
|
|
1321
2840
|
await configureDockerCredentialHelper(registry, target.user, oidc.audience, oidc.authority, jwt);
|
|
1322
2841
|
} catch (exc) {
|
|
@@ -1330,11 +2849,771 @@ async function login() {
|
|
|
1330
2849
|
return failed ? 1 : 0;
|
|
1331
2850
|
}
|
|
1332
2851
|
|
|
2852
|
+
// src/options.ts
|
|
2853
|
+
function defaultOptions() {
|
|
2854
|
+
return {
|
|
2855
|
+
tagPrefix: process.env.TAG_PREFIX ?? "",
|
|
2856
|
+
cachePrefix: process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "",
|
|
2857
|
+
tagLists: new Map,
|
|
2858
|
+
used: new Map
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
function validateScope(options, tool) {
|
|
2862
|
+
for (const [flag, scope] of options.used) {
|
|
2863
|
+
if (isEngine(tool) !== (scope === "container"))
|
|
2864
|
+
throw new CeError(`ce: ${flag} is a ${scope} option and cannot be used with ${tool}`);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function parseCeOptions(given, options, tool) {
|
|
2868
|
+
let argv = given;
|
|
2869
|
+
const use = (flag, scope) => {
|
|
2870
|
+
options.used.set(flag, scope);
|
|
2871
|
+
if (tool)
|
|
2872
|
+
validateScope(options, tool);
|
|
2873
|
+
};
|
|
2874
|
+
const value = (flag) => {
|
|
2875
|
+
const current = argv[0];
|
|
2876
|
+
if (current === flag) {
|
|
2877
|
+
const [result, rest] = consumeValue(argv.slice(1), flag);
|
|
2878
|
+
argv = rest;
|
|
2879
|
+
return result;
|
|
2880
|
+
}
|
|
2881
|
+
argv = argv.slice(1);
|
|
2882
|
+
return current.slice(flag.length + 1);
|
|
2883
|
+
};
|
|
2884
|
+
for (;; ) {
|
|
2885
|
+
const arg = argv[0];
|
|
2886
|
+
if (arg === "--")
|
|
2887
|
+
return { args: argv.slice(1), native: true };
|
|
2888
|
+
if (arg === "--tag-list" || arg?.startsWith("--tag-list=")) {
|
|
2889
|
+
use("--tag-list", "container");
|
|
2890
|
+
defineTagList(value("--tag-list"), options.tagLists);
|
|
2891
|
+
} else if (arg === "--tag-prefix" || arg?.startsWith("--tag-prefix=")) {
|
|
2892
|
+
use("--tag-prefix", "container");
|
|
2893
|
+
options.tagPrefix = value("--tag-prefix");
|
|
2894
|
+
} else if (arg === "--cache-prefix" || arg?.startsWith("--cache-prefix=")) {
|
|
2895
|
+
use("--cache-prefix", "container");
|
|
2896
|
+
options.cachePrefix = value("--cache-prefix");
|
|
2897
|
+
} else if (arg === "--oci-labels" || arg === "--no-oci-labels") {
|
|
2898
|
+
use(arg, "container");
|
|
2899
|
+
options.ociLabels = arg === "--oci-labels";
|
|
2900
|
+
argv = argv.slice(1);
|
|
2901
|
+
} else if (arg === "--cache-from-default") {
|
|
2902
|
+
use(arg, "container");
|
|
2903
|
+
options.cacheFromDefault = true;
|
|
2904
|
+
argv = argv.slice(1);
|
|
2905
|
+
} else if (arg === "--stage") {
|
|
2906
|
+
use(arg, "publishing");
|
|
2907
|
+
options.stage = true;
|
|
2908
|
+
argv = argv.slice(1);
|
|
2909
|
+
} else if (arg === "--tolerate-republish") {
|
|
2910
|
+
use(arg, "publishing");
|
|
2911
|
+
options.tolerateRepublish = true;
|
|
2912
|
+
argv = argv.slice(1);
|
|
2913
|
+
} else if (arg === "--token-env" || arg?.startsWith("--token-env=")) {
|
|
2914
|
+
use("--token-env", "publishing");
|
|
2915
|
+
options.tokenEnv = value("--token-env");
|
|
2916
|
+
} else if (arg === "--forgejo-oidc") {
|
|
2917
|
+
use(arg, "publishing");
|
|
2918
|
+
options.forgejoOidc = true;
|
|
2919
|
+
argv = argv.slice(1);
|
|
2920
|
+
} else if (arg === "--tag-env" || arg?.startsWith("--tag-env=")) {
|
|
2921
|
+
use("--tag-env", "publishing");
|
|
2922
|
+
options.tagEnv = value("--tag-env");
|
|
2923
|
+
} else if (arg === "--version-tag-env" || arg?.startsWith("--version-tag-env=")) {
|
|
2924
|
+
use("--version-tag-env", "publishing");
|
|
2925
|
+
options.versionTagEnv = value("--version-tag-env");
|
|
2926
|
+
} else if (arg === "--pack-output-env" || arg?.startsWith("--pack-output-env=")) {
|
|
2927
|
+
use("--pack-output-env", "publishing");
|
|
2928
|
+
options.packOutputEnv = value("--pack-output-env");
|
|
2929
|
+
} else if (arg === "--check-auth" || arg === "--no-check-auth") {
|
|
2930
|
+
use(arg, "publishing");
|
|
2931
|
+
options.checkAuth = arg === "--check-auth";
|
|
2932
|
+
argv = argv.slice(1);
|
|
2933
|
+
} else if (arg === "--auto-tag" || arg === "--no-auto-tag") {
|
|
2934
|
+
use(arg, "publishing");
|
|
2935
|
+
options.autoTag = arg === "--auto-tag";
|
|
2936
|
+
argv = argv.slice(1);
|
|
2937
|
+
} else if (arg === "--dry-run") {
|
|
2938
|
+
options.dryRun = true;
|
|
2939
|
+
argv = argv.slice(1);
|
|
2940
|
+
} else
|
|
2941
|
+
return { args: argv, native: false };
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// src/publish.ts
|
|
2946
|
+
import path7 from "node:path";
|
|
2947
|
+
|
|
2948
|
+
// src/npm-auth.ts
|
|
2949
|
+
function registryUrl(registry) {
|
|
2950
|
+
if (!registry)
|
|
2951
|
+
throw new CeError("ce: token authentication requires an explicit native --registry");
|
|
2952
|
+
let url;
|
|
2953
|
+
try {
|
|
2954
|
+
url = new URL(registry);
|
|
2955
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash || /[\u0000-\u0020\\=]/u.test(registry))
|
|
2956
|
+
throw new Error("invalid registry");
|
|
2957
|
+
} catch {
|
|
2958
|
+
throw new CeError("ce: token authentication requires an absolute HTTP(S) registry URL without credentials, query, fragment, whitespace, backslashes or equals signs");
|
|
2959
|
+
}
|
|
2960
|
+
if (!url.pathname.endsWith("/"))
|
|
2961
|
+
url.pathname += "/";
|
|
2962
|
+
return url;
|
|
2963
|
+
}
|
|
2964
|
+
function credentials(url, token) {
|
|
2965
|
+
return {
|
|
2966
|
+
registry: url.href,
|
|
2967
|
+
env: { [`npm_config_//${url.host}${url.pathname}:_authToken`]: token },
|
|
2968
|
+
token
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function npmAuth(registry, tokenEnv, preview) {
|
|
2972
|
+
if (!/^[A-Za-z_][A-Za-z\d_]*$/u.test(tokenEnv))
|
|
2973
|
+
throw new CeError("ce: --token-env requires an environment variable name");
|
|
2974
|
+
const url = registryUrl(registry);
|
|
2975
|
+
const token = preview ? "" : process.env[tokenEnv];
|
|
2976
|
+
if (!preview && (!token || /[\s\p{Cc}]/u.test(token)))
|
|
2977
|
+
throw new CeError(`ce: ${tokenEnv} must contain a nonempty npm token without control characters`);
|
|
2978
|
+
return credentials(url, token ?? "");
|
|
2979
|
+
}
|
|
2980
|
+
function forgejoNpmRegistry() {
|
|
2981
|
+
const ci = (name) => process.env[`FORGEJO_${name}`] || process.env[`GITHUB_${name}`];
|
|
2982
|
+
const server = ci("SERVER_URL");
|
|
2983
|
+
const owner = ci("REPOSITORY_OWNER") || ci("REPOSITORY")?.split("/")[0];
|
|
2984
|
+
if (!server || !owner)
|
|
2985
|
+
throw new CeError("ce: --forgejo-oidc needs a native --registry outside a Forgejo Actions run; FORGEJO_SERVER_URL and FORGEJO_REPOSITORY are not set");
|
|
2986
|
+
return `${server.replace(/\/+$/u, "")}/api/packages/${encodeURIComponent(owner)}/npm/`;
|
|
2987
|
+
}
|
|
2988
|
+
async function forgejoNpmAuth(registry, preview) {
|
|
2989
|
+
const url = registryUrl(registry);
|
|
2990
|
+
const audience = process.env.AUD;
|
|
2991
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
2992
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
2993
|
+
if (!audience || !requestUrl || !preview && !requestToken)
|
|
2994
|
+
throw new CeError("ce: Forgejo npm OIDC requires AUD, ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN; enable-openid-connect must be enabled for the job");
|
|
2995
|
+
try {
|
|
2996
|
+
const request = new URL(requestUrl);
|
|
2997
|
+
if (request.username || request.password || request.origin !== url.origin)
|
|
2998
|
+
throw new Error("different server");
|
|
2999
|
+
} catch {
|
|
3000
|
+
throw new CeError("ce: Forgejo npm registry and OIDC token request must use the same HTTP(S) origin without URL credentials");
|
|
3001
|
+
}
|
|
3002
|
+
if (preview)
|
|
3003
|
+
return credentials(url, "");
|
|
3004
|
+
const token = await forgejoOidcJwt(audience, requestUrl, requestToken);
|
|
3005
|
+
if (needsRefresh(jwtExpiry(token)))
|
|
3006
|
+
throw new CeError("ce: Forgejo OIDC token expires within the five-minute window; refusing to publish");
|
|
3007
|
+
return credentials(url, token);
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/pack.ts
|
|
3011
|
+
import fs7 from "node:fs";
|
|
3012
|
+
import path6 from "node:path";
|
|
3013
|
+
|
|
3014
|
+
// src/package-metadata.ts
|
|
3015
|
+
import fs6 from "node:fs";
|
|
3016
|
+
import path5 from "node:path";
|
|
3017
|
+
function readPackageManifest(file) {
|
|
3018
|
+
let data;
|
|
3019
|
+
try {
|
|
3020
|
+
if (fs6.statSync(file).isDirectory())
|
|
3021
|
+
data = readJsonFile(path5.join(file, "package.json"), undefined);
|
|
3022
|
+
else {
|
|
3023
|
+
const result = system.output([
|
|
3024
|
+
"tar",
|
|
3025
|
+
"-xOf",
|
|
3026
|
+
file,
|
|
3027
|
+
"package/package.json"
|
|
3028
|
+
]);
|
|
3029
|
+
if (result.status !== 0)
|
|
3030
|
+
throw new Error("cannot read package/package.json from tarball");
|
|
3031
|
+
data = JSON.parse(result.stdout);
|
|
3032
|
+
}
|
|
3033
|
+
} catch (exc) {
|
|
3034
|
+
throw new CeError(`ce: cannot read the publish target ${JSON.stringify(file)}: ${errorMessage(exc)}`);
|
|
3035
|
+
}
|
|
3036
|
+
if (!isObject(data))
|
|
3037
|
+
throw new CeError("ce: the publish target has no package.json object");
|
|
3038
|
+
return data;
|
|
3039
|
+
}
|
|
3040
|
+
function checkVersionTag(data, tag) {
|
|
3041
|
+
const version = typeof data.version === "string" ? SEMVER.exec(data.version)?.[1] : undefined;
|
|
3042
|
+
if (!version)
|
|
3043
|
+
throw new CeError("ce: the selected package needs a valid SemVer version for release validation");
|
|
3044
|
+
if (!tag)
|
|
3045
|
+
return;
|
|
3046
|
+
const name = tag.replace(/^refs\/tags\//u, "");
|
|
3047
|
+
if (SEMVER.exec(name)?.[1] !== version)
|
|
3048
|
+
throw new CeError(`ce: release tag ${JSON.stringify(tag)} does not match package version ${version}`);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// src/pack.ts
|
|
3052
|
+
function packWithMetadata(args, configArgs, options) {
|
|
3053
|
+
const separator = args.indexOf("--");
|
|
3054
|
+
const end = separator < 0 ? args.length : separator;
|
|
3055
|
+
let printJson = false;
|
|
3056
|
+
for (let i = 0;i < end; i++) {
|
|
3057
|
+
if (args[i] === "--json")
|
|
3058
|
+
printJson = args[i + 1] !== "false";
|
|
3059
|
+
else if (args[i] === "--json=true")
|
|
3060
|
+
printJson = true;
|
|
3061
|
+
else if (args[i] === "--json=false" || args[i] === "--no-json")
|
|
3062
|
+
printJson = false;
|
|
3063
|
+
}
|
|
3064
|
+
const native = [
|
|
3065
|
+
...args.slice(0, end),
|
|
3066
|
+
"--json",
|
|
3067
|
+
"--foreground-scripts=false",
|
|
3068
|
+
...args.slice(end)
|
|
3069
|
+
];
|
|
3070
|
+
if (options.dryRun) {
|
|
3071
|
+
if (!system.which("npm"))
|
|
3072
|
+
logError("ce: warning: npm is not on PATH, so running this here would fail");
|
|
3073
|
+
io.stdout(`${["npm", ...native].map(shellQuote).join(" ")}
|
|
3074
|
+
`);
|
|
3075
|
+
return 0;
|
|
3076
|
+
}
|
|
3077
|
+
if (!system.which("npm")) {
|
|
3078
|
+
logError("ce: npm not found on PATH");
|
|
3079
|
+
return 1;
|
|
3080
|
+
}
|
|
3081
|
+
const config = system.capture([
|
|
3082
|
+
"npm",
|
|
3083
|
+
"config",
|
|
3084
|
+
"list",
|
|
3085
|
+
...configArgs,
|
|
3086
|
+
"--json"
|
|
3087
|
+
]);
|
|
3088
|
+
io.stderr(config.stderr);
|
|
3089
|
+
if (config.status !== 0) {
|
|
3090
|
+
return config.status;
|
|
3091
|
+
}
|
|
3092
|
+
let settings;
|
|
3093
|
+
try {
|
|
3094
|
+
settings = JSON.parse(config.stdout);
|
|
3095
|
+
} catch {
|
|
3096
|
+
throw new CeError("ce: npm config did not return valid JSON");
|
|
3097
|
+
}
|
|
3098
|
+
if (!isObject(settings) || typeof settings["pack-destination"] !== "string" || !settings["pack-destination"] || /[\r\n\0]/u.test(settings["pack-destination"]))
|
|
3099
|
+
throw new CeError("ce: npm config did not return a valid pack-destination");
|
|
3100
|
+
if (settings["dry-run"] !== false)
|
|
3101
|
+
throw new CeError("ce: package metadata requires a real tarball; use ce's --dry-run to preview");
|
|
3102
|
+
if (settings.workspaces || Array.isArray(settings.workspace) && settings.workspace.length)
|
|
3103
|
+
throw new CeError("ce: package metadata supports one package; select a package path instead of workspace mode");
|
|
3104
|
+
const packed = system.capture(["npm", ...native]);
|
|
3105
|
+
io.stderr(packed.stderr);
|
|
3106
|
+
if (packed.status !== 0) {
|
|
3107
|
+
io.stdout(packed.stdout);
|
|
3108
|
+
return packed.status;
|
|
3109
|
+
}
|
|
3110
|
+
let report;
|
|
3111
|
+
try {
|
|
3112
|
+
report = JSON.parse(packed.stdout);
|
|
3113
|
+
} catch {
|
|
3114
|
+
throw new CeError("ce: npm pack did not return valid JSON");
|
|
3115
|
+
}
|
|
3116
|
+
const entries = Array.isArray(report) ? report : isObject(report) ? Object.values(report) : [];
|
|
3117
|
+
const entry = entries[0];
|
|
3118
|
+
if (entries.length !== 1 || !isObject(entry) || typeof entry.filename !== "string" || !entry.filename || path6.basename(entry.filename) !== entry.filename || /[\r\n\0]/u.test(entry.filename) || typeof entry.version !== "string" || !SEMVER.test(entry.version))
|
|
3119
|
+
throw new CeError("ce: npm pack must report exactly one tarball with a valid filename and version");
|
|
3120
|
+
const file = path6.resolve(settings["pack-destination"], entry.filename);
|
|
3121
|
+
try {
|
|
3122
|
+
if (!fs7.statSync(file).isFile())
|
|
3123
|
+
throw new Error("not a regular file");
|
|
3124
|
+
} catch {
|
|
3125
|
+
throw new CeError("ce: npm pack did not produce the reported tarball file");
|
|
3126
|
+
}
|
|
3127
|
+
const manifest = readPackageManifest(file);
|
|
3128
|
+
if (manifest.version !== entry.version)
|
|
3129
|
+
throw new CeError("ce: npm pack metadata does not match the packed package version");
|
|
3130
|
+
checkVersionTag(manifest, options.versionTag ?? "");
|
|
3131
|
+
const tarball = path6.relative(process.cwd(), file);
|
|
3132
|
+
if (/[\r\n\0]/u.test(tarball))
|
|
3133
|
+
throw new CeError("ce: the tarball path cannot contain line breaks or null characters");
|
|
3134
|
+
if (options.outputFile) {
|
|
3135
|
+
try {
|
|
3136
|
+
fs7.appendFileSync(options.outputFile, `tarball=${tarball}
|
|
3137
|
+
version=${entry.version}
|
|
3138
|
+
`);
|
|
3139
|
+
} catch (exc) {
|
|
3140
|
+
throw new CeError(`ce: cannot append package outputs: ${errorMessage(exc)}`);
|
|
3141
|
+
}
|
|
3142
|
+
logError(`ce: packed ${tarball} (${entry.version})`);
|
|
3143
|
+
} else if (printJson)
|
|
3144
|
+
io.stdout(packed.stdout);
|
|
3145
|
+
else
|
|
3146
|
+
io.stdout(`${entry.filename}
|
|
3147
|
+
`);
|
|
3148
|
+
return 0;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/publish.ts
|
|
3152
|
+
function isPackageManager(value) {
|
|
3153
|
+
return value === "npm" || value === "bun";
|
|
3154
|
+
}
|
|
3155
|
+
var VALUES = new Set([
|
|
3156
|
+
"--tag",
|
|
3157
|
+
"--access",
|
|
3158
|
+
"--registry",
|
|
3159
|
+
"--otp",
|
|
3160
|
+
"--auth-type",
|
|
3161
|
+
"--userconfig",
|
|
3162
|
+
"--globalconfig",
|
|
3163
|
+
"--cache",
|
|
3164
|
+
"--cafile",
|
|
3165
|
+
"--ca",
|
|
3166
|
+
"--prefix",
|
|
3167
|
+
"--workspace",
|
|
3168
|
+
"-w",
|
|
3169
|
+
"--filter",
|
|
3170
|
+
"-F",
|
|
3171
|
+
"--config",
|
|
3172
|
+
"-c",
|
|
3173
|
+
"--cwd",
|
|
3174
|
+
"--env-file",
|
|
3175
|
+
"--gzip-level",
|
|
3176
|
+
"--provenance-file",
|
|
3177
|
+
"--loglevel",
|
|
3178
|
+
"--logs-dir",
|
|
3179
|
+
"--cache-dir",
|
|
3180
|
+
"--backend",
|
|
3181
|
+
"--concurrent-scripts",
|
|
3182
|
+
"--network-concurrency",
|
|
3183
|
+
"--omit",
|
|
3184
|
+
"--cpu",
|
|
3185
|
+
"--os",
|
|
3186
|
+
"--libc",
|
|
3187
|
+
"--token-env",
|
|
3188
|
+
"--tag-env",
|
|
3189
|
+
"--version-tag-env",
|
|
3190
|
+
"--pack-output-env",
|
|
3191
|
+
"--pack-destination"
|
|
3192
|
+
]);
|
|
3193
|
+
var SWITCHES = new Set([
|
|
3194
|
+
"--dry-run",
|
|
3195
|
+
"--ignore-scripts",
|
|
3196
|
+
"--foreground-scripts",
|
|
3197
|
+
"--json",
|
|
3198
|
+
"--provenance",
|
|
3199
|
+
"--workspaces",
|
|
3200
|
+
"--include-workspace-root",
|
|
3201
|
+
"--force",
|
|
3202
|
+
"-f",
|
|
3203
|
+
"--silent",
|
|
3204
|
+
"--quiet",
|
|
3205
|
+
"--verbose",
|
|
3206
|
+
"--frozen-lockfile",
|
|
3207
|
+
"--offline",
|
|
3208
|
+
"--prefer-offline",
|
|
3209
|
+
"--lockfile-only",
|
|
3210
|
+
"--trust",
|
|
3211
|
+
"--global",
|
|
3212
|
+
"-g",
|
|
3213
|
+
"--yes",
|
|
3214
|
+
"-y",
|
|
3215
|
+
"--yarn",
|
|
3216
|
+
"--production",
|
|
3217
|
+
"-p",
|
|
3218
|
+
"--recursive",
|
|
3219
|
+
"-r",
|
|
3220
|
+
"--tolerate-republish",
|
|
3221
|
+
"--save",
|
|
3222
|
+
"--use-system-ca",
|
|
3223
|
+
"--use-openssl-ca",
|
|
3224
|
+
"--use-bundled-ca"
|
|
3225
|
+
]);
|
|
3226
|
+
var WORKSPACES = new Set([
|
|
3227
|
+
"--workspace",
|
|
3228
|
+
"-w",
|
|
3229
|
+
"--workspaces",
|
|
3230
|
+
"--filter",
|
|
3231
|
+
"-F",
|
|
3232
|
+
"--recursive",
|
|
3233
|
+
"-r"
|
|
3234
|
+
]);
|
|
3235
|
+
var CE_FLAGS = new Set([
|
|
3236
|
+
"--stage",
|
|
3237
|
+
"--tolerate-republish",
|
|
3238
|
+
"--auto-tag",
|
|
3239
|
+
"--no-auto-tag",
|
|
3240
|
+
"--token-env",
|
|
3241
|
+
"--check-auth",
|
|
3242
|
+
"--no-check-auth",
|
|
3243
|
+
"--forgejo-oidc",
|
|
3244
|
+
"--tag-env",
|
|
3245
|
+
"--version-tag-env",
|
|
3246
|
+
"--pack-output-env"
|
|
3247
|
+
]);
|
|
3248
|
+
var AUTH_CONFIG = new Set([
|
|
3249
|
+
"--userconfig",
|
|
3250
|
+
"--globalconfig",
|
|
3251
|
+
"--cafile",
|
|
3252
|
+
"--ca",
|
|
3253
|
+
"--cache",
|
|
3254
|
+
"--prefix",
|
|
3255
|
+
"--loglevel",
|
|
3256
|
+
"--logs-dir",
|
|
3257
|
+
"--use-system-ca",
|
|
3258
|
+
"--use-openssl-ca",
|
|
3259
|
+
"--use-bundled-ca"
|
|
3260
|
+
]);
|
|
3261
|
+
var PORTABLE_STAGE = new Set([
|
|
3262
|
+
"--tag",
|
|
3263
|
+
"--access",
|
|
3264
|
+
"--registry",
|
|
3265
|
+
"--otp",
|
|
3266
|
+
"--auth-type",
|
|
3267
|
+
"--dry-run",
|
|
3268
|
+
"--ignore-scripts",
|
|
3269
|
+
"--silent",
|
|
3270
|
+
"--verbose",
|
|
3271
|
+
"--json",
|
|
3272
|
+
"--provenance",
|
|
3273
|
+
"--provenance-file",
|
|
3274
|
+
"--no-provenance",
|
|
3275
|
+
"--ca",
|
|
3276
|
+
"--cafile",
|
|
3277
|
+
"--userconfig",
|
|
3278
|
+
"--globalconfig",
|
|
3279
|
+
"--help",
|
|
3280
|
+
"-h",
|
|
3281
|
+
"--version"
|
|
3282
|
+
]);
|
|
3283
|
+
function option(arg) {
|
|
3284
|
+
return arg.split("=", 1)[0];
|
|
3285
|
+
}
|
|
3286
|
+
function knownOption(arg) {
|
|
3287
|
+
return VALUES.has(option(arg)) || SWITCHES.has(option(arg)) || arg.startsWith("--no-") || arg.includes("=");
|
|
3288
|
+
}
|
|
3289
|
+
function parseNative(args) {
|
|
3290
|
+
const parsed = { options: [], words: [], rest: [] };
|
|
3291
|
+
for (let i = 0;i < args.length; ) {
|
|
3292
|
+
const arg = args[i];
|
|
3293
|
+
if (arg === "--") {
|
|
3294
|
+
parsed.rest = args.slice(i + 1);
|
|
3295
|
+
break;
|
|
3296
|
+
}
|
|
3297
|
+
if (!arg.startsWith("-")) {
|
|
3298
|
+
parsed.words.push({ value: arg, index: i });
|
|
3299
|
+
i++;
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
const name = option(arg);
|
|
3303
|
+
const joined = arg.includes("=");
|
|
3304
|
+
const takesValue = VALUES.has(name) && !joined;
|
|
3305
|
+
const missing = takesValue && (!args[i + 1] || args[i + 1].startsWith("-"));
|
|
3306
|
+
const end = takesValue && !missing ? i + 2 : i + 1;
|
|
3307
|
+
parsed.options.push({
|
|
3308
|
+
arg,
|
|
3309
|
+
name,
|
|
3310
|
+
joined,
|
|
3311
|
+
value: joined ? arg.slice(name.length + 1) : end > i + 1 ? args[i + 1] : undefined,
|
|
3312
|
+
known: knownOption(arg),
|
|
3313
|
+
missing,
|
|
3314
|
+
start: i,
|
|
3315
|
+
end
|
|
3316
|
+
});
|
|
3317
|
+
i = end;
|
|
3318
|
+
}
|
|
3319
|
+
return parsed;
|
|
3320
|
+
}
|
|
3321
|
+
function requireValue(o) {
|
|
3322
|
+
if (o.missing)
|
|
3323
|
+
throw new CeError(`ce: ${o.arg} requires a value`);
|
|
3324
|
+
}
|
|
3325
|
+
function invocation(manager, args, parsed) {
|
|
3326
|
+
const word = parsed.words[0];
|
|
3327
|
+
for (const o of parsed.options) {
|
|
3328
|
+
if (word && o.start > word.index)
|
|
3329
|
+
break;
|
|
3330
|
+
requireValue(o);
|
|
3331
|
+
if (!o.known && !CE_FLAGS.has(o.name))
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
if (!word)
|
|
3335
|
+
return;
|
|
3336
|
+
const i = word.index;
|
|
3337
|
+
if (word.value === "publish" || manager === "npm" && word.value.length >= 2 && "publish".startsWith(word.value))
|
|
3338
|
+
return { start: i, end: i + 1, staged: false, kind: "publish" };
|
|
3339
|
+
if (manager === "npm" && word.value === "stage" && args[i + 1] === "publish")
|
|
3340
|
+
return { start: i, end: i + 2, staged: true, kind: "publish" };
|
|
3341
|
+
if (manager === "npm" && word.value === "pack")
|
|
3342
|
+
return { start: i, end: i + 1, staged: false, kind: "pack" };
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
function manifest(manager, parsed, command, validatingRelease = false) {
|
|
3346
|
+
let cwd = process.cwd();
|
|
3347
|
+
for (const { arg, name, value, known } of parsed.options) {
|
|
3348
|
+
if (WORKSPACES.has(name) && !arg.endsWith("=false"))
|
|
3349
|
+
throw new CeError(validatingRelease ? "ce: release validation requires one package; workspaces may have different versions" : "ce: workspace publishing needs an explicit --tag or --no-auto-tag; packages may have different versions");
|
|
3350
|
+
if (name === "--prefix")
|
|
3351
|
+
throw new CeError(validatingRelease ? "ce: release validation requires a local publish path instead of --prefix" : "ce: use a local publish path, --tag or --no-auto-tag with --prefix");
|
|
3352
|
+
if (!known)
|
|
3353
|
+
throw new CeError(validatingRelease ? `ce: release validation cannot determine the value syntax of ${name}; use an =value option` : `ce: cannot infer a tag with ${name}; pass --tag or --no-auto-tag`);
|
|
3354
|
+
if (manager === "bun" && name === "--cwd")
|
|
3355
|
+
cwd = path7.resolve(process.cwd(), value);
|
|
3356
|
+
}
|
|
3357
|
+
const targets = [
|
|
3358
|
+
...parsed.words.filter((word) => word.index >= command.end).map((word) => word.value),
|
|
3359
|
+
...parsed.rest
|
|
3360
|
+
];
|
|
3361
|
+
if (targets.length > 1)
|
|
3362
|
+
throw new CeError(validatingRelease ? "ce: release validation supports one local publish target" : "ce: tag inference supports one local publish target; pass --tag or --no-auto-tag");
|
|
3363
|
+
const target = targets[0] ?? ".";
|
|
3364
|
+
if (!target)
|
|
3365
|
+
throw new CeError("ce: the publish target must not be empty");
|
|
3366
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:/u.test(target) && !target.startsWith("file:"))
|
|
3367
|
+
throw new CeError(validatingRelease ? "ce: release validation requires a local package directory or tarball" : "ce: automatic tags require a local package directory or tarball; pass --tag for other package specs");
|
|
3368
|
+
const file = path7.resolve(cwd, target.replace(/^file:/u, ""));
|
|
3369
|
+
return readPackageManifest(file);
|
|
3370
|
+
}
|
|
3371
|
+
function inferTag(data) {
|
|
3372
|
+
if (isObject(data.publishConfig) && data.publishConfig.tag !== undefined)
|
|
3373
|
+
return;
|
|
3374
|
+
const version = typeof data.version === "string" ? SEMVER.exec(data.version) : null;
|
|
3375
|
+
if (!version)
|
|
3376
|
+
throw new CeError("ce: the publish target needs a valid SemVer version to infer a tag; use --tag to choose one explicitly");
|
|
3377
|
+
const channel = version[5]?.split(".")[0];
|
|
3378
|
+
if (channel && (channel === "latest" || /^(?:v?\d+|x)$/iu.test(channel)))
|
|
3379
|
+
throw new CeError(`ce: prerelease ${JSON.stringify(data.version)} needs an explicit --tag; its first label is ambiguous or promotes latest`);
|
|
3380
|
+
return channel;
|
|
3381
|
+
}
|
|
3382
|
+
function nativeConfig(args) {
|
|
3383
|
+
const { options } = parseNative(args);
|
|
3384
|
+
return {
|
|
3385
|
+
registry: options.findLast((o) => o.name === "--registry"),
|
|
3386
|
+
config: options.filter((o) => AUTH_CONFIG.has(o.name)).flatMap((o) => args.slice(o.start, o.end))
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
function registryQuery(argv, auth) {
|
|
3390
|
+
if (!system.which("npm"))
|
|
3391
|
+
throw new CeError("ce: --tolerate-republish needs npm on PATH to query the registry");
|
|
3392
|
+
const result = system.capture(["npm", ...argv, "--json"], auth?.env);
|
|
3393
|
+
let data;
|
|
3394
|
+
try {
|
|
3395
|
+
data = JSON.parse(result.stdout || "null");
|
|
3396
|
+
} catch {
|
|
3397
|
+
data = undefined;
|
|
3398
|
+
}
|
|
3399
|
+
if (result.status === 0)
|
|
3400
|
+
return data ?? null;
|
|
3401
|
+
if (isObject(data) && isObject(data.error) && data.error.code === "E404")
|
|
3402
|
+
return;
|
|
3403
|
+
let detail = (result.stderr || result.stdout).trim();
|
|
3404
|
+
if (auth?.token)
|
|
3405
|
+
detail = detail.replaceAll(auth.token, "[REDACTED]");
|
|
3406
|
+
throw new CeError(`ce: --tolerate-republish could not query the registry with npm ${argv.slice(0, 2).join(" ")}: ${detail}`);
|
|
3407
|
+
}
|
|
3408
|
+
function releaseState({ name, version }, staged, config, auth) {
|
|
3409
|
+
const published = registryQuery(["view", `${name}@${version}`, "version", ...config], auth);
|
|
3410
|
+
if (Array.isArray(published) ? published.includes(version) : published === version)
|
|
3411
|
+
return "published";
|
|
3412
|
+
if (!staged)
|
|
3413
|
+
return;
|
|
3414
|
+
const items = registryQuery(["stage", "list", name, ...config], auth);
|
|
3415
|
+
return Array.isArray(items) && items.some((item) => isObject(item) && item.version === version && String(item.status).toLowerCase() !== "rejected") ? "staged" : undefined;
|
|
3416
|
+
}
|
|
3417
|
+
function ciReleaseTag() {
|
|
3418
|
+
try {
|
|
3419
|
+
const vars = new CiMetadata;
|
|
3420
|
+
return vars.get("version") === undefined ? "" : vars.get("tag");
|
|
3421
|
+
} catch (exc) {
|
|
3422
|
+
if (exc instanceof CeError)
|
|
3423
|
+
return "";
|
|
3424
|
+
throw exc;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function releaseTag(options) {
|
|
3428
|
+
return options.versionTagEnv === undefined ? ciReleaseTag() : envValue(options.versionTagEnv, "--version-tag-env");
|
|
3429
|
+
}
|
|
3430
|
+
async function runPackageManager(manager, given, options = {}) {
|
|
3431
|
+
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
3432
|
+
const parsed = parseNative(given);
|
|
3433
|
+
const selectedCommand = invocation(manager, given, parsed);
|
|
3434
|
+
const command = selectedCommand?.kind === "publish" ? selectedCommand : undefined;
|
|
3435
|
+
const pack = selectedCommand?.kind === "pack" ? selectedCommand : undefined;
|
|
3436
|
+
if (options.stage && !command)
|
|
3437
|
+
throw new CeError("ce: --stage requires npm publish or bun publish");
|
|
3438
|
+
const staged = options.stage || command?.staged;
|
|
3439
|
+
const client = staged ? "npm" : manager;
|
|
3440
|
+
const help = given.some((arg) => ["--help", "-h", "--version"].includes(arg));
|
|
3441
|
+
let args = given;
|
|
3442
|
+
let release;
|
|
3443
|
+
if (selectedCommand) {
|
|
3444
|
+
for (const o of parsed.options) {
|
|
3445
|
+
if (CE_FLAGS.has(o.name) && !(manager === "bun" && o.arg === "--tolerate-republish"))
|
|
3446
|
+
throw new CeError(`ce: ${o.name} must immediately follow ${manager}, before its native arguments`);
|
|
3447
|
+
requireValue(o);
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
if (!help) {
|
|
3451
|
+
if (options.packOutputEnv !== undefined && !pack)
|
|
3452
|
+
throw new CeError("ce: --pack-output-env requires npm pack");
|
|
3453
|
+
if (options.tagEnv !== undefined && !command)
|
|
3454
|
+
throw new CeError("ce: --tag-env requires npm publish or bun publish");
|
|
3455
|
+
if (options.tolerateRepublish && !command)
|
|
3456
|
+
throw new CeError("ce: --tolerate-republish requires npm publish or bun publish");
|
|
3457
|
+
if (options.versionTagEnv !== undefined && !command && !pack)
|
|
3458
|
+
throw new CeError("ce: --version-tag-env requires npm pack, npm publish or bun publish");
|
|
3459
|
+
}
|
|
3460
|
+
if (!help && pack && (options.packOutputEnv !== undefined || options.versionTagEnv !== undefined)) {
|
|
3461
|
+
if (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)
|
|
3462
|
+
throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
|
|
3463
|
+
const outputFile = options.packOutputEnv === undefined ? undefined : envValue(options.packOutputEnv, "--pack-output-env");
|
|
3464
|
+
if (outputFile !== undefined && !outputFile)
|
|
3465
|
+
throw new CeError(`ce: ${options.packOutputEnv} must name a package output file`);
|
|
3466
|
+
const versionTag = releaseTag(options);
|
|
3467
|
+
const configArgs = [];
|
|
3468
|
+
for (const o of parsed.options) {
|
|
3469
|
+
if (WORKSPACES.has(o.name) && !o.arg.endsWith("=false"))
|
|
3470
|
+
throw new CeError("ce: package metadata supports one package; select a package path instead of workspace mode");
|
|
3471
|
+
if (!o.known)
|
|
3472
|
+
throw new CeError(`ce: package metadata cannot determine the value syntax of ${o.name}; use an =value option`);
|
|
3473
|
+
configArgs.push(...given.slice(o.start, o.end));
|
|
3474
|
+
}
|
|
3475
|
+
const targets = [
|
|
3476
|
+
...parsed.words.filter((word) => word.index !== pack.start).map((word) => word.value),
|
|
3477
|
+
...parsed.rest
|
|
3478
|
+
];
|
|
3479
|
+
if (targets.length > 1 || targets.some((target) => !target))
|
|
3480
|
+
throw new CeError("ce: package metadata requires one nonempty package target");
|
|
3481
|
+
return packWithMetadata(given, configArgs, {
|
|
3482
|
+
outputFile,
|
|
3483
|
+
versionTag,
|
|
3484
|
+
dryRun
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
if (command) {
|
|
3488
|
+
const prefix = given.slice(0, command.start);
|
|
3489
|
+
let body = given.slice(command.end);
|
|
3490
|
+
if (staged && manager === "bun") {
|
|
3491
|
+
const unsupported = parsed.options.find((o) => !PORTABLE_STAGE.has(o.name));
|
|
3492
|
+
if (unsupported)
|
|
3493
|
+
throw new CeError(`ce: --stage uses npm; ${unsupported.name} is not a supported npm staging option`);
|
|
3494
|
+
}
|
|
3495
|
+
const versionTag = help ? "" : releaseTag(options);
|
|
3496
|
+
const explicitCheck = !help && options.versionTagEnv !== undefined;
|
|
3497
|
+
let selectedManifest;
|
|
3498
|
+
const metadata = () => selectedManifest ??= manifest(manager, parsed, command, explicitCheck);
|
|
3499
|
+
if (explicitCheck)
|
|
3500
|
+
checkVersionTag(metadata(), versionTag);
|
|
3501
|
+
else if (versionTag) {
|
|
3502
|
+
try {
|
|
3503
|
+
selectedManifest = manifest(manager, parsed, command, true);
|
|
3504
|
+
} catch (exc) {
|
|
3505
|
+
if (!(exc instanceof CeError))
|
|
3506
|
+
throw exc;
|
|
3507
|
+
logError(`ce: warning: not checking the package version against ${versionTag}: ${exc.message.replace(/^ce: /u, "")}`);
|
|
3508
|
+
}
|
|
3509
|
+
if (selectedManifest)
|
|
3510
|
+
checkVersionTag(selectedManifest, versionTag);
|
|
3511
|
+
}
|
|
3512
|
+
if (!help && options.tolerateRepublish && client === "bun") {
|
|
3513
|
+
if (!body.includes("--tolerate-republish"))
|
|
3514
|
+
body = ["--tolerate-republish", ...body];
|
|
3515
|
+
} else if (!help && options.tolerateRepublish) {
|
|
3516
|
+
try {
|
|
3517
|
+
selectedManifest ??= manifest(manager, parsed, command, true);
|
|
3518
|
+
} catch (exc) {
|
|
3519
|
+
if (!(exc instanceof CeError))
|
|
3520
|
+
throw exc;
|
|
3521
|
+
throw new CeError(exc.message.replace(/^ce: release validation/u, "ce: --tolerate-republish"));
|
|
3522
|
+
}
|
|
3523
|
+
const { name, version } = selectedManifest;
|
|
3524
|
+
if (typeof name !== "string" || !name || typeof version !== "string")
|
|
3525
|
+
throw new CeError("ce: --tolerate-republish needs a package name and version");
|
|
3526
|
+
const { publishConfig } = selectedManifest;
|
|
3527
|
+
const registry = isObject(publishConfig) && typeof publishConfig.registry === "string" ? publishConfig.registry : undefined;
|
|
3528
|
+
release = { name, version, registry };
|
|
3529
|
+
}
|
|
3530
|
+
let tagged = parsed.options.some((o) => o.name === "--tag");
|
|
3531
|
+
if (!help && options.tagEnv !== undefined) {
|
|
3532
|
+
const tag = envValue(options.tagEnv, "--tag-env");
|
|
3533
|
+
if (tag && !tagged) {
|
|
3534
|
+
body = [`--tag=${tag}`, ...body];
|
|
3535
|
+
tagged = true;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
if ((options.autoTag ?? true) && !help && !tagged && !(client === "npm" && (process.env.npm_config_tag || process.env.NPM_CONFIG_TAG))) {
|
|
3539
|
+
const tag = inferTag(metadata());
|
|
3540
|
+
if (tag)
|
|
3541
|
+
body = [`--tag=${tag}`, ...body];
|
|
3542
|
+
}
|
|
3543
|
+
args = [
|
|
3544
|
+
...prefix,
|
|
3545
|
+
...staged ? ["stage", "publish"] : ["publish"],
|
|
3546
|
+
...body
|
|
3547
|
+
];
|
|
3548
|
+
}
|
|
3549
|
+
let auth;
|
|
3550
|
+
const authConfig = [];
|
|
3551
|
+
if (!help && (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)) {
|
|
3552
|
+
if (!command || client !== "npm")
|
|
3553
|
+
throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
|
|
3554
|
+
if (options.tokenEnv !== undefined && options.forgejoOidc)
|
|
3555
|
+
throw new CeError("ce: choose --token-env or --forgejo-oidc, not both");
|
|
3556
|
+
if (options.tokenEnv === undefined && !options.forgejoOidc)
|
|
3557
|
+
throw new CeError("ce: --check-auth and --no-check-auth require --token-env or --forgejo-oidc");
|
|
3558
|
+
if (options.forgejoOidc && staged)
|
|
3559
|
+
throw new CeError("ce: the Forgejo npm registry does not support staged publishing; use direct publish");
|
|
3560
|
+
const native = nativeConfig(args);
|
|
3561
|
+
const registry = native.registry;
|
|
3562
|
+
authConfig.push(...native.config);
|
|
3563
|
+
auth = options.forgejoOidc ? await forgejoNpmAuth(registry?.value ?? forgejoNpmRegistry(), dryRun) : npmAuth(registry?.value, options.tokenEnv, dryRun);
|
|
3564
|
+
args = [...args];
|
|
3565
|
+
if (registry)
|
|
3566
|
+
args[registry.joined ? registry.start : registry.start + 1] = registry.joined ? `--registry=${auth.registry}` : auth.registry;
|
|
3567
|
+
else {
|
|
3568
|
+
const end = args.indexOf("--");
|
|
3569
|
+
args.splice(end < 0 ? args.length : end, 0, `--registry=${auth.registry}`);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
if (dryRun) {
|
|
3573
|
+
if (!system.which(client))
|
|
3574
|
+
logError(`ce: warning: ${client} is not on PATH, so running this here would fail`);
|
|
3575
|
+
io.stdout(`${[client, ...args].map(shellQuote).join(" ")}
|
|
3576
|
+
`);
|
|
3577
|
+
return 0;
|
|
3578
|
+
}
|
|
3579
|
+
if (auth && (options.checkAuth ?? !options.forgejoOidc)) {
|
|
3580
|
+
if (!system.which("npm")) {
|
|
3581
|
+
logError("ce: npm not found on PATH");
|
|
3582
|
+
return 1;
|
|
3583
|
+
}
|
|
3584
|
+
const result = system.run(["npm", "whoami", ...authConfig, `--registry=${auth.registry}`], undefined, auth.env);
|
|
3585
|
+
if (result.status !== 0) {
|
|
3586
|
+
logError(`ce: npm authentication failed for ${auth.registry}`);
|
|
3587
|
+
io.stderr(result.stderr.replaceAll(auth.token, "[REDACTED]"));
|
|
3588
|
+
return result.status;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
if (release) {
|
|
3592
|
+
const native = nativeConfig(args);
|
|
3593
|
+
const registry = native.registry?.value ?? release.registry;
|
|
3594
|
+
const where = registry ? [...native.config, `--registry=${registry}`] : native.config;
|
|
3595
|
+
const state = releaseState(release, staged, where, auth);
|
|
3596
|
+
if (state) {
|
|
3597
|
+
logError(`ce: ${release.name}@${release.version} is already ${state} on ${registry ?? "the configured registry"}; skipping`);
|
|
3598
|
+
return 0;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return auth ? system.exec(client, [client, ...args], auth.env) : system.exec(client, [client, ...args]);
|
|
3602
|
+
}
|
|
3603
|
+
|
|
1333
3604
|
// src/cli.ts
|
|
1334
3605
|
function helpText() {
|
|
1335
|
-
return `Usage: ${io.argv0()}
|
|
3606
|
+
return `Usage: ${io.argv0()} <tool> [ce options] [--] [command] [args]
|
|
3607
|
+
${io.argv0()} [ce options] <tool|login> [args]
|
|
3608
|
+
|
|
3609
|
+
Ce options immediately follow the tool and precede its native arguments.
|
|
3610
|
+
The before-tool form remains supported. -- explicitly ends ce option parsing.
|
|
1336
3611
|
|
|
1337
|
-
|
|
3612
|
+
Container options (docker, podman and buildah):
|
|
3613
|
+
--tag-list=NAME=TEMPLATES
|
|
3614
|
+
Define a reusable comma/whitespace-separated tag list.
|
|
3615
|
+
Use {tags.NAME} as a whole entry after an image's colon.
|
|
3616
|
+
Repeat for different names; lists may reference lists.
|
|
1338
3617
|
--tag-prefix=<prefix>
|
|
1339
3618
|
Prepend <prefix> to every tag passed via -t/--tag.
|
|
1340
3619
|
A trailing "-" is appended automatically if the prefix
|
|
@@ -1346,38 +3625,122 @@ Options:
|
|
|
1346
3625
|
--cache-from scopes stay shared. Defaults to the tag prefix
|
|
1347
3626
|
(TAG_PREFIX); set to "" to keep cache scopes shared.
|
|
1348
3627
|
Also available via CACHE_PREFIX environment variable.
|
|
1349
|
-
--cache
|
|
1350
|
-
--cache-to
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
3628
|
+
--cache-from-default
|
|
3629
|
+
Also read each gha --cache-to from its scope on the
|
|
3630
|
+
default branch, from the cache prefix as it is there,
|
|
3631
|
+
so a new branch or PR starts from its cache.
|
|
3632
|
+
Also available via CACHE_FROM_DEFAULT=true.
|
|
3633
|
+
--no-oci-labels Do not add the org.opencontainers.image.* labels and
|
|
3634
|
+
annotations (source, url, revision, version, title,
|
|
3635
|
+
description, created) that builds in a Forgejo Actions
|
|
3636
|
+
run otherwise get. A --label or --annotation with the
|
|
3637
|
+
same key wins over them.
|
|
3638
|
+
Also available via OCI_LABELS=false.
|
|
3639
|
+
Shared option:
|
|
3640
|
+
--dry-run Print the command without running the tool. For builds,
|
|
3641
|
+
include the summary and skipped tags and their guards.
|
|
3642
|
+
Template errors still exit 2;
|
|
3643
|
+
a missing tool is only a warning.
|
|
3644
|
+
Also available via CE_DRY_RUN=true.
|
|
3645
|
+
|
|
3646
|
+
Publishing options (npm and bun):
|
|
3647
|
+
--stage Use npm stage publish instead of direct publishing.
|
|
3648
|
+
The bun form also uses npm; Bun is not required to stage.
|
|
3649
|
+
--tolerate-republish
|
|
3650
|
+
Exit 0 without uploading when the version is already
|
|
3651
|
+
on the registry; with --stage, also when it is staged.
|
|
3652
|
+
Direct Bun publishing uses Bun's own option.
|
|
3653
|
+
--auto-tag Infer a prerelease dist-tag from its first label (default).
|
|
3654
|
+
--no-auto-tag Leave tag selection entirely to the package manager.
|
|
3655
|
+
Explicit --tag and publishConfig.tag take precedence.
|
|
3656
|
+
With npm, npm_config_tag/NPM_CONFIG_TAG also take precedence.
|
|
3657
|
+
Stable versions retain the native tag default and checks.
|
|
3658
|
+
--token-env=NAME Configure npm token authentication from an environment
|
|
3659
|
+
variable for this invocation. Requires native --registry.
|
|
3660
|
+
Supports npm and Bun staging; saves no credentials.
|
|
3661
|
+
--forgejo-oidc Fetch a Forgejo JWT using AUD and the Actions OIDC request
|
|
3662
|
+
variables. Requires npm direct publish; saves no
|
|
3663
|
+
credentials. No staging. --registry defaults to the
|
|
3664
|
+
repository owner's npm registry on this Forgejo server.
|
|
3665
|
+
--no-check-auth Skip the npm whoami check enabled by --token-env.
|
|
3666
|
+
--check-auth Enable that check (off by default for Forgejo).
|
|
3667
|
+
Requires --token-env or --forgejo-oidc.
|
|
3668
|
+
--tag-env=NAME Use a nonempty environment value as an explicit publish
|
|
3669
|
+
tag. Native --tag wins; empty/unset uses normal inference.
|
|
3670
|
+
--version-tag-env=NAME
|
|
3671
|
+
Match the selected package version to this Git tag or
|
|
3672
|
+
refs/tags/ ref (optional v). Empty/unset skips comparison.
|
|
3673
|
+
Without it, a build of a SemVer tag such as v1.2.3
|
|
3674
|
+
checks against that tag. Applies to publish and
|
|
3675
|
+
metadata-enabled npm pack.
|
|
3676
|
+
--pack-output-env=NAME
|
|
3677
|
+
Run npm pack with captured JSON and background lifecycle
|
|
3678
|
+
output, validate its tarball, and append tarball/version
|
|
3679
|
+
outputs to the file named by NAME (e.g. GITHUB_OUTPUT).
|
|
3680
|
+
Examples:
|
|
3681
|
+
ce npm --stage publish ./package.tgz
|
|
3682
|
+
ce bun publish --tag next
|
|
3683
|
+
|
|
3684
|
+
Container build shorthand (after build/bud):
|
|
3685
|
+
--cache=<spec> Pass <spec> to both --cache-from and --cache-to.
|
|
3686
|
+
A gha cache-from made this way leaves out mode=, which
|
|
3687
|
+
only applies to export. The same cache-from given twice
|
|
3688
|
+
is passed once.
|
|
1355
3689
|
|
|
1356
3690
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1357
3691
|
{name} The value of name; an error if it has none in this run.
|
|
1358
3692
|
{?name} Expands to nothing; leaves the tag out unless name has
|
|
1359
|
-
a value.
|
|
3693
|
+
a value. In a prefix, leaves the prefix empty instead.
|
|
3694
|
+
{!name} Keeps the tag only when name has no value.
|
|
3695
|
+
{?name=value} Requires an exact, case-sensitive value match.
|
|
3696
|
+
{?name|other} Requires either condition; adjacent guards require both.
|
|
3697
|
+
{!...} negates the entire condition, including OR.
|
|
3698
|
+
Leading guards before an image apply to its whole -t.
|
|
3699
|
+
{tags.NAME} A --tag-list's entries; only after an image's colon,
|
|
3700
|
+
as a whole comma/whitespace-separated entry.
|
|
1360
3701
|
{image} Registry host and repository, lowercased
|
|
1361
3702
|
{ref} Branch name, pr-<number>, or tag name
|
|
1362
3703
|
{branch} {pr} {tag}
|
|
1363
3704
|
Set only when building that kind of ref
|
|
1364
|
-
{default} Set only when building the default branch
|
|
3705
|
+
{default} Set only when building the default branch, named by
|
|
3706
|
+
the event file or else DEFAULT_BRANCH
|
|
1365
3707
|
{revision} {sha} The commit, in full and as 7 characters
|
|
1366
3708
|
{version} {major} {minor} {patch}
|
|
1367
3709
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1368
3710
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1369
3711
|
{env.NAME} The environment variable NAME
|
|
3712
|
+
{arg.NAME} The build arg NAME as the build gets it: from
|
|
3713
|
+
--build-arg, else the Containerfile's default for the
|
|
3714
|
+
stage built (--target's or the last)
|
|
3715
|
+
A guard that does not pass leaves its tag out or its prefix empty even if
|
|
3716
|
+
another guard there cannot be worked out.
|
|
1370
3717
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
1371
3718
|
in single quotes. Example:
|
|
1372
3719
|
-t '{image}:{ref},{?default}latest'
|
|
1373
3720
|
|
|
1374
3721
|
Commands:
|
|
1375
|
-
login
|
|
3722
|
+
<engine> login Log in with that engine using REGISTRY_* and Forgejo OIDC.
|
|
3723
|
+
Arguments after login use native login instead; -- login
|
|
3724
|
+
also selects native login without arguments.
|
|
3725
|
+
login Legacy configured login, trying docker, podman, buildah.
|
|
1376
3726
|
|
|
1377
|
-
|
|
3727
|
+
Tools:
|
|
1378
3728
|
docker [args] Passthrough to docker
|
|
1379
|
-
podman [args] Passthrough to podman
|
|
1380
|
-
buildah [args] Passthrough to buildah
|
|
3729
|
+
podman [args] Passthrough to podman
|
|
3730
|
+
buildah [args] Passthrough to buildah
|
|
3731
|
+
npm [args] Publish with inferred prerelease tags; otherwise passthrough
|
|
3732
|
+
bun [args] Publish with inferred prerelease tags; otherwise passthrough
|
|
3733
|
+
|
|
3734
|
+
Environment variables (used by builds):
|
|
3735
|
+
SBOM=true Add an SBOM: with Docker, BuildKit's attestation; with
|
|
3736
|
+
podman and buildah, syft's SPDX, written into the image
|
|
3737
|
+
at /root/buildinfo/content_manifests/sbom.spdx.json.
|
|
3738
|
+
SBOM=spdx selects the same format. SBOM=cyclonedx uses
|
|
3739
|
+
syft's CycloneDX with podman and buildah, written as
|
|
3740
|
+
sbom.cdx.json in that directory. Docker refuses it.
|
|
3741
|
+
An engine SBOM option you pass takes precedence.
|
|
3742
|
+
A podman or buildah --sbom with no output gets an image
|
|
3743
|
+
path matching its preset.
|
|
1381
3744
|
|
|
1382
3745
|
Environment variables (used by login):
|
|
1383
3746
|
Each registry is a "group". The default group uses the bare variables
|
|
@@ -1407,35 +3770,35 @@ Environment variables (used by login):
|
|
|
1407
3770
|
`;
|
|
1408
3771
|
}
|
|
1409
3772
|
async function main(argv) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
let cachePrefix = process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "";
|
|
1413
|
-
for (;; ) {
|
|
1414
|
-
const a = argv[0];
|
|
1415
|
-
if (a === "--tag-prefix")
|
|
1416
|
-
[tagPrefix, argv] = consumeValue(argv.slice(1), a);
|
|
1417
|
-
else if (a?.startsWith("--tag-prefix="))
|
|
1418
|
-
[tagPrefix, argv] = [a.slice("--tag-prefix=".length), argv.slice(1)];
|
|
1419
|
-
else if (a === "--cache-prefix")
|
|
1420
|
-
[cachePrefix, argv] = consumeValue(argv.slice(1), a);
|
|
1421
|
-
else if (a?.startsWith("--cache-prefix="))
|
|
1422
|
-
[cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
|
|
1423
|
-
else if (a === "--oci-labels")
|
|
1424
|
-
[ociLabels, argv] = [true, argv.slice(1)];
|
|
1425
|
-
else
|
|
1426
|
-
break;
|
|
1427
|
-
}
|
|
1428
|
-
const [command, ...rest] = argv;
|
|
3773
|
+
const options = defaultOptions();
|
|
3774
|
+
const [command, ...rest] = parseCeOptions(argv, options).args;
|
|
1429
3775
|
if (command === undefined) {
|
|
1430
3776
|
io.stderr(helpText());
|
|
1431
3777
|
return 0;
|
|
1432
3778
|
}
|
|
1433
|
-
if (command === "login")
|
|
1434
|
-
|
|
3779
|
+
if (command === "login") {
|
|
3780
|
+
if (rest.length)
|
|
3781
|
+
throw new CeError("ce: login takes no native arguments; use ce <engine> login, or place ce options before login");
|
|
3782
|
+
if (options.used.has("--stage"))
|
|
3783
|
+
throw new CeError("ce: --stage requires npm publish or bun publish");
|
|
3784
|
+
if (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)
|
|
3785
|
+
throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
|
|
3786
|
+
if (options.tolerateRepublish || options.tagEnv !== undefined || options.versionTagEnv !== undefined || options.packOutputEnv !== undefined)
|
|
3787
|
+
throw new CeError("ce: package preparation and release options require npm or bun");
|
|
3788
|
+
return login(undefined, options.dryRun ?? envFlag("CE_DRY_RUN"));
|
|
3789
|
+
}
|
|
1435
3790
|
if (command === "credential-helper")
|
|
1436
3791
|
return credentialHelper(rest);
|
|
1437
|
-
if (isEngine(command))
|
|
1438
|
-
|
|
3792
|
+
if (isEngine(command) || isPackageManager(command)) {
|
|
3793
|
+
validateScope(options, command);
|
|
3794
|
+
const parsed = parseCeOptions(rest, options, command);
|
|
3795
|
+
if (isEngine(command)) {
|
|
3796
|
+
if (!parsed.native && parsed.args.length === 1 && parsed.args[0] === "login")
|
|
3797
|
+
return login(command, options.dryRun ?? envFlag("CE_DRY_RUN"));
|
|
3798
|
+
return runEngine(command, parsed.args, options.tagPrefix, options.cachePrefix, options);
|
|
3799
|
+
}
|
|
3800
|
+
return runPackageManager(command, parsed.args, options);
|
|
3801
|
+
}
|
|
1439
3802
|
io.stderr(helpText());
|
|
1440
3803
|
return 1;
|
|
1441
3804
|
}
|