@izykitten/ce 0.0.3-dev.1 → 0.0.3-dev.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 +15 -6
- package/dist/ce.js +88 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,13 +64,11 @@ A branch, PR and default-branch build:
|
|
|
64
64
|
-t '{image}:{ref},{?default}latest,{?default}caddy-{env.CADDY_VERSION}' .
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
With `CACHE_PREFIX='{ref}'`, each branch and pull request gets a gha cache of its own, so a new one starts cold.
|
|
67
|
+
With `CACHE_PREFIX='{ref}'`, each branch and pull request gets a gha cache of its own, so a new one starts cold. With `CACHE_FROM_DEFAULT=true`, or `--cache-from-default` before the engine, each gha `--cache-to` is also read from its scope on the default branch: ce expands the cache prefix as it would be there, so PR 3 reads `pr-3` and `main` and writes only `pr-3`, and with `scope=js` it reads `main-js`. The default branch comes from the event file or `DEFAULT_BRANCH`, as for `{default}`. A prefix with `{sha}` or `{revision}` cannot be worked out for it, and is an error. This only picks what is read: whether it hits depends on what the default branch last wrote.
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
--cache type=gha,mode=max --cache-from type=gha,scope=main
|
|
71
|
-
```
|
|
69
|
+
The same can be written by hand, since ce only prefixes a `--cache-from` whose scope matches a `--cache-to` and passes any other on unchanged: `--cache type=gha,mode=max --cache-from type=gha,scope=main`.
|
|
72
70
|
|
|
73
|
-
|
|
71
|
+
The `--cache-from` that `--cache` makes leaves out a gha `mode=`, which only applies to export, and a `--cache-from` given twice, as the default branch's own scope is on the default branch, is passed once.
|
|
74
72
|
|
|
75
73
|
A release on a SemVer tag, with the version aliases only for stable releases:
|
|
76
74
|
|
|
@@ -80,6 +78,15 @@ A release on a SemVer tag, with the version aliases only for stable releases:
|
|
|
80
78
|
|
|
81
79
|
`{stable}` does not mean newest: rebuilding an older release moves its aliases back to it.
|
|
82
80
|
|
|
81
|
+
### Checking a template
|
|
82
|
+
|
|
83
|
+
`--dry-run` before the engine, or `CE_DRY_RUN=true`, prints the build summary and the engine command ce would run, and exits 0 without running it or waiting for the engine. A template that fails still exits 2, so it also works as a check before pushing a workflow:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
|
|
87
|
+
ce --dry-run docker buildx build -t '{image}:{ref},{?stable}{major}.{minor}' .
|
|
88
|
+
```
|
|
89
|
+
|
|
83
90
|
### OCI labels
|
|
84
91
|
|
|
85
92
|
With `OCI_LABELS=true`, or `--oci-labels` before the engine (`ce --oci-labels docker buildx build …`, like the prefix options), builds get `org.opencontainers.image.source`, `revision`, `version`, `title` and `description` labels from the run. Labels the run cannot tell are left out. They override a Dockerfile `LABEL` with the same key, and a `--label` you pass wins over them. `created` is not set, so the labels do not change the image on every rebuild.
|
|
@@ -92,7 +99,7 @@ The package installs `docker-credential-ce-forgejo` next to `ce`. `ce` puts that
|
|
|
92
99
|
|
|
93
100
|
## Compatibility
|
|
94
101
|
|
|
95
|
-
These
|
|
102
|
+
These behave the same under Node and Bun, and [test/contract.test.ts](test/contract.test.ts) checks them by running the built package under both. A release that changes one says so here:
|
|
96
103
|
|
|
97
104
|
- commands, flags and the environment variables `login` reads;
|
|
98
105
|
- the arguments the engine receives after tag and cache rewriting, and its exit code, signals and PID (`ce` replaces itself with the engine);
|
|
@@ -102,6 +109,8 @@ These stay the same from release to release, and [test/contract.test.ts](test/co
|
|
|
102
109
|
|
|
103
110
|
The wording of messages is not part of that and may change.
|
|
104
111
|
|
|
112
|
+
In 0.0.3, the `--cache-from` that `--cache` makes no longer has a gha `mode=`, which only applies to export, and a `--cache-from` given twice is passed once.
|
|
113
|
+
|
|
105
114
|
Compared with the Python version:
|
|
106
115
|
|
|
107
116
|
- The state file is locked with `ce-forgejo-credentials.json.pidlock`, holding the holder's PID and hostname, rather than `flock`, which Node does not have. A lock whose holder died is broken right away if the PID is gone on the same host, otherwise after two minutes. It does not exclude a Python `ce` using the same Docker config at the same moment.
|
package/dist/ce.js
CHANGED
|
@@ -298,7 +298,7 @@ class CiMetadata {
|
|
|
298
298
|
const ref = this.#parsedRef();
|
|
299
299
|
if (ref.kind !== "branch")
|
|
300
300
|
return;
|
|
301
|
-
return ref.name === this
|
|
301
|
+
return ref.name === this.defaultBranch() ? ref.name : undefined;
|
|
302
302
|
}
|
|
303
303
|
case "revision":
|
|
304
304
|
return this.#require("SHA", `{${name}}`);
|
|
@@ -359,7 +359,7 @@ class CiMetadata {
|
|
|
359
359
|
const ref = this.#parsedRef();
|
|
360
360
|
return ref.kind === "tag" ? SEMVER.exec(ref.name) : null;
|
|
361
361
|
}
|
|
362
|
-
|
|
362
|
+
defaultBranch() {
|
|
363
363
|
const repository = this.#ciEnv("EVENT_PATH") ? this.#payload().repository : undefined;
|
|
364
364
|
const fromEvent = isObject(repository) ? repository.default_branch : undefined;
|
|
365
365
|
const branch = typeof fromEvent === "string" && fromEvent || nonEmpty(this.#env.DEFAULT_BRANCH);
|
|
@@ -384,6 +384,21 @@ class CiMetadata {
|
|
|
384
384
|
return payload;
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
|
+
var ON_DEFAULT_BRANCH = new Set(["ref", "branch", "default"]);
|
|
388
|
+
var UNKNOWN_ON_DEFAULT_BRANCH = new Set(["revision", "sha"]);
|
|
389
|
+
function onDefaultBranch(vars, branch) {
|
|
390
|
+
return {
|
|
391
|
+
known: (name) => vars.known(name),
|
|
392
|
+
get(name) {
|
|
393
|
+
if (ON_DEFAULT_BRANCH.has(name))
|
|
394
|
+
return branch;
|
|
395
|
+
if (UNKNOWN_ON_DEFAULT_BRANCH.has(name)) {
|
|
396
|
+
throw new CeError(`ce: the default branch's cache scope cannot use {${name}}: this run does not know its commit`);
|
|
397
|
+
}
|
|
398
|
+
return name === "image" || name.startsWith("env.") ? vars.get(name) : undefined;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
387
402
|
|
|
388
403
|
// src/system.ts
|
|
389
404
|
import { spawnSync } from "node:child_process";
|
|
@@ -664,6 +679,7 @@ function ghaFields(value) {
|
|
|
664
679
|
const type = fields?.findLast((field) => fieldKey(field) === "type");
|
|
665
680
|
return type?.slice("type=".length) === "gha" ? fields : null;
|
|
666
681
|
}
|
|
682
|
+
var isGhaCache = (value) => ghaFields(value) !== null;
|
|
667
683
|
var scopeIndex = (fields) => fields.findLastIndex((field) => fieldKey(field) === "scope");
|
|
668
684
|
function cacheScopeName(value) {
|
|
669
685
|
const fields = ghaFields(value);
|
|
@@ -697,6 +713,12 @@ function normalizeCacheScope(value, cachePrefix) {
|
|
|
697
713
|
fields[i] = `scope=${scope}`;
|
|
698
714
|
return csvJoin(fields);
|
|
699
715
|
}
|
|
716
|
+
function withoutCacheMode(value) {
|
|
717
|
+
const fields = ghaFields(value);
|
|
718
|
+
if (!fields?.some((field) => fieldKey(field) === "mode"))
|
|
719
|
+
return value;
|
|
720
|
+
return csvJoin(fields.filter((field) => fieldKey(field) !== "mode"));
|
|
721
|
+
}
|
|
700
722
|
|
|
701
723
|
// src/build-args.ts
|
|
702
724
|
function consumeValue(argv, flag) {
|
|
@@ -716,7 +738,7 @@ var SUMMARIZED = {
|
|
|
716
738
|
"--build-arg": "buildArgs",
|
|
717
739
|
"--label": "labels"
|
|
718
740
|
};
|
|
719
|
-
function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {} } = {}) {
|
|
741
|
+
function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {}, defaultBranch } = {}) {
|
|
720
742
|
const args = [];
|
|
721
743
|
const rawCacheFrom = [];
|
|
722
744
|
const rawCacheTo = [];
|
|
@@ -770,7 +792,7 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
770
792
|
rawCacheTo.push(attached ?? next());
|
|
771
793
|
else if (name === "--cache") {
|
|
772
794
|
const v = attached ?? next();
|
|
773
|
-
rawCacheFrom.push(v);
|
|
795
|
+
rawCacheFrom.push(withoutCacheMode(v));
|
|
774
796
|
rawCacheTo.push(v);
|
|
775
797
|
} else if (Object.hasOwn(SUMMARIZED, name)) {
|
|
776
798
|
const flag = name;
|
|
@@ -798,11 +820,17 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
798
820
|
const added = Object.entries(labels).filter(([key]) => !ownKeys.has(key)).map(([key, value]) => `${key}=${value}`);
|
|
799
821
|
args.unshift(...added.map((label) => `--label=${label}`));
|
|
800
822
|
summary.labels.unshift(...added);
|
|
823
|
+
const prefixTemplate = cachePrefix;
|
|
801
824
|
if (rawCacheFrom.length || rawCacheTo.length)
|
|
802
825
|
cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
|
|
803
826
|
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
804
|
-
|
|
805
|
-
|
|
827
|
+
const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
|
|
828
|
+
const ghaTo = rawCacheTo.filter(isGhaCache);
|
|
829
|
+
if (defaultBranch && ghaTo.length) {
|
|
830
|
+
const prefix = expandPrefix(prefixTemplate, onDefaultBranch(vars, defaultBranch()), "cache prefix");
|
|
831
|
+
cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
|
|
832
|
+
}
|
|
833
|
+
for (const rewritten of new Set(cacheFrom)) {
|
|
806
834
|
args.push(`--cache-from=${rewritten}`);
|
|
807
835
|
summary.cacheFrom.push(rewritten);
|
|
808
836
|
}
|
|
@@ -1169,17 +1197,46 @@ function envFlag(name) {
|
|
|
1169
1197
|
return true;
|
|
1170
1198
|
throw new CeError(`ce: ${name} must be true or false, not ${JSON.stringify(process.env[name])}`);
|
|
1171
1199
|
}
|
|
1172
|
-
|
|
1200
|
+
function shellQuote(arg) {
|
|
1201
|
+
return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1202
|
+
}
|
|
1203
|
+
function engineCommand(engine, args) {
|
|
1204
|
+
if (engine === "docker" || !system.which("gosu"))
|
|
1205
|
+
return [engine, ...args];
|
|
1206
|
+
const user = process.env.USER ?? "";
|
|
1207
|
+
if (!user) {
|
|
1208
|
+
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
return ["gosu", user, engine, ...args];
|
|
1212
|
+
}
|
|
1213
|
+
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
1214
|
+
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
1173
1215
|
const command = findBuildCommand(engine, argv);
|
|
1174
1216
|
let args = argv;
|
|
1175
1217
|
let summary;
|
|
1176
1218
|
if (command) {
|
|
1177
1219
|
const vars = new CiMetadata;
|
|
1178
|
-
const labels = !command.tagsOnly && (ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1179
|
-
const
|
|
1220
|
+
const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1221
|
+
const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
|
|
1222
|
+
const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
|
|
1223
|
+
vars,
|
|
1224
|
+
labels,
|
|
1225
|
+
defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined
|
|
1226
|
+
});
|
|
1180
1227
|
args = [...argv.slice(0, command.start), ...parsed.args];
|
|
1181
1228
|
summary = parsed.summary;
|
|
1182
1229
|
}
|
|
1230
|
+
if (dryRun) {
|
|
1231
|
+
const run = engineCommand(engine, args);
|
|
1232
|
+
if (!run)
|
|
1233
|
+
return 1;
|
|
1234
|
+
if (summary)
|
|
1235
|
+
printBuildSummary(engine, args, summary);
|
|
1236
|
+
io.stdout(`${run.map(shellQuote).join(" ")}
|
|
1237
|
+
`);
|
|
1238
|
+
return 0;
|
|
1239
|
+
}
|
|
1183
1240
|
ensureHelperOnPath();
|
|
1184
1241
|
if (!system.which(engine)) {
|
|
1185
1242
|
logError(`Engine not found: ${engine}`);
|
|
@@ -1189,17 +1246,8 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, ociLabels) {
|
|
|
1189
1246
|
return 1;
|
|
1190
1247
|
if (summary)
|
|
1191
1248
|
printBuildSummary(engine, args, summary);
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
return system.exec(engine, [engine, ...args]);
|
|
1249
|
+
const run = engineCommand(engine, args);
|
|
1250
|
+
return run ? system.exec(run[0], run) : 1;
|
|
1203
1251
|
}
|
|
1204
1252
|
|
|
1205
1253
|
// src/login.ts
|
|
@@ -1352,7 +1400,8 @@ async function login() {
|
|
|
1352
1400
|
|
|
1353
1401
|
// src/cli.ts
|
|
1354
1402
|
function helpText() {
|
|
1355
|
-
return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--oci-labels]
|
|
1403
|
+
return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--oci-labels]
|
|
1404
|
+
[--cache-from-default] [--dry-run] [command|engine] [args]
|
|
1356
1405
|
|
|
1357
1406
|
Options:
|
|
1358
1407
|
--tag-prefix=<prefix>
|
|
@@ -1368,10 +1417,21 @@ Options:
|
|
|
1368
1417
|
Also available via CACHE_PREFIX environment variable.
|
|
1369
1418
|
--cache=<spec> Shorthand for passing <spec> to both --cache-from and
|
|
1370
1419
|
--cache-to. Supports the same forms as --cache-from/--cache-to.
|
|
1420
|
+
A gha cache-from made this way leaves out mode=, which
|
|
1421
|
+
only applies to export. The same cache-from given twice
|
|
1422
|
+
is passed once.
|
|
1423
|
+
--cache-from-default
|
|
1424
|
+
Also read each gha --cache-to from its scope on the
|
|
1425
|
+
default branch, from the cache prefix as it is there,
|
|
1426
|
+
so a new branch or PR starts from its cache.
|
|
1427
|
+
Also available via CACHE_FROM_DEFAULT=true.
|
|
1371
1428
|
--oci-labels Add org.opencontainers.image.* labels (source, revision,
|
|
1372
1429
|
version, title, description) from the Forgejo Actions
|
|
1373
1430
|
run to builds. A --label with the same key wins.
|
|
1374
1431
|
Also available via OCI_LABELS=true.
|
|
1432
|
+
--dry-run Print the build summary and the engine command, and
|
|
1433
|
+
exit without running it. Template errors still exit 2.
|
|
1434
|
+
Also available via CE_DRY_RUN=true.
|
|
1375
1435
|
|
|
1376
1436
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1377
1437
|
{name} The value of name; an error if it has none in this run.
|
|
@@ -1428,7 +1488,7 @@ Environment variables (used by login):
|
|
|
1428
1488
|
`;
|
|
1429
1489
|
}
|
|
1430
1490
|
async function main(argv) {
|
|
1431
|
-
|
|
1491
|
+
const options = {};
|
|
1432
1492
|
let tagPrefix = process.env.TAG_PREFIX ?? "";
|
|
1433
1493
|
let cachePrefix = process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "";
|
|
1434
1494
|
for (;; ) {
|
|
@@ -1442,7 +1502,11 @@ async function main(argv) {
|
|
|
1442
1502
|
else if (a?.startsWith("--cache-prefix="))
|
|
1443
1503
|
[cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
|
|
1444
1504
|
else if (a === "--oci-labels")
|
|
1445
|
-
[ociLabels, argv] = [true, argv.slice(1)];
|
|
1505
|
+
[options.ociLabels, argv] = [true, argv.slice(1)];
|
|
1506
|
+
else if (a === "--cache-from-default")
|
|
1507
|
+
[options.cacheFromDefault, argv] = [true, argv.slice(1)];
|
|
1508
|
+
else if (a === "--dry-run")
|
|
1509
|
+
[options.dryRun, argv] = [true, argv.slice(1)];
|
|
1446
1510
|
else
|
|
1447
1511
|
break;
|
|
1448
1512
|
}
|
|
@@ -1456,7 +1520,7 @@ async function main(argv) {
|
|
|
1456
1520
|
if (command === "credential-helper")
|
|
1457
1521
|
return credentialHelper(rest);
|
|
1458
1522
|
if (isEngine(command))
|
|
1459
|
-
return runEngine(command, rest, tagPrefix, cachePrefix,
|
|
1523
|
+
return runEngine(command, rest, tagPrefix, cachePrefix, options);
|
|
1460
1524
|
io.stderr(helpText());
|
|
1461
1525
|
return 1;
|
|
1462
1526
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izykitten/ce",
|
|
3
|
-
"version": "0.0.3-dev.
|
|
3
|
+
"version": "0.0.3-dev.2",
|
|
4
4
|
"description": "Container engine wrapper: tag/cache rewriting for docker, podman and buildah, registry login, and a Forgejo OIDC Docker credential helper",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|