@izykitten/ce 0.0.3-dev.1 → 0.0.3-dev.3
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 +28 -8
- package/dist/ce.js +181 -50
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,12 +43,23 @@ Tags and the `TAG_PREFIX`/`CACHE_PREFIX` values can use placeholders, filled fro
|
|
|
43
43
|
| `{version}`, `{major}`, `{minor}`, `{patch}` | from a SemVer tag such as `v1.2.3` or `1.2.3-rc.1` |
|
|
44
44
|
| `{stable}` | set only for a SemVer tag without a pre-release |
|
|
45
45
|
| `{env.NAME}` | the environment variable `NAME`, e.g. a value from an earlier step |
|
|
46
|
+
| `{arg.NAME}` | the build arg `NAME` from this command's `--build-arg`, the value the build gets |
|
|
46
47
|
|
|
47
48
|
`{name}` must have a value (an empty variable has none): if it has none in this run, ce fails with exit 2 before anything runs. To leave a tag out instead, guard it with `{?name}`, which expands to nothing and keeps the tag only when `name` has a value. Every guard in a tag has to pass, and a tag's other values are only needed when it is kept. Names are checked in every tag, so a typo fails even in one a guard leaves out. If the guards leave out every tag, ce fails rather than build an untagged image.
|
|
48
49
|
|
|
49
|
-
Values are made safe for a tag: any ASCII character a tag cannot have becomes `-`, so branch `ci/modernize-build` gives `ci-modernize-build`; a non-ASCII character is an error. A value never adds a tag or changes the image name.
|
|
50
|
+
Values are made safe for a tag: any ASCII character a tag cannot have becomes `-`, so branch `ci/modernize-build` gives `ci-modernize-build`; a non-ASCII character is an error. A value never adds a tag or changes the image name.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
Prefixes can use guards too. A guard that does not pass leaves the prefix empty, since a prefix cannot be left out, and the prefix's other values are then not needed. `CACHE_PREFIX='{?pr}pr-{pr}'` gives each pull request a cache of its own (`pr-3`) and leaves every other build sharing the unprefixed one; with `CACHE_FROM_DEFAULT`, a PR also reads that shared one, as the prefix is empty on the default branch. The image name cannot use guards.
|
|
53
|
+
|
|
54
|
+
`{arg.NAME}` takes the value from `--build-arg NAME=value`, or from the environment for `--build-arg NAME`, as the engine does, so a tag can name a value worked out on the command line:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
ce docker build --build-arg VERSION="$(cat VERSION)" -t '{image}:{arg.VERSION}' .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The last `--build-arg NAME` wins. One whose variable is not set is passed over by Docker, which keeps an earlier value, while podman and buildah drop the value for the Containerfile's default. Where the build would use the Containerfile's default, which ce does not read, `{arg.NAME}` is an error, guarded or not, unless another guard beside it does not pass. An empty value (`--build-arg NAME=`) is no value. With podman's `--build-arg-file`, which ce does not read either, `{arg.NAME}` needs a `--build-arg NAME` of its own.
|
|
61
|
+
|
|
62
|
+
Guards decide from the run's metadata, so they need it too: outside a Forgejo Actions run, `{?default}` fails like `{default}` would. Only `{env.NAME}` and `{arg.NAME}` work everywhere. A guard that does not pass decides on its own, though: a tag with one is left out, and a prefix with one is empty, even if another guard in it could not be worked out, so `{?default}{?env.X}` outside a run with `X` empty is simply left out. To tell whether a branch is the default one, ce reads `repository.default_branch` from the event file, or `DEFAULT_BRANCH` when the event has none, as on a scheduled run; set `DEFAULT_BRANCH: main` in such a workflow. Pull requests and tags are never the default branch, so they need neither.
|
|
52
63
|
|
|
53
64
|
Put templates in single quotes: `?` is a glob character in zsh.
|
|
54
65
|
|
|
@@ -64,13 +75,11 @@ A branch, PR and default-branch build:
|
|
|
64
75
|
-t '{image}:{ref},{?default}latest,{?default}caddy-{env.CADDY_VERSION}' .
|
|
65
76
|
```
|
|
66
77
|
|
|
67
|
-
With `CACHE_PREFIX='{ref}'`, each branch and pull request gets a gha cache of its own, so a new one starts cold.
|
|
78
|
+
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 that needs `{sha}` or `{revision}` there cannot be worked out for it, and is an error; one whose guards leave it empty there, like `{?pr}{sha}`, does not need them. This only picks what is read: whether it hits depends on what the default branch last wrote.
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
--cache type=gha,mode=max --cache-from type=gha,scope=main
|
|
71
|
-
```
|
|
80
|
+
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
81
|
|
|
73
|
-
|
|
82
|
+
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
83
|
|
|
75
84
|
A release on a SemVer tag, with the version aliases only for stable releases:
|
|
76
85
|
|
|
@@ -80,6 +89,15 @@ A release on a SemVer tag, with the version aliases only for stable releases:
|
|
|
80
89
|
|
|
81
90
|
`{stable}` does not mean newest: rebuilding an older release moves its aliases back to it.
|
|
82
91
|
|
|
92
|
+
### Checking a template
|
|
93
|
+
|
|
94
|
+
`--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. It checks the templates, not the engine: if the engine is not on `PATH`, it warns and still exits 0, so it works on a machine without one:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
|
|
98
|
+
ce --dry-run docker buildx build -t '{image}:{ref},{?stable}{major}.{minor}' .
|
|
99
|
+
```
|
|
100
|
+
|
|
83
101
|
### OCI labels
|
|
84
102
|
|
|
85
103
|
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 +110,7 @@ The package installs `docker-credential-ce-forgejo` next to `ce`. `ce` puts that
|
|
|
92
110
|
|
|
93
111
|
## Compatibility
|
|
94
112
|
|
|
95
|
-
These
|
|
113
|
+
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
114
|
|
|
97
115
|
- commands, flags and the environment variables `login` reads;
|
|
98
116
|
- 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 +120,8 @@ These stay the same from release to release, and [test/contract.test.ts](test/co
|
|
|
102
120
|
|
|
103
121
|
The wording of messages is not part of that and may change.
|
|
104
122
|
|
|
123
|
+
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.
|
|
124
|
+
|
|
105
125
|
Compared with the Python version:
|
|
106
126
|
|
|
107
127
|
- 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
|
@@ -244,7 +244,7 @@ class CiMetadata {
|
|
|
244
244
|
get(name) {
|
|
245
245
|
const envName = ENV_VARIABLE.exec(name)?.[1];
|
|
246
246
|
if (envName)
|
|
247
|
-
return nonEmpty(this.#env[envName]);
|
|
247
|
+
return Object.hasOwn(this.#env, envName) ? nonEmpty(this.#env[envName]) : undefined;
|
|
248
248
|
if (!this.#cache.has(name))
|
|
249
249
|
this.#cache.set(name, this.#resolve(name));
|
|
250
250
|
return this.#cache.get(name);
|
|
@@ -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.") || name.startsWith("arg.") ? vars.get(name) : undefined;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
387
402
|
|
|
388
403
|
// src/system.ts
|
|
389
404
|
import { spawnSync } from "node:child_process";
|
|
@@ -479,12 +494,26 @@ function parse(template, vars) {
|
|
|
479
494
|
text(template.slice(last));
|
|
480
495
|
return parts;
|
|
481
496
|
}
|
|
482
|
-
function
|
|
483
|
-
|
|
497
|
+
function guardsPass(parts, vars) {
|
|
498
|
+
let unknown;
|
|
484
499
|
for (const part of parts) {
|
|
485
|
-
if ("name" in part
|
|
486
|
-
|
|
500
|
+
if (!("name" in part) || !part.guard)
|
|
501
|
+
continue;
|
|
502
|
+
try {
|
|
503
|
+
if (vars.get(part.name) === undefined)
|
|
504
|
+
return false;
|
|
505
|
+
} catch (exc) {
|
|
506
|
+
unknown ??= exc;
|
|
507
|
+
}
|
|
487
508
|
}
|
|
509
|
+
if (unknown !== undefined)
|
|
510
|
+
throw unknown;
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
function expand(template, vars, encode) {
|
|
514
|
+
const parts = parse(template, vars);
|
|
515
|
+
if (!guardsPass(parts, vars))
|
|
516
|
+
return null;
|
|
488
517
|
let out = "";
|
|
489
518
|
for (const part of parts) {
|
|
490
519
|
if ("text" in part)
|
|
@@ -499,16 +528,21 @@ function expand(template, vars, encode) {
|
|
|
499
528
|
}
|
|
500
529
|
return out;
|
|
501
530
|
}
|
|
502
|
-
function checkValue(template, vars, what) {
|
|
503
|
-
|
|
504
|
-
|
|
531
|
+
function checkValue(template, vars, what, guards = false) {
|
|
532
|
+
const parts = parse(template, vars);
|
|
533
|
+
if (!guards && parts.some((part) => ("guard" in part) && part.guard)) {
|
|
534
|
+
throw new CeError(`ce: ${what} ${JSON.stringify(template)} cannot use {?guards}; they only apply to tags and prefixes`);
|
|
505
535
|
}
|
|
506
536
|
}
|
|
507
|
-
function expandValue(template, vars, encode, what) {
|
|
508
|
-
checkValue(template, vars, what);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
537
|
+
function expandValue(template, vars, encode, what, guards = false) {
|
|
538
|
+
checkValue(template, vars, what, guards);
|
|
539
|
+
const parts = parse(template, vars);
|
|
540
|
+
if (!guardsPass(parts, vars))
|
|
541
|
+
return "";
|
|
542
|
+
for (const part of parts) {
|
|
543
|
+
if ("name" in part && !part.guard && vars.get(part.name) === undefined) {
|
|
544
|
+
const hint = guards ? ` To leave the ${what} empty instead, guard it with {?${part.name}}.` : "";
|
|
545
|
+
throw new CeError(`ce: {${part.name}} has no value here, in the ${what} ${JSON.stringify(template)}.${hint}`);
|
|
512
546
|
}
|
|
513
547
|
}
|
|
514
548
|
return expand(template, vars, encode);
|
|
@@ -570,7 +604,7 @@ function expandImage(template, vars) {
|
|
|
570
604
|
return image;
|
|
571
605
|
}
|
|
572
606
|
function expandPrefix(prefix, vars, what) {
|
|
573
|
-
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what) : prefix;
|
|
607
|
+
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what, true) : prefix;
|
|
574
608
|
}
|
|
575
609
|
function literalRef(repo, tag) {
|
|
576
610
|
if (tag.includes("/")) {
|
|
@@ -593,7 +627,7 @@ function rewriteTagValue(value, tagPrefix, vars = new CiMetadata) {
|
|
|
593
627
|
if (hasPlaceholders(repoTemplate))
|
|
594
628
|
checkValue(repoTemplate, vars, "image name");
|
|
595
629
|
if (hasPlaceholders(tagPrefix))
|
|
596
|
-
checkValue(tagPrefix, vars, "tag prefix");
|
|
630
|
+
checkValue(tagPrefix, vars, "tag prefix", true);
|
|
597
631
|
let repo;
|
|
598
632
|
const expandRepo = () => repo ??= hasPlaceholders(repoTemplate) ? expandImage(repoTemplate, vars) : repoTemplate;
|
|
599
633
|
if (tagList === undefined)
|
|
@@ -664,6 +698,7 @@ function ghaFields(value) {
|
|
|
664
698
|
const type = fields?.findLast((field) => fieldKey(field) === "type");
|
|
665
699
|
return type?.slice("type=".length) === "gha" ? fields : null;
|
|
666
700
|
}
|
|
701
|
+
var isGhaCache = (value) => ghaFields(value) !== null;
|
|
667
702
|
var scopeIndex = (fields) => fields.findLastIndex((field) => fieldKey(field) === "scope");
|
|
668
703
|
function cacheScopeName(value) {
|
|
669
704
|
const fields = ghaFields(value);
|
|
@@ -697,6 +732,12 @@ function normalizeCacheScope(value, cachePrefix) {
|
|
|
697
732
|
fields[i] = `scope=${scope}`;
|
|
698
733
|
return csvJoin(fields);
|
|
699
734
|
}
|
|
735
|
+
function withoutCacheMode(value) {
|
|
736
|
+
const fields = ghaFields(value);
|
|
737
|
+
if (!fields?.some((field) => fieldKey(field) === "mode"))
|
|
738
|
+
return value;
|
|
739
|
+
return csvJoin(fields.filter((field) => fieldKey(field) !== "mode"));
|
|
740
|
+
}
|
|
700
741
|
|
|
701
742
|
// src/build-args.ts
|
|
702
743
|
function consumeValue(argv, flag) {
|
|
@@ -716,8 +757,27 @@ var SUMMARIZED = {
|
|
|
716
757
|
"--build-arg": "buildArgs",
|
|
717
758
|
"--label": "labels"
|
|
718
759
|
};
|
|
719
|
-
|
|
720
|
-
|
|
760
|
+
var ARG_VARIABLE = /^arg\.([A-Za-z_][A-Za-z0-9_]*)$/u;
|
|
761
|
+
function withBuildArgs(vars, values, argFile) {
|
|
762
|
+
return {
|
|
763
|
+
known: (name) => ARG_VARIABLE.test(name) || vars.known(name),
|
|
764
|
+
get(name) {
|
|
765
|
+
const arg = ARG_VARIABLE.exec(name)?.[1];
|
|
766
|
+
if (arg === undefined)
|
|
767
|
+
return vars.get(name);
|
|
768
|
+
const value = values.get(arg);
|
|
769
|
+
if (value === undefined && argFile) {
|
|
770
|
+
throw new CeError(`ce: {${name}} needs --build-arg ${arg} on this command, as ce does not read --build-arg-file`);
|
|
771
|
+
}
|
|
772
|
+
if (value == null) {
|
|
773
|
+
throw new CeError(`ce: {${name}} needs --build-arg ${arg}=<value> on this command, or --build-arg ${arg} with ${arg} set; ` + "ce does not read defaults from the Containerfile");
|
|
774
|
+
}
|
|
775
|
+
return value || undefined;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, labels = {}, defaultBranch, env = process.env, engine = "docker" } = {}) {
|
|
780
|
+
const parts = [];
|
|
721
781
|
const rawCacheFrom = [];
|
|
722
782
|
const rawCacheTo = [];
|
|
723
783
|
const summary = {
|
|
@@ -732,16 +792,23 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
732
792
|
context: ""
|
|
733
793
|
};
|
|
734
794
|
let templated = false;
|
|
795
|
+
const buildArgs = new Map;
|
|
796
|
+
let argFile = false;
|
|
735
797
|
let rest = [];
|
|
736
798
|
const addTags = (value, flag) => {
|
|
737
799
|
templated ||= hasPlaceholders(value);
|
|
738
|
-
|
|
739
|
-
for (const ref of refs)
|
|
740
|
-
args.push(`${flag}${ref}`);
|
|
741
|
-
summary.tags.push(...refs);
|
|
800
|
+
parts.push({ tags: value, flag });
|
|
742
801
|
};
|
|
743
802
|
const summarize = (flag, value) => {
|
|
744
803
|
const field = SUMMARIZED[flag];
|
|
804
|
+
if (field === "buildArgs") {
|
|
805
|
+
if (value.includes("="))
|
|
806
|
+
buildArgs.set(...splitAt(value, "="));
|
|
807
|
+
else if (Object.hasOwn(env, value))
|
|
808
|
+
buildArgs.set(value, env[value]);
|
|
809
|
+
else if (engine !== "docker")
|
|
810
|
+
buildArgs.set(value, null);
|
|
811
|
+
}
|
|
745
812
|
if (field === "buildArgs" || field === "labels")
|
|
746
813
|
summary[field].push(value);
|
|
747
814
|
else
|
|
@@ -770,27 +837,40 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
770
837
|
rawCacheTo.push(attached ?? next());
|
|
771
838
|
else if (name === "--cache") {
|
|
772
839
|
const v = attached ?? next();
|
|
773
|
-
rawCacheFrom.push(v);
|
|
840
|
+
rawCacheFrom.push(withoutCacheMode(v));
|
|
774
841
|
rawCacheTo.push(v);
|
|
775
842
|
} else if (Object.hasOwn(SUMMARIZED, name)) {
|
|
776
843
|
const flag = name;
|
|
777
844
|
if (attached === undefined) {
|
|
778
845
|
const v = next();
|
|
779
|
-
|
|
846
|
+
parts.push(arg, v);
|
|
780
847
|
summarize(flag, v);
|
|
781
848
|
} else {
|
|
782
|
-
|
|
849
|
+
parts.push(arg);
|
|
783
850
|
summarize(flag, attached);
|
|
784
851
|
}
|
|
852
|
+
} else if (name === "--build-arg-file") {
|
|
853
|
+
argFile = true;
|
|
854
|
+
parts.push(arg);
|
|
855
|
+
if (attached === undefined)
|
|
856
|
+
parts.push(next());
|
|
785
857
|
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
786
|
-
|
|
858
|
+
parts.push(arg);
|
|
787
859
|
summary.dockerfile = arg.slice(2);
|
|
788
860
|
} else {
|
|
789
|
-
|
|
861
|
+
parts.push(arg);
|
|
790
862
|
if (!arg.startsWith("-"))
|
|
791
863
|
summary.context = arg;
|
|
792
864
|
}
|
|
793
865
|
}
|
|
866
|
+
vars = withBuildArgs(vars, buildArgs, argFile);
|
|
867
|
+
const args = parts.flatMap((part) => {
|
|
868
|
+
if (typeof part === "string")
|
|
869
|
+
return [part];
|
|
870
|
+
const refs = rewriteTagValue(part.tags, tagPrefix, vars);
|
|
871
|
+
summary.tags.push(...refs);
|
|
872
|
+
return refs.map((ref) => `${part.flag}${ref}`);
|
|
873
|
+
});
|
|
794
874
|
if (templated && !summary.tags.length) {
|
|
795
875
|
throw new CeError("ce: the guards in the -t templates left out every tag, so the image would have none");
|
|
796
876
|
}
|
|
@@ -798,11 +878,17 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
798
878
|
const added = Object.entries(labels).filter(([key]) => !ownKeys.has(key)).map(([key, value]) => `${key}=${value}`);
|
|
799
879
|
args.unshift(...added.map((label) => `--label=${label}`));
|
|
800
880
|
summary.labels.unshift(...added);
|
|
881
|
+
const prefixTemplate = cachePrefix;
|
|
801
882
|
if (rawCacheFrom.length || rawCacheTo.length)
|
|
802
883
|
cachePrefix = expandPrefix(cachePrefix, vars, "cache prefix");
|
|
803
884
|
const toNames = new Set(rawCacheTo.map(cacheScopeName));
|
|
804
|
-
|
|
805
|
-
|
|
885
|
+
const cacheFrom = rawCacheFrom.map((c) => toNames.has(cacheScopeName(c)) ? normalizeCacheScope(c, cachePrefix) : c);
|
|
886
|
+
const ghaTo = rawCacheTo.filter(isGhaCache);
|
|
887
|
+
if (defaultBranch && ghaTo.length) {
|
|
888
|
+
const prefix = expandPrefix(prefixTemplate, onDefaultBranch(vars, defaultBranch()), "cache prefix");
|
|
889
|
+
cacheFrom.push(...ghaTo.map((c) => normalizeCacheScope(withoutCacheMode(c), prefix)));
|
|
890
|
+
}
|
|
891
|
+
for (const rewritten of new Set(cacheFrom)) {
|
|
806
892
|
args.push(`--cache-from=${rewritten}`);
|
|
807
893
|
summary.cacheFrom.push(rewritten);
|
|
808
894
|
}
|
|
@@ -1169,18 +1255,50 @@ function envFlag(name) {
|
|
|
1169
1255
|
return true;
|
|
1170
1256
|
throw new CeError(`ce: ${name} must be true or false, not ${JSON.stringify(process.env[name])}`);
|
|
1171
1257
|
}
|
|
1172
|
-
|
|
1258
|
+
function shellQuote(arg) {
|
|
1259
|
+
return /^[A-Za-z0-9_@%+:,./-][A-Za-z0-9_@%+=:,./-]*$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1260
|
+
}
|
|
1261
|
+
function engineCommand(engine, args) {
|
|
1262
|
+
if (engine === "docker" || !system.which("gosu"))
|
|
1263
|
+
return [engine, ...args];
|
|
1264
|
+
const user = process.env.USER ?? "";
|
|
1265
|
+
if (!user) {
|
|
1266
|
+
logError("ce: gosu is available but USER is not set; cannot drop privileges. Set USER or remove gosu from PATH.");
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
return ["gosu", user, engine, ...args];
|
|
1270
|
+
}
|
|
1271
|
+
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
1272
|
+
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
1273
|
+
ensureHelperOnPath();
|
|
1173
1274
|
const command = findBuildCommand(engine, argv);
|
|
1174
1275
|
let args = argv;
|
|
1175
1276
|
let summary;
|
|
1176
1277
|
if (command) {
|
|
1177
1278
|
const vars = new CiMetadata;
|
|
1178
|
-
const labels = !command.tagsOnly && (ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1179
|
-
const
|
|
1279
|
+
const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1280
|
+
const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
|
|
1281
|
+
const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
|
|
1282
|
+
engine,
|
|
1283
|
+
vars,
|
|
1284
|
+
labels,
|
|
1285
|
+
defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined
|
|
1286
|
+
});
|
|
1180
1287
|
args = [...argv.slice(0, command.start), ...parsed.args];
|
|
1181
1288
|
summary = parsed.summary;
|
|
1182
1289
|
}
|
|
1183
|
-
|
|
1290
|
+
if (dryRun) {
|
|
1291
|
+
const run = engineCommand(engine, args);
|
|
1292
|
+
if (!run)
|
|
1293
|
+
return 1;
|
|
1294
|
+
if (summary)
|
|
1295
|
+
printBuildSummary(engine, args, summary);
|
|
1296
|
+
if (!system.which(engine))
|
|
1297
|
+
logError(`ce: warning: ${engine} is not on PATH, so running this here would fail`);
|
|
1298
|
+
io.stdout(`${run.map(shellQuote).join(" ")}
|
|
1299
|
+
`);
|
|
1300
|
+
return 0;
|
|
1301
|
+
}
|
|
1184
1302
|
if (!system.which(engine)) {
|
|
1185
1303
|
logError(`Engine not found: ${engine}`);
|
|
1186
1304
|
return 1;
|
|
@@ -1189,17 +1307,8 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, ociLabels) {
|
|
|
1189
1307
|
return 1;
|
|
1190
1308
|
if (summary)
|
|
1191
1309
|
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]);
|
|
1310
|
+
const run = engineCommand(engine, args);
|
|
1311
|
+
return run ? system.exec(run[0], run) : 1;
|
|
1203
1312
|
}
|
|
1204
1313
|
|
|
1205
1314
|
// src/login.ts
|
|
@@ -1352,7 +1461,8 @@ async function login() {
|
|
|
1352
1461
|
|
|
1353
1462
|
// src/cli.ts
|
|
1354
1463
|
function helpText() {
|
|
1355
|
-
return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--oci-labels]
|
|
1464
|
+
return `Usage: ${io.argv0()} [--tag-prefix=<prefix>] [--cache-prefix=<prefix>] [--oci-labels]
|
|
1465
|
+
[--cache-from-default] [--dry-run] [command|engine] [args]
|
|
1356
1466
|
|
|
1357
1467
|
Options:
|
|
1358
1468
|
--tag-prefix=<prefix>
|
|
@@ -1368,15 +1478,27 @@ Options:
|
|
|
1368
1478
|
Also available via CACHE_PREFIX environment variable.
|
|
1369
1479
|
--cache=<spec> Shorthand for passing <spec> to both --cache-from and
|
|
1370
1480
|
--cache-to. Supports the same forms as --cache-from/--cache-to.
|
|
1481
|
+
A gha cache-from made this way leaves out mode=, which
|
|
1482
|
+
only applies to export. The same cache-from given twice
|
|
1483
|
+
is passed once.
|
|
1484
|
+
--cache-from-default
|
|
1485
|
+
Also read each gha --cache-to from its scope on the
|
|
1486
|
+
default branch, from the cache prefix as it is there,
|
|
1487
|
+
so a new branch or PR starts from its cache.
|
|
1488
|
+
Also available via CACHE_FROM_DEFAULT=true.
|
|
1371
1489
|
--oci-labels Add org.opencontainers.image.* labels (source, revision,
|
|
1372
1490
|
version, title, description) from the Forgejo Actions
|
|
1373
1491
|
run to builds. A --label with the same key wins.
|
|
1374
1492
|
Also available via OCI_LABELS=true.
|
|
1493
|
+
--dry-run Print the build summary and the engine command, and
|
|
1494
|
+
exit without running it. Template errors still exit 2;
|
|
1495
|
+
a missing engine is only a warning.
|
|
1496
|
+
Also available via CE_DRY_RUN=true.
|
|
1375
1497
|
|
|
1376
1498
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1377
1499
|
{name} The value of name; an error if it has none in this run.
|
|
1378
1500
|
{?name} Expands to nothing; leaves the tag out unless name has
|
|
1379
|
-
a value.
|
|
1501
|
+
a value. In a prefix, leaves the prefix empty instead.
|
|
1380
1502
|
{image} Registry host and repository, lowercased
|
|
1381
1503
|
{ref} Branch name, pr-<number>, or tag name
|
|
1382
1504
|
{branch} {pr} {tag}
|
|
@@ -1388,6 +1510,11 @@ Placeholders, in build tags and in the tag and cache prefixes:
|
|
|
1388
1510
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1389
1511
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1390
1512
|
{env.NAME} The environment variable NAME
|
|
1513
|
+
{arg.NAME} The build arg NAME from this command's --build-arg;
|
|
1514
|
+
an error without one, even guarded, as ce does not
|
|
1515
|
+
read Containerfiles
|
|
1516
|
+
A guard that does not pass leaves its tag out or its prefix empty even if
|
|
1517
|
+
another guard there cannot be worked out.
|
|
1391
1518
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
1392
1519
|
in single quotes. Example:
|
|
1393
1520
|
-t '{image}:{ref},{?default}latest'
|
|
@@ -1428,7 +1555,7 @@ Environment variables (used by login):
|
|
|
1428
1555
|
`;
|
|
1429
1556
|
}
|
|
1430
1557
|
async function main(argv) {
|
|
1431
|
-
|
|
1558
|
+
const options = {};
|
|
1432
1559
|
let tagPrefix = process.env.TAG_PREFIX ?? "";
|
|
1433
1560
|
let cachePrefix = process.env.CACHE_PREFIX ?? process.env.TAG_PREFIX ?? "";
|
|
1434
1561
|
for (;; ) {
|
|
@@ -1442,7 +1569,11 @@ async function main(argv) {
|
|
|
1442
1569
|
else if (a?.startsWith("--cache-prefix="))
|
|
1443
1570
|
[cachePrefix, argv] = [a.slice("--cache-prefix=".length), argv.slice(1)];
|
|
1444
1571
|
else if (a === "--oci-labels")
|
|
1445
|
-
[ociLabels, argv] = [true, argv.slice(1)];
|
|
1572
|
+
[options.ociLabels, argv] = [true, argv.slice(1)];
|
|
1573
|
+
else if (a === "--cache-from-default")
|
|
1574
|
+
[options.cacheFromDefault, argv] = [true, argv.slice(1)];
|
|
1575
|
+
else if (a === "--dry-run")
|
|
1576
|
+
[options.dryRun, argv] = [true, argv.slice(1)];
|
|
1446
1577
|
else
|
|
1447
1578
|
break;
|
|
1448
1579
|
}
|
|
@@ -1456,7 +1587,7 @@ async function main(argv) {
|
|
|
1456
1587
|
if (command === "credential-helper")
|
|
1457
1588
|
return credentialHelper(rest);
|
|
1458
1589
|
if (isEngine(command))
|
|
1459
|
-
return runEngine(command, rest, tagPrefix, cachePrefix,
|
|
1590
|
+
return runEngine(command, rest, tagPrefix, cachePrefix, options);
|
|
1460
1591
|
io.stderr(helpText());
|
|
1461
1592
|
return 1;
|
|
1462
1593
|
}
|
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.3",
|
|
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",
|