@izykitten/ce 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/bin/ce +2 -2
- package/bin/docker-credential-ce-forgejo +2 -2
- package/dist/ce.js +946 -0
- package/package.json +10 -3
- package/src/ce.ts +0 -1189
- package/src/libc.ts +0 -104
package/dist/ce.js
ADDED
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
class CeError extends Error {
|
|
3
|
+
name = "CeError";
|
|
4
|
+
}
|
|
5
|
+
function isErrno(exc, code) {
|
|
6
|
+
if (!(exc instanceof Error))
|
|
7
|
+
return false;
|
|
8
|
+
const actual = exc.code;
|
|
9
|
+
return typeof actual === "string" && (code === undefined || actual === code);
|
|
10
|
+
}
|
|
11
|
+
function errorMessage(exc) {
|
|
12
|
+
return exc instanceof Error ? exc.message : String(exc);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/system.ts
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { setTimeout } from "node:timers/promises";
|
|
20
|
+
var io = {
|
|
21
|
+
stdout: (text) => void fs.writeSync(1, text),
|
|
22
|
+
stderr: (text) => void fs.writeSync(2, text),
|
|
23
|
+
stdin: () => fs.readFileSync(0, "utf8"),
|
|
24
|
+
argv0: () => process.argv[1] ?? "ce"
|
|
25
|
+
};
|
|
26
|
+
function log(line) {
|
|
27
|
+
io.stdout(`${line}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
function logError(line) {
|
|
31
|
+
io.stderr(`${line}
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function childEnv() {
|
|
35
|
+
return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
36
|
+
}
|
|
37
|
+
function which(command) {
|
|
38
|
+
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, command));
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
try {
|
|
41
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
42
|
+
if (fs.statSync(candidate).isFile())
|
|
43
|
+
return candidate;
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
var system = {
|
|
49
|
+
which,
|
|
50
|
+
run(argv, input) {
|
|
51
|
+
const result = spawnSync(argv[0], argv.slice(1), {
|
|
52
|
+
input,
|
|
53
|
+
stdio: [input === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
54
|
+
env: childEnv(),
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
maxBuffer: Infinity
|
|
57
|
+
});
|
|
58
|
+
if (result.error)
|
|
59
|
+
throw result.error;
|
|
60
|
+
return { status: result.status ?? 1, stderr: result.stderr ?? "" };
|
|
61
|
+
},
|
|
62
|
+
sleep: (ms) => setTimeout(ms),
|
|
63
|
+
exec(file, argv) {
|
|
64
|
+
const resolved = system.which(file);
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
logError(`ce: ${file} not found on PATH`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
process.execve(resolved, argv, childEnv());
|
|
70
|
+
throw new Error(`execve returned for ${file}`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function binDir() {
|
|
74
|
+
return fs.realpathSync(path.join(import.meta.dirname, "..", "bin"));
|
|
75
|
+
}
|
|
76
|
+
function ensureHelperOnPath() {
|
|
77
|
+
const dir = binDir();
|
|
78
|
+
const parts = (process.env.PATH ?? "").split(path.delimiter);
|
|
79
|
+
if (!parts.includes(dir))
|
|
80
|
+
process.env.PATH = [dir, ...parts].join(path.delimiter);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/tags.ts
|
|
84
|
+
var INVALID_TAG_CHARS = /[^A-Za-z0-9_.\-]/gu;
|
|
85
|
+
var INVALID_SCOPE_CHARS = /[^A-Za-z0-9_.:/\-]/gu;
|
|
86
|
+
var RENOVATE_RE = /^(.*)\/renovate_(.*)$/u;
|
|
87
|
+
var GHA_RE = /^type=gha(,mode=max)?(?:,scope=(.*))?$/u;
|
|
88
|
+
var SEPARATORS = "-_.";
|
|
89
|
+
function sanitizeChars(value, charRegex) {
|
|
90
|
+
return value.replace(charRegex, "_");
|
|
91
|
+
}
|
|
92
|
+
function assertAscii(label, value) {
|
|
93
|
+
for (const ch of value) {
|
|
94
|
+
const code = ch.codePointAt(0);
|
|
95
|
+
if (code >= 128) {
|
|
96
|
+
const hex = code.toString(16).toUpperCase().padStart(4, "0");
|
|
97
|
+
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.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function stripProto(value) {
|
|
102
|
+
return value.replace(/^https?:\/\//u, "");
|
|
103
|
+
}
|
|
104
|
+
function splitOnce(value, sep) {
|
|
105
|
+
const i = value.indexOf(sep);
|
|
106
|
+
return i < 0 ? [value, ""] : [value.slice(0, i), value.slice(i + sep.length)];
|
|
107
|
+
}
|
|
108
|
+
function sanitizeTag(value) {
|
|
109
|
+
assertAscii("tag", value);
|
|
110
|
+
value = stripProto(value);
|
|
111
|
+
let out = value;
|
|
112
|
+
if (value.includes(":")) {
|
|
113
|
+
const [repo, tag] = splitOnce(value, ":");
|
|
114
|
+
if (tag.includes("/")) {
|
|
115
|
+
const [first, rest] = splitOnce(tag, "/");
|
|
116
|
+
out = `${repo}/${sanitizeChars(first, INVALID_TAG_CHARS)}:${sanitizeChars(rest, INVALID_TAG_CHARS)}`;
|
|
117
|
+
} else {
|
|
118
|
+
out = `${repo}:${sanitizeChars(tag, INVALID_TAG_CHARS)}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const m = RENOVATE_RE.exec(out);
|
|
122
|
+
return m ? `${m[1]}/renovate:${m[2]}` : out;
|
|
123
|
+
}
|
|
124
|
+
function computeSafePrefix(prefix) {
|
|
125
|
+
assertAscii("tag prefix", prefix);
|
|
126
|
+
const p = sanitizeChars(prefix, INVALID_TAG_CHARS);
|
|
127
|
+
return p && !SEPARATORS.includes(p.at(-1)) ? `${p}-` : p;
|
|
128
|
+
}
|
|
129
|
+
function rewriteTagValue(value, tagPrefix) {
|
|
130
|
+
const v = stripProto(value);
|
|
131
|
+
if (!v.includes(":")) {
|
|
132
|
+
assertAscii("tag", v);
|
|
133
|
+
return [v];
|
|
134
|
+
}
|
|
135
|
+
const [repo, tags] = splitOnce(v, ":");
|
|
136
|
+
const safePrefix = computeSafePrefix(tagPrefix);
|
|
137
|
+
return tags.split(/[,\s]+/u).filter(Boolean).map((tag) => sanitizeTag(`${repo}:${safePrefix}${tag}`)).filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
function cacheScopeName(value) {
|
|
140
|
+
const m = GHA_RE.exec(value);
|
|
141
|
+
if (!m)
|
|
142
|
+
return value;
|
|
143
|
+
const scope = m[2];
|
|
144
|
+
if (scope === undefined)
|
|
145
|
+
return "";
|
|
146
|
+
assertAscii("cache scope", scope);
|
|
147
|
+
return sanitizeChars(scope, INVALID_SCOPE_CHARS);
|
|
148
|
+
}
|
|
149
|
+
function normalizeCacheScope(value, cachePrefix) {
|
|
150
|
+
const m = GHA_RE.exec(value);
|
|
151
|
+
if (!m)
|
|
152
|
+
return value;
|
|
153
|
+
if (!cachePrefix)
|
|
154
|
+
return value;
|
|
155
|
+
assertAscii("cache prefix", cachePrefix);
|
|
156
|
+
const prefix = sanitizeChars(cachePrefix, INVALID_TAG_CHARS);
|
|
157
|
+
let scope = splitOnce(cacheScopeName(value), ",")[0];
|
|
158
|
+
if (!scope) {
|
|
159
|
+
scope = prefix.replace(/[-_.]+$/u, "");
|
|
160
|
+
} else if (prefix) {
|
|
161
|
+
scope = SEPARATORS.includes(prefix.at(-1)) ? `${prefix}${scope}` : `${prefix}-${scope}`;
|
|
162
|
+
}
|
|
163
|
+
return m[1] ? `type=gha,mode=max,scope=${scope}` : `type=gha,scope=${scope}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/build-args.ts
|
|
167
|
+
function consumeValue(argv, flag) {
|
|
168
|
+
if (argv.length === 0)
|
|
169
|
+
throw new CeError(`ce: ${flag} requires a value but none was provided.`);
|
|
170
|
+
const v = argv[0];
|
|
171
|
+
if (v.length > 1 && v.startsWith("-")) {
|
|
172
|
+
throw new CeError(`ce: ${flag} requires a value but the next token ${JSON.stringify(v)} looks like a flag.`);
|
|
173
|
+
}
|
|
174
|
+
return [v, argv.slice(1)];
|
|
175
|
+
}
|
|
176
|
+
var SUMMARIZED = {
|
|
177
|
+
"-f": "dockerfile",
|
|
178
|
+
"--file": "dockerfile",
|
|
179
|
+
"--target": "target",
|
|
180
|
+
"--platform": "platform",
|
|
181
|
+
"--build-arg": "buildArgs"
|
|
182
|
+
};
|
|
183
|
+
function parseBuildArgs(argv, tagPrefix, cachePrefix) {
|
|
184
|
+
const args = [];
|
|
185
|
+
const rawCacheFrom = [];
|
|
186
|
+
const rawCacheTo = [];
|
|
187
|
+
const summary = {
|
|
188
|
+
tags: [],
|
|
189
|
+
cacheFrom: [],
|
|
190
|
+
cacheTo: [],
|
|
191
|
+
buildArgs: [],
|
|
192
|
+
dockerfile: "",
|
|
193
|
+
target: "",
|
|
194
|
+
platform: "",
|
|
195
|
+
context: ""
|
|
196
|
+
};
|
|
197
|
+
const addTags = (value, flag) => {
|
|
198
|
+
const refs = rewriteTagValue(value, tagPrefix);
|
|
199
|
+
for (const ref of refs)
|
|
200
|
+
args.push(`${flag}${ref}`);
|
|
201
|
+
summary.tags.push(...refs);
|
|
202
|
+
};
|
|
203
|
+
const summarize = (flag, value) => {
|
|
204
|
+
const field = SUMMARIZED[flag];
|
|
205
|
+
if (field === "buildArgs")
|
|
206
|
+
summary.buildArgs.push(value);
|
|
207
|
+
else
|
|
208
|
+
summary[field] = value;
|
|
209
|
+
};
|
|
210
|
+
for (let i = 0;i < argv.length; i++) {
|
|
211
|
+
const arg = argv[i];
|
|
212
|
+
const [name, attached] = arg.startsWith("--") && arg.includes("=") ? splitAt(arg, "=") : [arg, undefined];
|
|
213
|
+
const next = () => consumeValue(argv.slice(++i), arg)[0];
|
|
214
|
+
if (arg === "-t")
|
|
215
|
+
addTags(next(), "-t");
|
|
216
|
+
else if (arg === "--tag")
|
|
217
|
+
addTags(next(), "--tag=");
|
|
218
|
+
else if (arg.startsWith("-t"))
|
|
219
|
+
addTags(arg.slice(2), "-t");
|
|
220
|
+
else if (name === "--tag")
|
|
221
|
+
addTags(attached, "--tag=");
|
|
222
|
+
else if (name === "--cache-from")
|
|
223
|
+
rawCacheFrom.push(attached ?? next());
|
|
224
|
+
else if (name === "--cache-to")
|
|
225
|
+
rawCacheTo.push(attached ?? next());
|
|
226
|
+
else if (name === "--cache") {
|
|
227
|
+
const v = attached ?? next();
|
|
228
|
+
rawCacheFrom.push(v);
|
|
229
|
+
rawCacheTo.push(v);
|
|
230
|
+
} else if (Object.hasOwn(SUMMARIZED, name)) {
|
|
231
|
+
const flag = name;
|
|
232
|
+
if (attached === undefined) {
|
|
233
|
+
const v = next();
|
|
234
|
+
args.push(arg, v);
|
|
235
|
+
summarize(flag, v);
|
|
236
|
+
} else {
|
|
237
|
+
args.push(arg);
|
|
238
|
+
summarize(flag, attached);
|
|
239
|
+
}
|
|
240
|
+
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
241
|
+
args.push(arg);
|
|
242
|
+
summary.dockerfile = arg.slice(2);
|
|
243
|
+
} else {
|
|
244
|
+
args.push(arg);
|
|
245
|
+
if (!arg.startsWith("-"))
|
|
246
|
+
summary.context = arg;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
250
|
+
for (const c of rawCacheFrom) {
|
|
251
|
+
const rewritten = toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c;
|
|
252
|
+
args.push(`--cache-from=${rewritten}`);
|
|
253
|
+
summary.cacheFrom.push(rewritten);
|
|
254
|
+
}
|
|
255
|
+
for (const c of rawCacheTo) {
|
|
256
|
+
const rewritten = normalizeCacheScope(c, cachePrefix);
|
|
257
|
+
args.push(`--cache-to=${rewritten}`);
|
|
258
|
+
summary.cacheTo.push(rewritten);
|
|
259
|
+
}
|
|
260
|
+
return { args, summary };
|
|
261
|
+
}
|
|
262
|
+
function splitAt(value, sep) {
|
|
263
|
+
const i = value.indexOf(sep);
|
|
264
|
+
return [value.slice(0, i), value.slice(i + 1)];
|
|
265
|
+
}
|
|
266
|
+
function printBuildSummary(engine, args, summary) {
|
|
267
|
+
const subcommand = args.find((arg) => !arg.startsWith("-") && arg !== "buildx");
|
|
268
|
+
if (subcommand !== "build" && subcommand !== "bud")
|
|
269
|
+
return;
|
|
270
|
+
const out = ["[ce] build summary:", ` engine: ${engine}`, ` subcommand: ${subcommand}`];
|
|
271
|
+
const list = (title, items) => {
|
|
272
|
+
if (items.length)
|
|
273
|
+
out.push(` ${title}:`, ...items.map((item) => ` - ${item}`));
|
|
274
|
+
};
|
|
275
|
+
list("tags", summary.tags);
|
|
276
|
+
list("cache-from", summary.cacheFrom);
|
|
277
|
+
list("cache-to", summary.cacheTo);
|
|
278
|
+
if (summary.dockerfile)
|
|
279
|
+
out.push(` dockerfile: ${summary.dockerfile}`);
|
|
280
|
+
if (summary.target)
|
|
281
|
+
out.push(` target: ${summary.target}`);
|
|
282
|
+
if (summary.platform)
|
|
283
|
+
out.push(` platform: ${summary.platform}`);
|
|
284
|
+
list("build args", summary.buildArgs);
|
|
285
|
+
if (summary.context)
|
|
286
|
+
out.push(` context: ${summary.context}`);
|
|
287
|
+
io.stderr(`${out.join(`
|
|
288
|
+
`)}
|
|
289
|
+
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/json.ts
|
|
294
|
+
import { randomUUID } from "node:crypto";
|
|
295
|
+
import fs2 from "node:fs";
|
|
296
|
+
import path2 from "node:path";
|
|
297
|
+
function isObject(value) {
|
|
298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
299
|
+
}
|
|
300
|
+
function readJsonFile(file, fallback) {
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(fs2.readFileSync(file)));
|
|
303
|
+
} catch (exc) {
|
|
304
|
+
if (isErrno(exc, "ENOENT"))
|
|
305
|
+
return fallback;
|
|
306
|
+
throw new CeError(`cannot read JSON file ${JSON.stringify(file)}: ${errorMessage(exc)}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function dumpJson(payload) {
|
|
310
|
+
return JSON.stringify(payload, (_key, value) => isObject(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : value);
|
|
311
|
+
}
|
|
312
|
+
function writePrivateJson(file, payload) {
|
|
313
|
+
const directory = path2.dirname(file);
|
|
314
|
+
fs2.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
315
|
+
const temporary = path2.join(directory, `.ce-forgejo-${randomUUID().slice(0, 8)}`);
|
|
316
|
+
const fd = fs2.openSync(temporary, "wx", 384);
|
|
317
|
+
try {
|
|
318
|
+
try {
|
|
319
|
+
fs2.fchmodSync(fd, 384);
|
|
320
|
+
fs2.writeSync(fd, `${dumpJson(payload)}
|
|
321
|
+
`);
|
|
322
|
+
fs2.fsyncSync(fd);
|
|
323
|
+
} finally {
|
|
324
|
+
fs2.closeSync(fd);
|
|
325
|
+
}
|
|
326
|
+
fs2.renameSync(temporary, file);
|
|
327
|
+
} finally {
|
|
328
|
+
fs2.rmSync(temporary, { force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/url.ts
|
|
333
|
+
function sameAuthority(a, b) {
|
|
334
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
335
|
+
}
|
|
336
|
+
var SCHEME_RE = /^[a-z][a-z\d+.-]*:\/\//iu;
|
|
337
|
+
function parseHttpUrl(value) {
|
|
338
|
+
const raw = value.trim();
|
|
339
|
+
if (!raw)
|
|
340
|
+
throw new CeError("empty URL or host");
|
|
341
|
+
let url;
|
|
342
|
+
try {
|
|
343
|
+
url = new URL(SCHEME_RE.test(raw) ? raw : `https://${raw}`);
|
|
344
|
+
} catch {
|
|
345
|
+
throw new CeError(`invalid URL or host ${JSON.stringify(value)}`);
|
|
346
|
+
}
|
|
347
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
348
|
+
throw new CeError(`unsupported URL scheme ${JSON.stringify(url.protocol.slice(0, -1))}`);
|
|
349
|
+
}
|
|
350
|
+
if (!url.hostname)
|
|
351
|
+
throw new CeError(`missing hostname in ${JSON.stringify(value)}`);
|
|
352
|
+
return url;
|
|
353
|
+
}
|
|
354
|
+
function urlAuthority(value) {
|
|
355
|
+
const url = parseHttpUrl(value);
|
|
356
|
+
const port = url.port ? Number(url.port) : url.protocol === "http:" ? 80 : 443;
|
|
357
|
+
return [url.hostname.replace(/^\[(.*)\]$/u, "$1"), port];
|
|
358
|
+
}
|
|
359
|
+
function assertAbsoluteHttpUrl(value, error) {
|
|
360
|
+
try {
|
|
361
|
+
if (!SCHEME_RE.test(value.trim()))
|
|
362
|
+
throw new CeError(error);
|
|
363
|
+
parseHttpUrl(value);
|
|
364
|
+
} catch {
|
|
365
|
+
throw new CeError(error);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function registryServer(value) {
|
|
369
|
+
const url = parseHttpUrl(value);
|
|
370
|
+
const authority = value.trim().replace(SCHEME_RE, "").split(/[/\\?#]/u, 1)[0].split("@").at(-1);
|
|
371
|
+
const port = url.port || /:(\d+)$/u.exec(authority)?.[1];
|
|
372
|
+
return port ? `${url.hostname}:${Number(port)}` : url.hostname;
|
|
373
|
+
}
|
|
374
|
+
function registryKey(value) {
|
|
375
|
+
return registryServer(value).toLowerCase();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/oidc.ts
|
|
379
|
+
var REFRESH_WINDOW_S = 5 * 60;
|
|
380
|
+
var REQUEST_TIMEOUT_MS = 30000;
|
|
381
|
+
var decodeUtf8 = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
382
|
+
async function forgejoOidcJwt(audience, requestUrl, requestToken) {
|
|
383
|
+
assertAbsoluteHttpUrl(requestUrl, "token request URL must be an absolute HTTP(S) URL");
|
|
384
|
+
let tokenUrl;
|
|
385
|
+
let headers;
|
|
386
|
+
try {
|
|
387
|
+
const url = new URL(requestUrl);
|
|
388
|
+
if (url.username || url.password)
|
|
389
|
+
throw new Error("credentials in URL");
|
|
390
|
+
url.searchParams.set("audience", audience);
|
|
391
|
+
tokenUrl = url.href;
|
|
392
|
+
headers = { Accept: "application/json", Authorization: `bearer ${requestToken}` };
|
|
393
|
+
new Headers(headers);
|
|
394
|
+
} catch {
|
|
395
|
+
throw new CeError("token request URL or headers are invalid");
|
|
396
|
+
}
|
|
397
|
+
let body;
|
|
398
|
+
try {
|
|
399
|
+
const response = await fetch(tokenUrl, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
400
|
+
if (!response.ok)
|
|
401
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
402
|
+
body = await response.arrayBuffer();
|
|
403
|
+
} catch (exc) {
|
|
404
|
+
throw new CeError(`token request failed: ${errorMessage(exc)}`);
|
|
405
|
+
}
|
|
406
|
+
let payload;
|
|
407
|
+
try {
|
|
408
|
+
payload = JSON.parse(decodeUtf8(body));
|
|
409
|
+
} catch {
|
|
410
|
+
throw new CeError("token response is not valid UTF-8 JSON");
|
|
411
|
+
}
|
|
412
|
+
if (!isObject(payload))
|
|
413
|
+
throw new CeError("token response is not a JSON object");
|
|
414
|
+
const jwt = payload.value;
|
|
415
|
+
if (typeof jwt !== "string" || !jwt.trim()) {
|
|
416
|
+
throw new CeError("token response has no non-empty string 'value'");
|
|
417
|
+
}
|
|
418
|
+
return jwt.trim();
|
|
419
|
+
}
|
|
420
|
+
function jwtExpiry(jwt) {
|
|
421
|
+
const parts = jwt.split(".");
|
|
422
|
+
if (parts.length !== 3)
|
|
423
|
+
throw new CeError("OIDC token is not a three-part JWT");
|
|
424
|
+
let payload;
|
|
425
|
+
try {
|
|
426
|
+
payload = JSON.parse(decodeUtf8(Buffer.from(parts[1], "base64url")));
|
|
427
|
+
} catch {
|
|
428
|
+
throw new CeError("OIDC token has an invalid JWT payload");
|
|
429
|
+
}
|
|
430
|
+
const expiry = isObject(payload) ? payload.exp : undefined;
|
|
431
|
+
if (typeof expiry !== "number")
|
|
432
|
+
throw new CeError("OIDC token has no numeric 'exp' claim");
|
|
433
|
+
return expiry;
|
|
434
|
+
}
|
|
435
|
+
function needsRefresh(expiry) {
|
|
436
|
+
return expiry <= Date.now() / 1000 + REFRESH_WINDOW_S;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/state.ts
|
|
440
|
+
import fs4 from "node:fs";
|
|
441
|
+
import { homedir } from "node:os";
|
|
442
|
+
import path3 from "node:path";
|
|
443
|
+
|
|
444
|
+
// src/lock.ts
|
|
445
|
+
import fs3 from "node:fs";
|
|
446
|
+
import { hostname } from "node:os";
|
|
447
|
+
import { setTimeout as setTimeout2 } from "node:timers/promises";
|
|
448
|
+
var STALE_AFTER_MS = 120000;
|
|
449
|
+
var POLL_MS = 50;
|
|
450
|
+
function snapshot(lockPath) {
|
|
451
|
+
try {
|
|
452
|
+
const { ino, mtimeMs } = fs3.statSync(lockPath);
|
|
453
|
+
return { ino, mtimeMs, owner: fs3.readFileSync(lockPath, "utf8").trim() };
|
|
454
|
+
} catch (exc) {
|
|
455
|
+
if (isErrno(exc, "ENOENT"))
|
|
456
|
+
return null;
|
|
457
|
+
throw exc;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function isStale({ mtimeMs, owner }) {
|
|
461
|
+
if (Date.now() - mtimeMs > STALE_AFTER_MS)
|
|
462
|
+
return true;
|
|
463
|
+
const match = /^(\d+)@(.*)$/u.exec(owner);
|
|
464
|
+
if (!match || match[2] !== hostname())
|
|
465
|
+
return false;
|
|
466
|
+
try {
|
|
467
|
+
process.kill(Number(match[1]), 0);
|
|
468
|
+
return false;
|
|
469
|
+
} catch (exc) {
|
|
470
|
+
return !isErrno(exc, "EPERM");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function breakIfStale(lockPath) {
|
|
474
|
+
const stale = snapshot(lockPath);
|
|
475
|
+
if (!stale || !isStale(stale))
|
|
476
|
+
return;
|
|
477
|
+
const guard = `${lockPath}.break`;
|
|
478
|
+
try {
|
|
479
|
+
fs3.writeFileSync(guard, "", { flag: "wx", mode: 384 });
|
|
480
|
+
} catch (exc) {
|
|
481
|
+
if (!isErrno(exc, "EEXIST"))
|
|
482
|
+
throw exc;
|
|
483
|
+
const held = snapshot(guard);
|
|
484
|
+
if (held && Date.now() - held.mtimeMs > STALE_AFTER_MS) {
|
|
485
|
+
throw new CeError(`stale lock guard ${JSON.stringify(guard)}: remove it if no ce is running`);
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const current = snapshot(lockPath);
|
|
491
|
+
if (current && current.ino === stale.ino && current.mtimeMs === stale.mtimeMs && current.owner === stale.owner) {
|
|
492
|
+
fs3.rmSync(lockPath, { force: true });
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
fs3.rmSync(guard, { force: true });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function withFileLock(lockPath, fn) {
|
|
499
|
+
const owner = `${process.pid}@${hostname()}`;
|
|
500
|
+
let ino;
|
|
501
|
+
for (;; ) {
|
|
502
|
+
let fd;
|
|
503
|
+
try {
|
|
504
|
+
fd = fs3.openSync(lockPath, "wx", 384);
|
|
505
|
+
} catch (exc) {
|
|
506
|
+
if (!isErrno(exc, "EEXIST"))
|
|
507
|
+
throw exc;
|
|
508
|
+
breakIfStale(lockPath);
|
|
509
|
+
await setTimeout2(POLL_MS);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
fs3.writeSync(fd, `${owner}
|
|
514
|
+
`);
|
|
515
|
+
ino = fs3.fstatSync(fd).ino;
|
|
516
|
+
} finally {
|
|
517
|
+
fs3.closeSync(fd);
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
return await fn();
|
|
523
|
+
} finally {
|
|
524
|
+
const current = snapshot(lockPath);
|
|
525
|
+
if (current?.ino === ino && current.owner === owner)
|
|
526
|
+
fs3.rmSync(lockPath, { force: true });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/state.ts
|
|
531
|
+
var HELPER_SUFFIX = "ce-forgejo";
|
|
532
|
+
var STATE_FILE = "ce-forgejo-credentials.json";
|
|
533
|
+
function dockerConfigDir() {
|
|
534
|
+
const configured = process.env.DOCKER_CONFIG;
|
|
535
|
+
if (!configured)
|
|
536
|
+
return path3.join(homedir(), ".docker");
|
|
537
|
+
const expanded = configured === "~" || configured.startsWith("~/") ? path3.join(homedir(), configured.slice(1)) : configured;
|
|
538
|
+
return path3.resolve(expanded);
|
|
539
|
+
}
|
|
540
|
+
function helperStatePath() {
|
|
541
|
+
return path3.join(dockerConfigDir(), STATE_FILE);
|
|
542
|
+
}
|
|
543
|
+
async function withHelperState(fn) {
|
|
544
|
+
const statePath = helperStatePath();
|
|
545
|
+
fs4.mkdirSync(path3.dirname(statePath), { recursive: true, mode: 448 });
|
|
546
|
+
return withFileLock(`${statePath}.pidlock`, async () => {
|
|
547
|
+
const state = readJsonFile(statePath, { version: 1, registries: {} });
|
|
548
|
+
if (!isObject(state) || !isObject(state.registries)) {
|
|
549
|
+
throw new CeError("Forgejo credential-helper state is invalid");
|
|
550
|
+
}
|
|
551
|
+
const before = JSON.stringify(state);
|
|
552
|
+
const result = await fn(state);
|
|
553
|
+
if (JSON.stringify(state) !== before)
|
|
554
|
+
writePrivateJson(statePath, state);
|
|
555
|
+
return result;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async function saveHelperState(registry, username, audience, authority, jwt) {
|
|
559
|
+
const expiry = jwtExpiry(jwt);
|
|
560
|
+
if (needsRefresh(expiry))
|
|
561
|
+
throw new CeError("OIDC token expires within the five-minute refresh window");
|
|
562
|
+
await withHelperState((state) => {
|
|
563
|
+
state.registries[registryKey(registry)] = {
|
|
564
|
+
audience,
|
|
565
|
+
authority: [authority[0], authority[1]],
|
|
566
|
+
expires_at: expiry,
|
|
567
|
+
token: jwt,
|
|
568
|
+
username
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function configureDockerCredentialHelper(registry, username, audience, authority, jwt) {
|
|
573
|
+
await saveHelperState(registry, username, audience, authority, jwt);
|
|
574
|
+
const configPath = path3.join(dockerConfigDir(), "config.json");
|
|
575
|
+
const config = readJsonFile(configPath, {});
|
|
576
|
+
if (!isObject(config))
|
|
577
|
+
throw new CeError("Docker config is not a JSON object");
|
|
578
|
+
config.credHelpers ??= {};
|
|
579
|
+
const helpers = config.credHelpers;
|
|
580
|
+
if (!isObject(helpers))
|
|
581
|
+
throw new CeError("Docker config 'credHelpers' is not a JSON object");
|
|
582
|
+
helpers[registry] = HELPER_SUFFIX;
|
|
583
|
+
const auths = config.auths;
|
|
584
|
+
if (isObject(auths)) {
|
|
585
|
+
for (const key of Object.keys(auths)) {
|
|
586
|
+
if (registryKey(key) === registryKey(registry))
|
|
587
|
+
delete auths[key];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
writePrivateJson(configPath, config);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/credential-helper.ts
|
|
594
|
+
async function helperGet(registry) {
|
|
595
|
+
const key = registryKey(registry);
|
|
596
|
+
return withHelperState(async (state) => {
|
|
597
|
+
const entry = state.registries[key];
|
|
598
|
+
if (!isObject(entry)) {
|
|
599
|
+
throw new CeError(`no Forgejo OIDC credentials configured for ${JSON.stringify(registry)}`);
|
|
600
|
+
}
|
|
601
|
+
const { username, token, expires_at: expiry } = entry;
|
|
602
|
+
if (typeof username !== "string" || !username) {
|
|
603
|
+
throw new CeError("Forgejo credential-helper username is missing");
|
|
604
|
+
}
|
|
605
|
+
if (typeof token !== "string" || typeof expiry !== "number") {
|
|
606
|
+
throw new CeError("Forgejo credential-helper token state is invalid");
|
|
607
|
+
}
|
|
608
|
+
if (!needsRefresh(expiry))
|
|
609
|
+
return { Username: username, Secret: token };
|
|
610
|
+
const { audience, authority } = entry;
|
|
611
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
612
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
613
|
+
if (typeof audience !== "string" || !audience) {
|
|
614
|
+
throw new CeError("Forgejo credential-helper audience is missing");
|
|
615
|
+
}
|
|
616
|
+
if (!(requestUrl && requestToken)) {
|
|
617
|
+
throw new CeError("OIDC refresh requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN");
|
|
618
|
+
}
|
|
619
|
+
const [host, port] = urlAuthority(requestUrl);
|
|
620
|
+
if (!Array.isArray(authority) || authority.length !== 2 || authority[0] !== host || authority[1] !== port) {
|
|
621
|
+
throw new CeError("OIDC token-request host does not match the registry");
|
|
622
|
+
}
|
|
623
|
+
const refreshed = await forgejoOidcJwt(audience, requestUrl, requestToken);
|
|
624
|
+
const refreshedExpiry = jwtExpiry(refreshed);
|
|
625
|
+
if (needsRefresh(refreshedExpiry)) {
|
|
626
|
+
throw new CeError("refreshed OIDC token expires within the five-minute window");
|
|
627
|
+
}
|
|
628
|
+
entry.token = refreshed;
|
|
629
|
+
entry.expires_at = refreshedExpiry;
|
|
630
|
+
return { Username: username, Secret: refreshed };
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
async function helperStore(input) {
|
|
634
|
+
const payload = JSON.parse(input);
|
|
635
|
+
if (!isObject(payload))
|
|
636
|
+
throw new CeError("credential-helper store payload is invalid");
|
|
637
|
+
const { ServerURL: registry, Username: username, Secret: secret } = payload;
|
|
638
|
+
const filled = (value) => typeof value === "string" && value !== "";
|
|
639
|
+
if (!(filled(registry) && filled(username) && filled(secret))) {
|
|
640
|
+
throw new CeError("credential-helper store fields are incomplete");
|
|
641
|
+
}
|
|
642
|
+
const expiry = jwtExpiry(secret);
|
|
643
|
+
await withHelperState((state) => {
|
|
644
|
+
const entry = state.registries[registryKey(registry)] ??= {};
|
|
645
|
+
Object.assign(entry, { expires_at: expiry, token: secret, username });
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async function credentialHelper(argv) {
|
|
649
|
+
const [command] = argv;
|
|
650
|
+
if (argv.length !== 1 || !["get", "store", "erase", "list"].includes(command)) {
|
|
651
|
+
logError("ce: credential-helper requires get|store|erase|list");
|
|
652
|
+
return 1;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
if (command === "get") {
|
|
656
|
+
io.stdout(`${JSON.stringify(await helperGet(io.stdin().trim()))}
|
|
657
|
+
`);
|
|
658
|
+
} else if (command === "store") {
|
|
659
|
+
await helperStore(io.stdin());
|
|
660
|
+
} else if (command === "erase") {
|
|
661
|
+
const key = registryKey(io.stdin().trim());
|
|
662
|
+
await withHelperState((state) => void delete state.registries[key]);
|
|
663
|
+
} else {
|
|
664
|
+
const entries = await withHelperState((state) => Object.fromEntries(Object.entries(state.registries).filter(([, entry]) => isObject(entry)).map(([key, entry]) => [key, entry.username ?? ""])));
|
|
665
|
+
io.stdout(`${JSON.stringify(entries)}
|
|
666
|
+
`);
|
|
667
|
+
}
|
|
668
|
+
return 0;
|
|
669
|
+
} catch (exc) {
|
|
670
|
+
if (!(exc instanceof CeError || exc instanceof SyntaxError || isErrno(exc)))
|
|
671
|
+
throw exc;
|
|
672
|
+
logError(`ce: Forgejo credential helper: ${errorMessage(exc)}`);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/engine.ts
|
|
678
|
+
var isEngine = (value) => ["docker", "podman", "buildah"].includes(value);
|
|
679
|
+
var WAIT_ATTEMPTS = 30;
|
|
680
|
+
async function waitForEngine(engine) {
|
|
681
|
+
if (engine === "buildah")
|
|
682
|
+
return true;
|
|
683
|
+
for (let attempt = 1;attempt <= WAIT_ATTEMPTS; attempt++) {
|
|
684
|
+
if (system.run([engine, "info"]).status === 0)
|
|
685
|
+
return true;
|
|
686
|
+
logError(`waiting for ${engine}... (${attempt}/${WAIT_ATTEMPTS})`);
|
|
687
|
+
await system.sleep(1000);
|
|
688
|
+
}
|
|
689
|
+
logError(`no engine available: ${engine}`);
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
async function runEngine(engine, argv, tagPrefix, cachePrefix) {
|
|
693
|
+
const { args, summary } = parseBuildArgs(argv, tagPrefix, cachePrefix);
|
|
694
|
+
ensureHelperOnPath();
|
|
695
|
+
if (!system.which(engine)) {
|
|
696
|
+
logError(`Engine not found: ${engine}`);
|
|
697
|
+
return 1;
|
|
698
|
+
}
|
|
699
|
+
if (!await waitForEngine(engine))
|
|
700
|
+
return 1;
|
|
701
|
+
printBuildSummary(engine, args, summary);
|
|
702
|
+
if (engine === "docker")
|
|
703
|
+
return system.exec("docker", ["docker", ...args]);
|
|
704
|
+
if (system.which("gosu")) {
|
|
705
|
+
const user = process.env.USER ?? "";
|
|
706
|
+
if (!user) {
|
|
707
|
+
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
708
|
+
return 1;
|
|
709
|
+
}
|
|
710
|
+
return system.exec("gosu", ["gosu", user, engine, ...args]);
|
|
711
|
+
}
|
|
712
|
+
return system.exec(engine, [engine, ...args]);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/login.ts
|
|
716
|
+
var ENGINES = ["docker", "podman", "buildah"];
|
|
717
|
+
function registryGroups() {
|
|
718
|
+
const prefix = "REGISTRY_HOST__";
|
|
719
|
+
const suffixed = Object.keys(process.env).filter((name) => name.startsWith(prefix) && name.length > prefix.length).sort().map((name) => name.slice(prefix.length));
|
|
720
|
+
return [...process.env.REGISTRY_HOST ? [""] : [], ...suffixed];
|
|
721
|
+
}
|
|
722
|
+
function oidcConfig() {
|
|
723
|
+
const audience = process.env.AUD ?? "";
|
|
724
|
+
if (!audience)
|
|
725
|
+
return null;
|
|
726
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL ?? "";
|
|
727
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? "";
|
|
728
|
+
if (!(requestUrl && requestToken)) {
|
|
729
|
+
return "Forgejo OIDC login config is incomplete: AUD requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN";
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
assertAbsoluteHttpUrl(requestUrl, "must be an absolute HTTP(S) URL");
|
|
733
|
+
return { audience, requestUrl, requestToken, authority: urlAuthority(requestUrl) };
|
|
734
|
+
} catch (exc) {
|
|
735
|
+
if (!(exc instanceof CeError))
|
|
736
|
+
throw exc;
|
|
737
|
+
return `Invalid ACTIONS_ID_TOKEN_REQUEST_URL: ${exc.message}`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function loginWithEngines(target, password) {
|
|
741
|
+
for (const engine of ENGINES) {
|
|
742
|
+
if (!system.which(engine))
|
|
743
|
+
continue;
|
|
744
|
+
let result;
|
|
745
|
+
try {
|
|
746
|
+
result = system.run([engine, "login", "-u", target.user, "--password-stdin", target.registry], `${password}
|
|
747
|
+
`);
|
|
748
|
+
} catch (exc) {
|
|
749
|
+
if (isErrno(exc, "ENOENT"))
|
|
750
|
+
continue;
|
|
751
|
+
throw exc;
|
|
752
|
+
}
|
|
753
|
+
if (result.status === 0)
|
|
754
|
+
return engine;
|
|
755
|
+
logError(`${engine} login failed for '${target.label}' (${target.registry}):`);
|
|
756
|
+
io.stderr(result.stderr);
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
async function login() {
|
|
761
|
+
ensureHelperOnPath();
|
|
762
|
+
const groups = registryGroups();
|
|
763
|
+
if (groups.length === 0) {
|
|
764
|
+
log("Skipping login: no REGISTRY_HOST env vars set");
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
const oidc = oidcConfig();
|
|
768
|
+
if (typeof oidc === "string") {
|
|
769
|
+
logError(oidc);
|
|
770
|
+
return 1;
|
|
771
|
+
}
|
|
772
|
+
const targets = [];
|
|
773
|
+
let failed = false;
|
|
774
|
+
for (const group of groups) {
|
|
775
|
+
const suffix = group ? `__${group}` : "";
|
|
776
|
+
const host = process.env[`REGISTRY_HOST${suffix}`] ?? "";
|
|
777
|
+
const user = process.env[`REGISTRY_USER${suffix}`] ?? "";
|
|
778
|
+
const password = process.env[`REGISTRY_PASSWORD${suffix}`] ?? "";
|
|
779
|
+
const label = group || "default";
|
|
780
|
+
const incomplete = `Login config for '${label}' is incomplete: host/user/password not all set`;
|
|
781
|
+
if (!(host && user)) {
|
|
782
|
+
logError(incomplete);
|
|
783
|
+
failed = true;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
for (const rawRegistry of host.split(/\s+/u).filter(Boolean)) {
|
|
787
|
+
let authority;
|
|
788
|
+
let registry;
|
|
789
|
+
try {
|
|
790
|
+
authority = urlAuthority(rawRegistry);
|
|
791
|
+
registry = registryServer(rawRegistry);
|
|
792
|
+
} catch (exc) {
|
|
793
|
+
if (!(exc instanceof CeError))
|
|
794
|
+
throw exc;
|
|
795
|
+
logError(`Invalid registry host for '${label}' (${rawRegistry}): ${exc.message}`);
|
|
796
|
+
failed = true;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const useOidc = oidc !== null && sameAuthority(authority, oidc.authority);
|
|
800
|
+
if (!useOidc && !password) {
|
|
801
|
+
logError(incomplete);
|
|
802
|
+
failed = true;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
targets.push({ registry, label, user, password, useOidc });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
let jwt = null;
|
|
809
|
+
if (oidc && targets.some((target) => target.useOidc)) {
|
|
810
|
+
try {
|
|
811
|
+
jwt = await forgejoOidcJwt(oidc.audience, oidc.requestUrl, oidc.requestToken);
|
|
812
|
+
} catch (exc) {
|
|
813
|
+
if (!(exc instanceof CeError))
|
|
814
|
+
throw exc;
|
|
815
|
+
logError(`Failed to fetch Forgejo OIDC JWT: ${exc.message}`);
|
|
816
|
+
return 1;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const target of targets) {
|
|
820
|
+
const { registry, label } = target;
|
|
821
|
+
const password = target.useOidc ? jwt : target.password;
|
|
822
|
+
if (password === null) {
|
|
823
|
+
logError(`Forgejo OIDC JWT unavailable for '${label}' (${registry})`);
|
|
824
|
+
failed = true;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
log(`Logging in to ${registry} (${label})...`);
|
|
828
|
+
const engine = loginWithEngines(target, password);
|
|
829
|
+
if (!engine) {
|
|
830
|
+
logError(`Login failed for '${label}' (${registry})`);
|
|
831
|
+
failed = true;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
log(`Logged in to ${registry} (${label}) via ${engine} ` + `(and all engines at or below it: docker < podman < buildah share credentials)`);
|
|
835
|
+
if (target.useOidc && oidc && jwt) {
|
|
836
|
+
try {
|
|
837
|
+
await configureDockerCredentialHelper(registry, target.user, oidc.audience, oidc.authority, jwt);
|
|
838
|
+
} catch (exc) {
|
|
839
|
+
if (!(exc instanceof CeError || isErrno(exc)))
|
|
840
|
+
throw exc;
|
|
841
|
+
logError(`Failed to configure Forgejo credential helper for '${label}' (${registry}): ${errorMessage(exc)}`);
|
|
842
|
+
failed = true;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return failed ? 1 : 0;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/cli.ts
|
|
850
|
+
function helpText() {
|
|
851
|
+
return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [command|engine] [args]
|
|
852
|
+
|
|
853
|
+
Options:
|
|
854
|
+
--tag-prefix=<prefix>
|
|
855
|
+
Prepend <prefix> to every tag passed via -t/--tag.
|
|
856
|
+
A trailing "-" is appended automatically if the prefix
|
|
857
|
+
does not already end with "-", "_", or ".".
|
|
858
|
+
Also available via TAG_PREFIX environment variable.
|
|
859
|
+
--cache-prefix=<prefix>
|
|
860
|
+
Prepend <prefix> to every gha cache scope passed via
|
|
861
|
+
--cache-from/--cache-to. Defaults to the tag prefix
|
|
862
|
+
(TAG_PREFIX); set to "" to keep cache scopes shared.
|
|
863
|
+
Also available via CACHE_PREFIX environment variable.
|
|
864
|
+
--cache=<spec> Shorthand for passing <spec> to both --cache-from and
|
|
865
|
+
--cache-to. Supports the same forms as --cache-from/--cache-to.
|
|
866
|
+
|
|
867
|
+
Commands:
|
|
868
|
+
login Log in to all available engines
|
|
869
|
+
|
|
870
|
+
Engines:
|
|
871
|
+
docker [args] Passthrough to docker
|
|
872
|
+
podman [args] Passthrough to podman (via gosu if available)
|
|
873
|
+
buildah [args] Passthrough to buildah (via gosu if available)
|
|
874
|
+
|
|
875
|
+
Environment variables (used by login):
|
|
876
|
+
Each registry is a "group". The default group uses the bare variables
|
|
877
|
+
below; additional registries use a __<name> suffix that ties the three
|
|
878
|
+
variables together.
|
|
879
|
+
REGISTRY_USER[__<name>] username
|
|
880
|
+
REGISTRY_PASSWORD[__<name>] password
|
|
881
|
+
REGISTRY_HOST[__<name>] registry host(s) (https:// stripped);
|
|
882
|
+
whitespace-separated list logs in to each
|
|
883
|
+
with the same creds, e.g. "docker.io dhi.io"
|
|
884
|
+
AUD Forgejo Authorized Integration audience.
|
|
885
|
+
When set, the registry matching the Forgejo
|
|
886
|
+
Actions token-request host uses a short-lived
|
|
887
|
+
OIDC JWT instead of REGISTRY_PASSWORD.
|
|
888
|
+
Docker uses a per-host credential helper that
|
|
889
|
+
refreshes the JWT five minutes before expiry.
|
|
890
|
+
Requires ACTIONS_ID_TOKEN_REQUEST_URL and
|
|
891
|
+
ACTIONS_ID_TOKEN_REQUEST_TOKEN from a job
|
|
892
|
+
with enable-openid-connect: true.
|
|
893
|
+
Defining REGISTRY_HOST__<name> adds a group; its creds come from
|
|
894
|
+
REGISTRY_USER__<name> and REGISTRY_PASSWORD__<name>. Example:
|
|
895
|
+
REGISTRY_HOST__DOCKER="docker.io dhi.io" + REGISTRY_USER__DOCKER +
|
|
896
|
+
REGISTRY_PASSWORD__DOCKER logs in to both Docker Hub and dhi.io with one
|
|
897
|
+
set of creds, alongside the default registry. The bare (unsuffixed)
|
|
898
|
+
variables are the default group — no suffix needed for the primary
|
|
899
|
+
registry.
|
|
900
|
+
`;
|
|
901
|
+
}
|
|
902
|
+
async function main(argv) {
|
|
903
|
+
let tagPrefix = process.env.TAG_PREFIX ?? "";
|
|
904
|
+
let cachePrefix = process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "";
|
|
905
|
+
for (;; ) {
|
|
906
|
+
const a = argv[0];
|
|
907
|
+
if (a === "--tag-prefix")
|
|
908
|
+
[tagPrefix, argv] = consumeValue(argv.slice(1), a);
|
|
909
|
+
else if (a?.startsWith("--tag-prefix="))
|
|
910
|
+
[tagPrefix, argv] = [a.slice("--tag-prefix=".length), argv.slice(1)];
|
|
911
|
+
else if (a === "--cache-prefix")
|
|
912
|
+
[cachePrefix, argv] = consumeValue(argv.slice(1), a);
|
|
913
|
+
else if (a?.startsWith("--cache-prefix="))
|
|
914
|
+
[cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
|
|
915
|
+
else
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
const [command, ...rest] = argv;
|
|
919
|
+
if (command === undefined) {
|
|
920
|
+
io.stderr(helpText());
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
if (command === "login")
|
|
924
|
+
return login();
|
|
925
|
+
if (command === "credential-helper")
|
|
926
|
+
return credentialHelper(rest);
|
|
927
|
+
if (isEngine(command))
|
|
928
|
+
return runEngine(command, rest, tagPrefix, cachePrefix);
|
|
929
|
+
io.stderr(helpText());
|
|
930
|
+
return 1;
|
|
931
|
+
}
|
|
932
|
+
async function entry(argv) {
|
|
933
|
+
try {
|
|
934
|
+
process.exit(await main(argv));
|
|
935
|
+
} catch (exc) {
|
|
936
|
+
if (!(exc instanceof CeError))
|
|
937
|
+
throw exc;
|
|
938
|
+
logError(exc.message);
|
|
939
|
+
process.exit(2);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
export {
|
|
943
|
+
entry,
|
|
944
|
+
helpText,
|
|
945
|
+
main
|
|
946
|
+
};
|