@izykitten/ce 0.0.3-dev.1 → 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 +285 -29
- package/dist/ce.js +3318 -976
- 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,190 +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;
|
|
361
186
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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");
|
|
199
|
+
}
|
|
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`);
|
|
368
214
|
}
|
|
369
|
-
return
|
|
215
|
+
return;
|
|
370
216
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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;
|
|
376
231
|
try {
|
|
377
|
-
|
|
232
|
+
fd = fs2.openSync(lockPath, "wx", 384);
|
|
378
233
|
} catch (exc) {
|
|
379
|
-
|
|
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];
|
|
380
316
|
}
|
|
381
|
-
if (!isObject(payload))
|
|
382
|
-
throw new CeError(`ce: the event file ${JSON.stringify(file)} is missing or not a JSON object`);
|
|
383
|
-
this.#event = payload;
|
|
384
|
-
return payload;
|
|
385
317
|
}
|
|
318
|
+
writePrivateJson(configPath, config);
|
|
386
319
|
}
|
|
387
320
|
|
|
388
321
|
// src/system.ts
|
|
389
322
|
import { spawnSync } from "node:child_process";
|
|
390
|
-
import
|
|
391
|
-
import
|
|
392
|
-
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";
|
|
393
327
|
var io = {
|
|
394
|
-
stdout: (text) => void
|
|
395
|
-
stderr: (text) => void
|
|
396
|
-
stdin: () =>
|
|
328
|
+
stdout: (text) => void fs4.writeSync(1, text),
|
|
329
|
+
stderr: (text) => void fs4.writeSync(2, text),
|
|
330
|
+
stdin: () => fs4.readFileSync(0, "utf8"),
|
|
397
331
|
argv0: () => process.argv[1] ?? "ce"
|
|
398
332
|
};
|
|
399
333
|
function log(line) {
|
|
@@ -404,15 +338,33 @@ function logError(line) {
|
|
|
404
338
|
io.stderr(`${line}
|
|
405
339
|
`);
|
|
406
340
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
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));
|
|
409
361
|
}
|
|
410
362
|
function which(command) {
|
|
411
|
-
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));
|
|
412
364
|
for (const candidate of candidates) {
|
|
413
365
|
try {
|
|
414
|
-
|
|
415
|
-
if (
|
|
366
|
+
fs4.accessSync(candidate, fs4.constants.X_OK);
|
|
367
|
+
if (fs4.statSync(candidate).isFile())
|
|
416
368
|
return candidate;
|
|
417
369
|
} catch {}
|
|
418
370
|
}
|
|
@@ -420,11 +372,11 @@ function which(command) {
|
|
|
420
372
|
}
|
|
421
373
|
var system = {
|
|
422
374
|
which,
|
|
423
|
-
run(argv, input) {
|
|
375
|
+
run(argv, input, env) {
|
|
424
376
|
const result = spawnSync(argv[0], argv.slice(1), {
|
|
425
377
|
input,
|
|
426
378
|
stdio: [input === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
427
|
-
env: childEnv(),
|
|
379
|
+
env: childEnv(env),
|
|
428
380
|
encoding: "utf8",
|
|
429
381
|
maxBuffer: Infinity
|
|
430
382
|
});
|
|
@@ -432,773 +384,2290 @@ var system = {
|
|
|
432
384
|
throw result.error;
|
|
433
385
|
return { status: result.status ?? 1, stderr: result.stderr ?? "" };
|
|
434
386
|
},
|
|
435
|
-
|
|
436
|
-
|
|
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) {
|
|
437
417
|
const resolved = system.which(file);
|
|
438
418
|
if (!resolved) {
|
|
439
419
|
logError(`ce: ${file} not found on PATH`);
|
|
440
420
|
return 1;
|
|
441
421
|
}
|
|
442
|
-
process.execve(resolved, argv, childEnv());
|
|
422
|
+
process.execve(resolved, argv, childEnv(env));
|
|
443
423
|
throw new Error(`execve returned for ${file}`);
|
|
444
424
|
}
|
|
445
425
|
};
|
|
446
426
|
function binDir() {
|
|
447
|
-
return
|
|
427
|
+
return fs4.realpathSync(path3.join(import.meta.dirname, "..", "bin"));
|
|
448
428
|
}
|
|
449
429
|
function ensureHelperOnPath() {
|
|
450
430
|
const dir = binDir();
|
|
451
|
-
const parts = (process.env.PATH ?? "").split(
|
|
431
|
+
const parts = (process.env.PATH ?? "").split(path3.delimiter);
|
|
452
432
|
if (!parts.includes(dir))
|
|
453
|
-
process.env.PATH = [dir, ...parts].join(
|
|
433
|
+
process.env.PATH = [dir, ...parts].join(path3.delimiter);
|
|
454
434
|
}
|
|
455
435
|
|
|
456
|
-
// src/
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const text = (value) => {
|
|
464
|
-
if (hasPlaceholders(value))
|
|
465
|
-
throw new CeError(`ce: unmatched brace in ${JSON.stringify(template)}`);
|
|
466
|
-
if (value)
|
|
467
|
-
parts.push({ text: value });
|
|
468
|
-
};
|
|
469
|
-
let last = 0;
|
|
470
|
-
for (const match of template.matchAll(PLACEHOLDER)) {
|
|
471
|
-
text(template.slice(last, match.index));
|
|
472
|
-
last = match.index + match[0].length;
|
|
473
|
-
const guard = match[1].startsWith("?");
|
|
474
|
-
const name = guard ? match[1].slice(1) : match[1];
|
|
475
|
-
if (!vars.known(name))
|
|
476
|
-
throw new CeError(`ce: unknown placeholder ${match[0]} in ${JSON.stringify(template)}`);
|
|
477
|
-
parts.push({ name, guard });
|
|
478
|
-
}
|
|
479
|
-
text(template.slice(last));
|
|
480
|
-
return parts;
|
|
481
|
-
}
|
|
482
|
-
function expand(template, vars, encode) {
|
|
483
|
-
const parts = parse(template, vars);
|
|
484
|
-
for (const part of parts) {
|
|
485
|
-
if ("name" in part && part.guard && vars.get(part.name) === undefined)
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
let out = "";
|
|
489
|
-
for (const part of parts) {
|
|
490
|
-
if ("text" in part)
|
|
491
|
-
out += part.text;
|
|
492
|
-
else if (!part.guard) {
|
|
493
|
-
const value = vars.get(part.name);
|
|
494
|
-
if (value === undefined) {
|
|
495
|
-
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}}.`);
|
|
496
|
-
}
|
|
497
|
-
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)}`);
|
|
498
443
|
}
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
});
|
|
501
475
|
}
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
|
|
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");
|
|
505
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
|
+
});
|
|
506
490
|
}
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
`);
|
|
512
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;
|
|
513
517
|
}
|
|
514
|
-
return expand(template, vars, encode);
|
|
515
518
|
}
|
|
516
519
|
|
|
517
|
-
// src/
|
|
518
|
-
var
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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;
|
|
525
607
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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;
|
|
532
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;
|
|
533
653
|
}
|
|
654
|
+
return null;
|
|
534
655
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 = "";
|
|
541
665
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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;
|
|
548
776
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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 {}
|
|
552
793
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
throw new CeError(`ce: ${JSON.stringify(value)} cannot go in an image name`);
|
|
563
|
-
return value;
|
|
564
|
-
}
|
|
565
|
-
function expandImage(template, vars) {
|
|
566
|
-
const image = expandValue(template, vars, encodeRepoValue, "image name");
|
|
567
|
-
if (!/^(?:[^/:]+:\d+\/)?[^:]*$/u.test(image)) {
|
|
568
|
-
throw new CeError(`ce: image name ${JSON.stringify(image)} from ${JSON.stringify(template)} has a ":" that is not a registry port`);
|
|
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 [];
|
|
569
803
|
}
|
|
570
|
-
|
|
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;
|
|
571
874
|
}
|
|
572
|
-
function
|
|
573
|
-
|
|
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) };
|
|
574
878
|
}
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}
|
|
579
908
|
}
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
for (const tag of tags) {
|
|
607
|
-
const expanded = hasPlaceholders(tag) ? expand(tag, vars, encodeTagValue) : tag;
|
|
608
|
-
if (expanded === null)
|
|
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)
|
|
609
935
|
continue;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
}
|
|
617
948
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
});
|
|
637
973
|
continue;
|
|
638
974
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
+
});
|
|
649
997
|
}
|
|
650
|
-
fields.push(field);
|
|
651
|
-
if (i === value.length)
|
|
652
|
-
return fields;
|
|
653
|
-
if (value[i] !== ",")
|
|
654
|
-
return null;
|
|
655
|
-
i++;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
function csvJoin(fields) {
|
|
659
|
-
return fields.map((field) => /^[ \t]|[",\r\n]/u.test(field) ? `"${field.replaceAll('"', '""')}"` : field).join(",");
|
|
660
|
-
}
|
|
661
|
-
var fieldKey = (field) => field.split("=", 1)[0].toLowerCase();
|
|
662
|
-
function ghaFields(value) {
|
|
663
|
-
const fields = csvFields(value);
|
|
664
|
-
const type = fields?.findLast((field) => fieldKey(field) === "type");
|
|
665
|
-
return type?.slice("type=".length) === "gha" ? fields : null;
|
|
666
|
-
}
|
|
667
|
-
var scopeIndex = (fields) => fields.findLastIndex((field) => fieldKey(field) === "scope");
|
|
668
|
-
function cacheScopeName(value) {
|
|
669
|
-
const fields = ghaFields(value);
|
|
670
|
-
if (!fields)
|
|
671
|
-
return value;
|
|
672
|
-
const i = scopeIndex(fields);
|
|
673
|
-
if (i < 0)
|
|
674
|
-
return "";
|
|
675
|
-
const scope = fields[i].slice("scope=".length);
|
|
676
|
-
assertAscii("cache scope", scope);
|
|
677
|
-
return sanitizeChars(scope, INVALID_SCOPE_CHARS);
|
|
678
|
-
}
|
|
679
|
-
function normalizeCacheScope(value, cachePrefix) {
|
|
680
|
-
const fields = ghaFields(value);
|
|
681
|
-
if (!fields)
|
|
682
|
-
return value;
|
|
683
|
-
if (!cachePrefix)
|
|
684
|
-
return value;
|
|
685
|
-
assertAscii("cache prefix", cachePrefix);
|
|
686
|
-
const prefix = sanitizeChars(cachePrefix, INVALID_TAG_CHARS);
|
|
687
|
-
let scope = cacheScopeName(value);
|
|
688
|
-
if (!scope) {
|
|
689
|
-
scope = prefix.replace(/[-_.]+$/u, "");
|
|
690
|
-
} else if (prefix) {
|
|
691
|
-
scope = SEPARATORS.includes(prefix.at(-1)) ? `${prefix}${scope}` : `${prefix}-${scope}`;
|
|
692
998
|
}
|
|
693
|
-
|
|
694
|
-
if (i < 0)
|
|
695
|
-
fields.push(`scope=${scope}`);
|
|
696
|
-
else
|
|
697
|
-
fields[i] = `scope=${scope}`;
|
|
698
|
-
return csvJoin(fields);
|
|
999
|
+
return parsed;
|
|
699
1000
|
}
|
|
700
1001
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
}
|
|
708
1249
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
"--platform": "platform",
|
|
716
|
-
"--build-arg": "buildArgs",
|
|
717
|
-
"--label": "labels"
|
|
718
|
-
};
|
|
719
|
-
function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {} } = {}) {
|
|
720
|
-
const args = [];
|
|
721
|
-
const rawCacheFrom = [];
|
|
722
|
-
const rawCacheTo = [];
|
|
723
|
-
const summary = {
|
|
724
|
-
tags: [],
|
|
725
|
-
cacheFrom: [],
|
|
726
|
-
cacheTo: [],
|
|
727
|
-
buildArgs: [],
|
|
728
|
-
labels: [],
|
|
729
|
-
dockerfile: "",
|
|
730
|
-
target: "",
|
|
731
|
-
platform: "",
|
|
732
|
-
context: ""
|
|
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;
|
|
733
1256
|
};
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
templated ||= hasPlaceholders(value);
|
|
738
|
-
const refs = rewriteTagValue(value, tagPrefix, vars);
|
|
739
|
-
for (const ref of refs)
|
|
740
|
-
args.push(`${flag}${ref}`);
|
|
741
|
-
summary.tags.push(...refs);
|
|
1257
|
+
const replaced = (index) => {
|
|
1258
|
+
const stage = parsed.stages[index].name;
|
|
1259
|
+
return buildkit && stage ? contexts.find((c) => stageKey(familiar(c)) === stageKey(stage)) : undefined;
|
|
742
1260
|
};
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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 };
|
|
749
1273
|
};
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
args.push(arg, v);
|
|
780
|
-
summarize(flag, v);
|
|
781
|
-
} else {
|
|
782
|
-
args.push(arg);
|
|
783
|
-
summarize(flag, attached);
|
|
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);
|
|
784
1303
|
}
|
|
785
|
-
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
786
|
-
args.push(arg);
|
|
787
|
-
summary.dockerfile = arg.slice(2);
|
|
788
1304
|
} else {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
}
|
|
792
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)}`);
|
|
793
1465
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
summary.labels.unshift(...added);
|
|
801
|
-
if (rawCacheFrom.length || rawCacheTo.length)
|
|
802
|
-
cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
|
|
803
|
-
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
804
|
-
for (const c of rawCacheFrom) {
|
|
805
|
-
const rewritten = toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c;
|
|
806
|
-
args.push(`--cache-from=${rewritten}`);
|
|
807
|
-
summary.cacheFrom.push(rewritten);
|
|
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;
|
|
808
1472
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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;
|
|
813
1485
|
}
|
|
814
|
-
|
|
815
|
-
|
|
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 };
|
|
816
1493
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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)
|
|
824
1505
|
return;
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
}
|
|
830
1538
|
};
|
|
831
|
-
|
|
832
|
-
list("cache-from", summary.cacheFrom);
|
|
833
|
-
list("cache-to", summary.cacheTo);
|
|
834
|
-
if (summary.dockerfile)
|
|
835
|
-
out.push(` dockerfile: ${summary.dockerfile}`);
|
|
836
|
-
if (summary.target)
|
|
837
|
-
out.push(` target: ${summary.target}`);
|
|
838
|
-
if (summary.platform)
|
|
839
|
-
out.push(` platform: ${summary.platform}`);
|
|
840
|
-
list("build args", summary.buildArgs);
|
|
841
|
-
list("labels", summary.labels);
|
|
842
|
-
if (summary.context)
|
|
843
|
-
out.push(` context: ${summary.context}`);
|
|
844
|
-
io.stderr(`${out.join(`
|
|
845
|
-
`)}
|
|
1539
|
+
}
|
|
846
1540
|
|
|
847
|
-
|
|
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;
|
|
848
1573
|
}
|
|
1574
|
+
var rfc3339 = (date) => date.toISOString().replace(/\.\d{3}Z$/u, "Z");
|
|
849
1575
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
let headers;
|
|
858
|
-
try {
|
|
859
|
-
const url = new URL(requestUrl);
|
|
860
|
-
if (url.username || url.password)
|
|
861
|
-
throw new Error("credentials in URL");
|
|
862
|
-
url.searchParams.set("audience", audience);
|
|
863
|
-
tokenUrl = url.href;
|
|
864
|
-
headers = { Accept: "application/json", Authorization: `bearer ${requestToken}` };
|
|
865
|
-
new Headers(headers);
|
|
866
|
-
} catch {
|
|
867
|
-
throw new CeError("token request URL or headers are invalid");
|
|
1576
|
+
class CiMetadata {
|
|
1577
|
+
#env;
|
|
1578
|
+
#cache = new Map;
|
|
1579
|
+
#ref;
|
|
1580
|
+
#event;
|
|
1581
|
+
constructor(env = process.env) {
|
|
1582
|
+
this.#env = env;
|
|
868
1583
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const response = await fetch(tokenUrl, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
872
|
-
if (!response.ok)
|
|
873
|
-
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
874
|
-
body = await response.arrayBuffer();
|
|
875
|
-
} catch (exc) {
|
|
876
|
-
throw new CeError(`token request failed: ${errorMessage(exc)}`);
|
|
1584
|
+
known(name) {
|
|
1585
|
+
return CI_VARIABLES.includes(name) || ENV_VARIABLE.test(name);
|
|
877
1586
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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);
|
|
883
1594
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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;
|
|
889
1633
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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;
|
|
1731
|
+
}
|
|
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;
|
|
901
1746
|
}
|
|
902
|
-
const expiry = isObject(payload) ? payload.exp : undefined;
|
|
903
|
-
if (typeof expiry !== "number")
|
|
904
|
-
throw new CeError("OIDC token has no numeric 'exp' claim");
|
|
905
|
-
return expiry;
|
|
906
1747
|
}
|
|
907
|
-
|
|
908
|
-
|
|
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
|
+
};
|
|
909
1762
|
}
|
|
910
1763
|
|
|
911
|
-
// src/
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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 });
|
|
1808
|
+
}
|
|
1809
|
+
return tags;
|
|
1810
|
+
}
|
|
915
1811
|
|
|
916
|
-
// src/
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1812
|
+
// src/template.ts
|
|
1813
|
+
var PLACEHOLDER = /\{([^{}]*)\}/gu;
|
|
1814
|
+
function hasPlaceholders(value) {
|
|
1815
|
+
return value.includes("{") || value.includes("}");
|
|
1816
|
+
}
|
|
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
|
+
});
|
|
1843
|
+
} else {
|
|
1844
|
+
if (!vars.known(expression))
|
|
1845
|
+
throw new CeError(`ce: unknown placeholder ${match[0]} in ${JSON.stringify(template)}`);
|
|
1846
|
+
parts.push({ name: expression });
|
|
1847
|
+
}
|
|
930
1848
|
}
|
|
1849
|
+
text(template.slice(last));
|
|
1850
|
+
return parts;
|
|
931
1851
|
}
|
|
932
|
-
function
|
|
933
|
-
|
|
1852
|
+
function failedGuard(parts, vars) {
|
|
1853
|
+
let unknown;
|
|
1854
|
+
for (const part of parts) {
|
|
1855
|
+
if (!("guard" in part))
|
|
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;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (unknown !== undefined)
|
|
1885
|
+
throw unknown;
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
function checkTemplate(template, vars) {
|
|
1889
|
+
parse(template, vars);
|
|
1890
|
+
}
|
|
1891
|
+
function guardsPass(template, vars, onSkip) {
|
|
1892
|
+
const failure = failedGuard(parse(template, vars), vars);
|
|
1893
|
+
if (!failure)
|
|
934
1894
|
return true;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1895
|
+
onSkip?.(failure);
|
|
1896
|
+
return false;
|
|
1897
|
+
}
|
|
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;
|
|
943
1904
|
}
|
|
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}}.`);
|
|
944
1906
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const held = snapshot(guard);
|
|
956
|
-
if (held && Date.now() - held.mtimeMs > STALE_AFTER_MS) {
|
|
957
|
-
throw new CeError(`stale lock guard ${JSON.stringify(guard)}: remove it if no ce is running`);
|
|
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);
|
|
958
1917
|
}
|
|
959
|
-
return;
|
|
960
1918
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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`);
|
|
1925
|
+
}
|
|
1926
|
+
return parts;
|
|
1927
|
+
}
|
|
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)}`);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
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})`;
|
|
1944
|
+
}
|
|
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.`);
|
|
965
1959
|
}
|
|
966
|
-
} finally {
|
|
967
|
-
fs3.rmSync(guard, { force: true });
|
|
968
1960
|
}
|
|
969
1961
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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 "-".`);
|
|
1979
|
+
}
|
|
1980
|
+
return `${repo}:${tag}`;
|
|
1981
|
+
}
|
|
1982
|
+
function encodeTagValue(value) {
|
|
1983
|
+
assertAscii("placeholder value", value);
|
|
1984
|
+
return value.replace(INVALID_TAG_CHARS, "-");
|
|
1985
|
+
}
|
|
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`);
|
|
1996
|
+
}
|
|
1997
|
+
return image;
|
|
1998
|
+
}
|
|
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)}`;
|
|
2006
|
+
}
|
|
2007
|
+
return `${repo}:${sanitizeChars(tag, INVALID_TAG_CHARS)}`;
|
|
2008
|
+
}
|
|
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()];
|
|
2032
|
+
}
|
|
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)));
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return refs;
|
|
2066
|
+
}
|
|
2067
|
+
function csvFields(value) {
|
|
2068
|
+
value = value.replace(/\r?\n$/u, "");
|
|
2069
|
+
if (!value)
|
|
2070
|
+
return null;
|
|
2071
|
+
const fields = [];
|
|
2072
|
+
let i = 0;
|
|
2073
|
+
for (;; ) {
|
|
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;
|
|
982
2083
|
continue;
|
|
983
2084
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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 += '"';
|
|
990
2095
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (current?.ino === ino && current.owner === owner)
|
|
998
|
-
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++;
|
|
999
2102
|
}
|
|
1000
2103
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
var HELPER_SUFFIX = "ce-forgejo";
|
|
1004
|
-
var STATE_FILE = "ce-forgejo-credentials.json";
|
|
1005
|
-
function dockerConfigDir() {
|
|
1006
|
-
const configured = process.env.DOCKER_CONFIG;
|
|
1007
|
-
if (!configured)
|
|
1008
|
-
return path3.join(homedir(), ".docker");
|
|
1009
|
-
const expanded = configured === "~" || configured.startsWith("~/") ? path3.join(homedir(), configured.slice(1)) : configured;
|
|
1010
|
-
return path3.resolve(expanded);
|
|
1011
|
-
}
|
|
1012
|
-
function helperStatePath() {
|
|
1013
|
-
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(",");
|
|
1014
2106
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
if (!isObject(state) || !isObject(state.registries)) {
|
|
1021
|
-
throw new CeError("Forgejo credential-helper state is invalid");
|
|
1022
|
-
}
|
|
1023
|
-
const before = JSON.stringify(state);
|
|
1024
|
-
const result = await fn(state);
|
|
1025
|
-
if (JSON.stringify(state) !== before)
|
|
1026
|
-
writePrivateJson(statePath, state);
|
|
1027
|
-
return result;
|
|
1028
|
-
});
|
|
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;
|
|
1029
2112
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
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);
|
|
1043
2125
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (!
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
for (const key of Object.keys(auths)) {
|
|
1058
|
-
if (registryKey(key) === registryKey(registry))
|
|
1059
|
-
delete auths[key];
|
|
1060
|
-
}
|
|
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}`;
|
|
1061
2139
|
}
|
|
1062
|
-
|
|
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"));
|
|
1063
2152
|
}
|
|
1064
2153
|
|
|
1065
|
-
// src/
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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;
|
|
1094
2201
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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;
|
|
1099
2231
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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;
|
|
1104
2243
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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);
|
|
2309
|
+
}
|
|
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
|
+
}
|
|
2398
|
+
}
|
|
1113
2399
|
}
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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}`);
|
|
1118
2444
|
});
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if (argv.length !== 1 || !["get", "store", "erase", "list"].includes(command)) {
|
|
1123
|
-
logError("ce: credential-helper requires get|store|erase|list");
|
|
1124
|
-
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(""));
|
|
1125
2448
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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);
|
|
1146
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
|
+
`);
|
|
1147
2575
|
}
|
|
1148
2576
|
|
|
1149
2577
|
// src/engine.ts
|
|
1150
2578
|
var isEngine = (value) => ["docker", "podman", "buildah"].includes(value);
|
|
1151
2579
|
var WAIT_ATTEMPTS = 30;
|
|
1152
|
-
async function waitForEngine(engine) {
|
|
2580
|
+
async function waitForEngine(engine, options = []) {
|
|
1153
2581
|
if (engine === "buildah")
|
|
1154
2582
|
return true;
|
|
2583
|
+
const argv = [engine, ...options, "info"];
|
|
2584
|
+
let stderr = "";
|
|
1155
2585
|
for (let attempt = 1;attempt <= WAIT_ATTEMPTS; attempt++) {
|
|
1156
|
-
|
|
2586
|
+
const result = system.run(argv);
|
|
2587
|
+
if (result.status === 0)
|
|
1157
2588
|
return true;
|
|
2589
|
+
stderr = result.stderr.trim();
|
|
1158
2590
|
logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
|
|
1159
2591
|
await system.sleep(1000);
|
|
1160
2592
|
}
|
|
1161
2593
|
logError(`no engine available: ${engine}`);
|
|
2594
|
+
if (stderr)
|
|
2595
|
+
logError(`${argv.map(shellQuote).join(" ")} last said:
|
|
2596
|
+
${stderr}`);
|
|
1162
2597
|
return false;
|
|
1163
2598
|
}
|
|
1164
|
-
function
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
+
}
|
|
1171
2615
|
}
|
|
1172
|
-
|
|
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();
|
|
1173
2625
|
const command = findBuildCommand(engine, argv);
|
|
1174
2626
|
let args = argv;
|
|
1175
2627
|
let summary;
|
|
1176
2628
|
if (command) {
|
|
1177
2629
|
const vars = new CiMetadata;
|
|
1178
|
-
const labels = !command.tagsOnly && (ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1179
|
-
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
|
+
});
|
|
1180
2643
|
args = [...argv.slice(0, command.start), ...parsed.args];
|
|
1181
2644
|
summary = parsed.summary;
|
|
1182
2645
|
}
|
|
1183
|
-
|
|
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
|
+
}
|
|
1184
2663
|
if (!system.which(engine)) {
|
|
1185
2664
|
logError(`Engine not found: ${engine}`);
|
|
1186
2665
|
return 1;
|
|
1187
2666
|
}
|
|
1188
|
-
if (!await waitForEngine(engine))
|
|
2667
|
+
if (!await waitForEngine(engine, command?.engineOptions))
|
|
1189
2668
|
return 1;
|
|
1190
2669
|
if (summary)
|
|
1191
2670
|
printBuildSummary(engine, args, summary);
|
|
1192
|
-
if (engine === "docker")
|
|
1193
|
-
return system.exec("docker", ["docker", ...args]);
|
|
1194
|
-
if (system.which("gosu")) {
|
|
1195
|
-
const user = process.env.USER ?? "";
|
|
1196
|
-
if (!user) {
|
|
1197
|
-
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1198
|
-
return 1;
|
|
1199
|
-
}
|
|
1200
|
-
return system.exec("gosu", ["gosu", user, engine, ...args]);
|
|
1201
|
-
}
|
|
1202
2671
|
return system.exec(engine, [engine, ...args]);
|
|
1203
2672
|
}
|
|
1204
2673
|
|
|
@@ -1218,26 +2687,41 @@ function oidcConfig() {
|
|
|
1218
2687
|
return null;
|
|
1219
2688
|
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
1220
2689
|
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
1221
|
-
const missing = [
|
|
2690
|
+
const missing = [
|
|
2691
|
+
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
2692
|
+
"ACTIONS_ID_TOKEN_REQUEST_TOKEN"
|
|
2693
|
+
].filter((name) => !process.env[name]);
|
|
1222
2694
|
if (missing.length) {
|
|
1223
2695
|
return `Forgejo OIDC login config is incomplete: AUD is set, but ${missing.map(unsetOrEmpty).join("; ")}`;
|
|
1224
2696
|
}
|
|
1225
2697
|
try {
|
|
1226
2698
|
assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
|
|
1227
|
-
return {
|
|
2699
|
+
return {
|
|
2700
|
+
audience,
|
|
2701
|
+
requestUrl,
|
|
2702
|
+
requestToken,
|
|
2703
|
+
authority: urlAuthority(requestUrl)
|
|
2704
|
+
};
|
|
1228
2705
|
} catch (exc) {
|
|
1229
2706
|
if (!(exc instanceof CeError))
|
|
1230
2707
|
throw exc;
|
|
1231
2708
|
return `Invalid ACTIONS_ID_TOKEN_REQUEST_URL: ${exc.message}`;
|
|
1232
2709
|
}
|
|
1233
2710
|
}
|
|
1234
|
-
function loginWithEngines(target, password) {
|
|
1235
|
-
for (const engine of
|
|
2711
|
+
function loginWithEngines(target, password, engines) {
|
|
2712
|
+
for (const engine of engines) {
|
|
1236
2713
|
if (!system.which(engine))
|
|
1237
2714
|
continue;
|
|
1238
2715
|
let result;
|
|
1239
2716
|
try {
|
|
1240
|
-
result = system.run([
|
|
2717
|
+
result = system.run([
|
|
2718
|
+
engine,
|
|
2719
|
+
"login",
|
|
2720
|
+
"-u",
|
|
2721
|
+
target.user,
|
|
2722
|
+
"--password-stdin",
|
|
2723
|
+
target.registry
|
|
2724
|
+
], `${password}
|
|
1241
2725
|
`);
|
|
1242
2726
|
} catch (exc) {
|
|
1243
2727
|
if (isErrno(exc, "ENOENT"))
|
|
@@ -1251,12 +2735,13 @@ function loginWithEngines(target, password) {
|
|
|
1251
2735
|
}
|
|
1252
2736
|
return null;
|
|
1253
2737
|
}
|
|
1254
|
-
async function login() {
|
|
2738
|
+
async function login(engine, dryRun = false) {
|
|
1255
2739
|
ensureHelperOnPath();
|
|
2740
|
+
const engines = engine ? [engine] : ENGINES;
|
|
1256
2741
|
const groups = registryGroups();
|
|
1257
2742
|
if (groups.length === 0) {
|
|
1258
|
-
|
|
1259
|
-
return
|
|
2743
|
+
logError("ce: no registries to log in to; set REGISTRY_HOST, REGISTRY_USER and REGISTRY_PASSWORD");
|
|
2744
|
+
return 1;
|
|
1260
2745
|
}
|
|
1261
2746
|
const oidc = oidcConfig();
|
|
1262
2747
|
if (typeof oidc === "string") {
|
|
@@ -1280,7 +2765,10 @@ async function login() {
|
|
|
1280
2765
|
try {
|
|
1281
2766
|
const authority = urlAuthority(rawRegistry);
|
|
1282
2767
|
const registry = registryServer(rawRegistry);
|
|
1283
|
-
registries.push({
|
|
2768
|
+
registries.push({
|
|
2769
|
+
registry,
|
|
2770
|
+
useOidc: oidc !== null && sameAuthority(authority, oidc.authority)
|
|
2771
|
+
});
|
|
1284
2772
|
} catch (exc) {
|
|
1285
2773
|
if (!(exc instanceof CeError))
|
|
1286
2774
|
throw exc;
|
|
@@ -1309,6 +2797,17 @@ async function login() {
|
|
|
1309
2797
|
targets.push({ registry, label, user, password, useOidc });
|
|
1310
2798
|
}
|
|
1311
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;
|
|
2810
|
+
}
|
|
1312
2811
|
let jwt = null;
|
|
1313
2812
|
if (oidc && targets.some((target) => target.useOidc)) {
|
|
1314
2813
|
try {
|
|
@@ -1329,14 +2828,14 @@ async function login() {
|
|
|
1329
2828
|
continue;
|
|
1330
2829
|
}
|
|
1331
2830
|
log(`Logging in to ${registry} (${label})...`);
|
|
1332
|
-
const
|
|
1333
|
-
if (!
|
|
2831
|
+
const used = loginWithEngines(target, password, engines);
|
|
2832
|
+
if (!used) {
|
|
1334
2833
|
logError(`Login failed for '${label}' (${registry})`);
|
|
1335
2834
|
failed = true;
|
|
1336
2835
|
continue;
|
|
1337
2836
|
}
|
|
1338
|
-
log(`Logged in to ${registry} (${label}) via ${
|
|
1339
|
-
if (target.useOidc && oidc && jwt) {
|
|
2837
|
+
log(`Logged in to ${registry} (${label}) via ${used}`);
|
|
2838
|
+
if (target.useOidc && oidc && jwt && (!engine || engine === "docker")) {
|
|
1340
2839
|
try {
|
|
1341
2840
|
await configureDockerCredentialHelper(registry, target.user, oidc.audience, oidc.authority, jwt);
|
|
1342
2841
|
} catch (exc) {
|
|
@@ -1350,11 +2849,771 @@ async function login() {
|
|
|
1350
2849
|
return failed ? 1 : 0;
|
|
1351
2850
|
}
|
|
1352
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
|
+
|
|
1353
3604
|
// src/cli.ts
|
|
1354
3605
|
function helpText() {
|
|
1355
|
-
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.
|
|
1356
3611
|
|
|
1357
|
-
|
|
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.
|
|
1358
3617
|
--tag-prefix=<prefix>
|
|
1359
3618
|
Prepend <prefix> to every tag passed via -t/--tag.
|
|
1360
3619
|
A trailing "-" is appended automatically if the prefix
|
|
@@ -1366,17 +3625,79 @@ Options:
|
|
|
1366
3625
|
--cache-from scopes stay shared. Defaults to the tag prefix
|
|
1367
3626
|
(TAG_PREFIX); set to "" to keep cache scopes shared.
|
|
1368
3627
|
Also available via CACHE_PREFIX environment variable.
|
|
1369
|
-
--cache
|
|
1370
|
-
--cache-to
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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.
|
|
1375
3689
|
|
|
1376
3690
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1377
3691
|
{name} The value of name; an error if it has none in this run.
|
|
1378
3692
|
{?name} Expands to nothing; leaves the tag out unless name has
|
|
1379
|
-
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.
|
|
1380
3701
|
{image} Registry host and repository, lowercased
|
|
1381
3702
|
{ref} Branch name, pr-<number>, or tag name
|
|
1382
3703
|
{branch} {pr} {tag}
|
|
@@ -1388,17 +3709,38 @@ Placeholders, in build tags and in the tag and cache prefixes:
|
|
|
1388
3709
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1389
3710
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1390
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.
|
|
1391
3717
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
1392
3718
|
in single quotes. Example:
|
|
1393
3719
|
-t '{image}:{ref},{?default}latest'
|
|
1394
3720
|
|
|
1395
3721
|
Commands:
|
|
1396
|
-
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.
|
|
1397
3726
|
|
|
1398
|
-
|
|
3727
|
+
Tools:
|
|
1399
3728
|
docker [args] Passthrough to docker
|
|
1400
|
-
podman [args] Passthrough to podman
|
|
1401
|
-
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.
|
|
1402
3744
|
|
|
1403
3745
|
Environment variables (used by login):
|
|
1404
3746
|
Each registry is a "group". The default group uses the bare variables
|
|
@@ -1428,35 +3770,35 @@ Environment variables (used by login):
|
|
|
1428
3770
|
`;
|
|
1429
3771
|
}
|
|
1430
3772
|
async function main(argv) {
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
let cachePrefix = process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "";
|
|
1434
|
-
for (;; ) {
|
|
1435
|
-
const a = argv[0];
|
|
1436
|
-
if (a === "--tag-prefix")
|
|
1437
|
-
[tagPrefix, argv] = consumeValue(argv.slice(1), a);
|
|
1438
|
-
else if (a?.startsWith("--tag-prefix="))
|
|
1439
|
-
[tagPrefix, argv] = [a.slice("--tag-prefix=".length), argv.slice(1)];
|
|
1440
|
-
else if (a === "--cache-prefix")
|
|
1441
|
-
[cachePrefix, argv] = consumeValue(argv.slice(1), a);
|
|
1442
|
-
else if (a?.startsWith("--cache-prefix="))
|
|
1443
|
-
[cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
|
|
1444
|
-
else if (a === "--oci-labels")
|
|
1445
|
-
[ociLabels, argv] = [true, argv.slice(1)];
|
|
1446
|
-
else
|
|
1447
|
-
break;
|
|
1448
|
-
}
|
|
1449
|
-
const [command, ...rest] = argv;
|
|
3773
|
+
const options = defaultOptions();
|
|
3774
|
+
const [command, ...rest] = parseCeOptions(argv, options).args;
|
|
1450
3775
|
if (command === undefined) {
|
|
1451
3776
|
io.stderr(helpText());
|
|
1452
3777
|
return 0;
|
|
1453
3778
|
}
|
|
1454
|
-
if (command === "login")
|
|
1455
|
-
|
|
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
|
+
}
|
|
1456
3790
|
if (command === "credential-helper")
|
|
1457
3791
|
return credentialHelper(rest);
|
|
1458
|
-
if (isEngine(command))
|
|
1459
|
-
|
|
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
|
+
}
|
|
1460
3802
|
io.stderr(helpText());
|
|
1461
3803
|
return 1;
|
|
1462
3804
|
}
|