@izykitten/ce 0.0.3-dev.0 → 0.0.3-dev.10

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