@izykitten/ce 0.0.3-dev.2 → 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 +15 -4
- package/dist/ce.js +96 -29
- 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,7 +75,7 @@ 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. 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
|
|
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
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`.
|
|
70
81
|
|
|
@@ -80,7 +91,7 @@ A release on a SemVer tag, with the version aliases only for stable releases:
|
|
|
80
91
|
|
|
81
92
|
### Checking a template
|
|
82
93
|
|
|
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:
|
|
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:
|
|
84
95
|
|
|
85
96
|
```sh
|
|
86
97
|
GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
|
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);
|
|
@@ -395,7 +395,7 @@ function onDefaultBranch(vars, branch) {
|
|
|
395
395
|
if (UNKNOWN_ON_DEFAULT_BRANCH.has(name)) {
|
|
396
396
|
throw new CeError(`ce: the default branch's cache scope cannot use {${name}}: this run does not know its commit`);
|
|
397
397
|
}
|
|
398
|
-
return name === "image" || name.startsWith("env.") ? vars.get(name) : undefined;
|
|
398
|
+
return name === "image" || name.startsWith("env.") || name.startsWith("arg.") ? vars.get(name) : undefined;
|
|
399
399
|
}
|
|
400
400
|
};
|
|
401
401
|
}
|
|
@@ -494,12 +494,26 @@ function parse(template, vars) {
|
|
|
494
494
|
text(template.slice(last));
|
|
495
495
|
return parts;
|
|
496
496
|
}
|
|
497
|
-
function
|
|
498
|
-
|
|
497
|
+
function guardsPass(parts, vars) {
|
|
498
|
+
let unknown;
|
|
499
499
|
for (const part of parts) {
|
|
500
|
-
if ("name" in part
|
|
501
|
-
|
|
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
|
+
}
|
|
502
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;
|
|
503
517
|
let out = "";
|
|
504
518
|
for (const part of parts) {
|
|
505
519
|
if ("text" in part)
|
|
@@ -514,16 +528,21 @@ function expand(template, vars, encode) {
|
|
|
514
528
|
}
|
|
515
529
|
return out;
|
|
516
530
|
}
|
|
517
|
-
function checkValue(template, vars, what) {
|
|
518
|
-
|
|
519
|
-
|
|
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`);
|
|
520
535
|
}
|
|
521
536
|
}
|
|
522
|
-
function expandValue(template, vars, encode, what) {
|
|
523
|
-
checkValue(template, vars, what);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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}`);
|
|
527
546
|
}
|
|
528
547
|
}
|
|
529
548
|
return expand(template, vars, encode);
|
|
@@ -585,7 +604,7 @@ function expandImage(template, vars) {
|
|
|
585
604
|
return image;
|
|
586
605
|
}
|
|
587
606
|
function expandPrefix(prefix, vars, what) {
|
|
588
|
-
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what) : prefix;
|
|
607
|
+
return hasPlaceholders(prefix) ? expandValue(prefix, vars, encodeTagValue, what, true) : prefix;
|
|
589
608
|
}
|
|
590
609
|
function literalRef(repo, tag) {
|
|
591
610
|
if (tag.includes("/")) {
|
|
@@ -608,7 +627,7 @@ function rewriteTagValue(value, tagPrefix, vars = new CiMetadata) {
|
|
|
608
627
|
if (hasPlaceholders(repoTemplate))
|
|
609
628
|
checkValue(repoTemplate, vars, "image name");
|
|
610
629
|
if (hasPlaceholders(tagPrefix))
|
|
611
|
-
checkValue(tagPrefix, vars, "tag prefix");
|
|
630
|
+
checkValue(tagPrefix, vars, "tag prefix", true);
|
|
612
631
|
let repo;
|
|
613
632
|
const expandRepo = () => repo ??= hasPlaceholders(repoTemplate) ? expandImage(repoTemplate, vars) : repoTemplate;
|
|
614
633
|
if (tagList === undefined)
|
|
@@ -738,8 +757,27 @@ var SUMMARIZED = {
|
|
|
738
757
|
"--build-arg": "buildArgs",
|
|
739
758
|
"--label": "labels"
|
|
740
759
|
};
|
|
741
|
-
|
|
742
|
-
|
|
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 = [];
|
|
743
781
|
const rawCacheFrom = [];
|
|
744
782
|
const rawCacheTo = [];
|
|
745
783
|
const summary = {
|
|
@@ -754,16 +792,23 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
754
792
|
context: ""
|
|
755
793
|
};
|
|
756
794
|
let templated = false;
|
|
795
|
+
const buildArgs = new Map;
|
|
796
|
+
let argFile = false;
|
|
757
797
|
let rest = [];
|
|
758
798
|
const addTags = (value, flag) => {
|
|
759
799
|
templated ||= hasPlaceholders(value);
|
|
760
|
-
|
|
761
|
-
for (const ref of refs)
|
|
762
|
-
args.push(`${flag}${ref}`);
|
|
763
|
-
summary.tags.push(...refs);
|
|
800
|
+
parts.push({ tags: value, flag });
|
|
764
801
|
};
|
|
765
802
|
const summarize = (flag, value) => {
|
|
766
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
|
+
}
|
|
767
812
|
if (field === "buildArgs" || field === "labels")
|
|
768
813
|
summary[field].push(value);
|
|
769
814
|
else
|
|
@@ -798,21 +843,34 @@ function parseBuildArgs(argv, tagPrefix, cachePrefix, { vars = new CiMetadata, l
|
|
|
798
843
|
const flag = name;
|
|
799
844
|
if (attached === undefined) {
|
|
800
845
|
const v = next();
|
|
801
|
-
|
|
846
|
+
parts.push(arg, v);
|
|
802
847
|
summarize(flag, v);
|
|
803
848
|
} else {
|
|
804
|
-
|
|
849
|
+
parts.push(arg);
|
|
805
850
|
summarize(flag, attached);
|
|
806
851
|
}
|
|
852
|
+
} else if (name === "--build-arg-file") {
|
|
853
|
+
argFile = true;
|
|
854
|
+
parts.push(arg);
|
|
855
|
+
if (attached === undefined)
|
|
856
|
+
parts.push(next());
|
|
807
857
|
} else if (arg.startsWith("-f") && arg.length > 2) {
|
|
808
|
-
|
|
858
|
+
parts.push(arg);
|
|
809
859
|
summary.dockerfile = arg.slice(2);
|
|
810
860
|
} else {
|
|
811
|
-
|
|
861
|
+
parts.push(arg);
|
|
812
862
|
if (!arg.startsWith("-"))
|
|
813
863
|
summary.context = arg;
|
|
814
864
|
}
|
|
815
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
|
+
});
|
|
816
874
|
if (templated && !summary.tags.length) {
|
|
817
875
|
throw new CeError("ce: the guards in the -t templates left out every tag, so the image would have none");
|
|
818
876
|
}
|
|
@@ -1212,6 +1270,7 @@ function engineCommand(engine, args) {
|
|
|
1212
1270
|
}
|
|
1213
1271
|
async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
1214
1272
|
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
1273
|
+
ensureHelperOnPath();
|
|
1215
1274
|
const command = findBuildCommand(engine, argv);
|
|
1216
1275
|
let args = argv;
|
|
1217
1276
|
let summary;
|
|
@@ -1220,6 +1279,7 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1220
1279
|
const labels = !command.tagsOnly && (options.ociLabels ?? envFlag("OCI_LABELS")) ? vars.labels() : {};
|
|
1221
1280
|
const fromDefault = !command.tagsOnly && (options.cacheFromDefault ?? envFlag("CACHE_FROM_DEFAULT"));
|
|
1222
1281
|
const parsed = parseBuildArgs(argv.slice(command.start), tagPrefix, cachePrefix, {
|
|
1282
|
+
engine,
|
|
1223
1283
|
vars,
|
|
1224
1284
|
labels,
|
|
1225
1285
|
defaultBranch: fromDefault ? () => vars.defaultBranch() : undefined
|
|
@@ -1233,11 +1293,12 @@ async function runEngine(engine, argv, tagPrefix, cachePrefix, options = {}) {
|
|
|
1233
1293
|
return 1;
|
|
1234
1294
|
if (summary)
|
|
1235
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`);
|
|
1236
1298
|
io.stdout(`${run.map(shellQuote).join(" ")}
|
|
1237
1299
|
`);
|
|
1238
1300
|
return 0;
|
|
1239
1301
|
}
|
|
1240
|
-
ensureHelperOnPath();
|
|
1241
1302
|
if (!system.which(engine)) {
|
|
1242
1303
|
logError(`Engine not found: ${engine}`);
|
|
1243
1304
|
return 1;
|
|
@@ -1430,13 +1491,14 @@ Options:
|
|
|
1430
1491
|
run to builds. A --label with the same key wins.
|
|
1431
1492
|
Also available via OCI_LABELS=true.
|
|
1432
1493
|
--dry-run Print the build summary and the engine command, and
|
|
1433
|
-
exit without running it. Template errors still exit 2
|
|
1494
|
+
exit without running it. Template errors still exit 2;
|
|
1495
|
+
a missing engine is only a warning.
|
|
1434
1496
|
Also available via CE_DRY_RUN=true.
|
|
1435
1497
|
|
|
1436
1498
|
Placeholders, in build tags and in the tag and cache prefixes:
|
|
1437
1499
|
{name} The value of name; an error if it has none in this run.
|
|
1438
1500
|
{?name} Expands to nothing; leaves the tag out unless name has
|
|
1439
|
-
a value.
|
|
1501
|
+
a value. In a prefix, leaves the prefix empty instead.
|
|
1440
1502
|
{image} Registry host and repository, lowercased
|
|
1441
1503
|
{ref} Branch name, pr-<number>, or tag name
|
|
1442
1504
|
{branch} {pr} {tag}
|
|
@@ -1448,6 +1510,11 @@ Placeholders, in build tags and in the tag and cache prefixes:
|
|
|
1448
1510
|
From a SemVer tag such as v1.2.3 (v optional)
|
|
1449
1511
|
{stable} Set only for a SemVer tag without a pre-release
|
|
1450
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.
|
|
1451
1518
|
Values are made safe for a tag ("ci/x" becomes "ci-x"). Quote templates
|
|
1452
1519
|
in single quotes. Example:
|
|
1453
1520
|
-t '{image}:{ref},{?default}latest'
|
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",
|