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

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