@izykitten/ce 0.0.3-dev.0 → 0.0.3-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -26
- package/dist/ce.js +3369 -1006
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# @izykitten/ce
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CI wrapper for `docker`, `podman` and `buildah` builds and npm/Bun publishing: registry login (including Forgejo OIDC), container tag and cache rewriting, staged package releases, and a Docker credential helper that refreshes the Forgejo token.
|
|
4
4
|
|
|
5
5
|
Started as a port of the Python `ce` script from the `code.izy.sh/library/runner` image, with the same commands, flags, environment variables and exit codes, so a workflow only has to swap `ce` for `npx` or `bunx`. It runs on Node (22.15 or later) and Bun. Any job with Node or Bun and an engine CLI can use it; it no longer needs the runner image.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npx @izykitten/ce login
|
|
10
|
+
npx @izykitten/ce docker login
|
|
11
11
|
npx @izykitten/ce docker buildx build --push --cache type=gha,mode=max -t "$SERVER/$REPO:$REF,latest" .
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
`bunx @izykitten/ce …` works the same. The scripts run under `node`; `bun` runs them too when invoked as `bun node_modules/.bin/ce …` or through `bunx --bun`. When installed as a dependency, call it as `ce` (`npx ce …`). Run `ce` with no arguments for the full help, including every environment variable `login` reads.
|
|
15
15
|
|
|
16
|
+
Ce options immediately follow the tool, before its native arguments:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
ce docker --tag-prefix ci --dry-run buildx build -t 'registry.example/app:{ref}' .
|
|
20
|
+
ce npm --stage publish ./package.tgz
|
|
21
|
+
ce bun publish --tag next
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Options are scoped to their tool: container options apply to Docker, Podman and Buildah; publishing options apply to npm and Bun; `--dry-run` is shared. Ce stops consuming its options at the first native argument, or at an explicit `--`. For example, `ce docker --tag-prefix ci --context remote build .` passes `--context remote build .` to Docker. Existing options before the tool remain supported; a scalar option after the tool overrides one before it. Native arguments after the command retain their meaning: `ce npm --dry-run publish` only prints ce's command, while `ce npm publish --dry-run` runs npm's own dry run and its applicable lifecycle scripts.
|
|
25
|
+
|
|
16
26
|
In a Forgejo workflow:
|
|
17
27
|
|
|
18
28
|
```yaml
|
|
@@ -22,59 +32,293 @@ In a Forgejo workflow:
|
|
|
22
32
|
REGISTRY_USER: ${{ forgejo.actor }}
|
|
23
33
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }}
|
|
24
34
|
REGISTRY_HOST: ${{ forgejo.server_url }}
|
|
25
|
-
run: bunx @izykitten/ce login
|
|
35
|
+
run: bunx @izykitten/ce docker login
|
|
26
36
|
- name: Container Build & Push
|
|
27
|
-
run:
|
|
37
|
+
run: >
|
|
38
|
+
bunx @izykitten/ce docker buildx build --push
|
|
39
|
+
-t '{image}:{ref},{?version}{version},{?stable}{major}.{minor},{?stable}{major},{?stable}latest' .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
[Recommended tags](#recommended-tags) says which tags that gives, and how to pick what `latest` follows.
|
|
43
|
+
|
|
44
|
+
For container engines, build commands are rewritten: `build`, `buildx build` and `image build` for Docker and Podman, `build` and `bud` for Buildah, and the tags of `docker buildx imagetools create`. Bare `<engine> login` uses ce's configured login; other commands, such as `docker run -t`, reach the engine unchanged.
|
|
45
|
+
|
|
46
|
+
## npm and Bun publishing
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
ce npm publish # direct publication with npm
|
|
50
|
+
ce npm --stage publish # npm staging, awaiting maintainer approval
|
|
51
|
+
ce bun publish # direct publication with Bun
|
|
52
|
+
ce bun --stage publish ./package.tgz # npm staging; no Bun executable is needed
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--tolerate-republish` makes a release run idempotent: when the registry already has the package's version, ce exits 0 without uploading. With npm it asks the target registry first, with `npm view` and, when staging, `npm stage list`, using the same credentials as the upload; a rejected stage does not count. A failed query stops the publish rather than guessing. Direct Bun publishing passes Bun's own `--tolerate-republish`. The option needs a local package directory or tarball.
|
|
56
|
+
|
|
57
|
+
Staging always runs [`npm stage publish`](https://docs.npmjs.com/cli/v12/commands/npm-stage), using npm's configuration and lifecycle scripts. It requires an npm version with that command (CI uses 12.2.1). Direct publication uses the requested client. Existing authentication remains in effect unless one of ce's authentication options is selected. Ce does not retry a failed publish through another mode. The CI workflow stages npmjs releases and publishes directly to Forgejo.
|
|
58
|
+
|
|
59
|
+
For npm token authentication, name the environment variable that holds the token and pass an explicit native registry:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
ce npm --token-env=NODE_AUTH_TOKEN --stage publish ./package.tgz \
|
|
63
|
+
--registry=https://registry.npmjs.org/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Ce supplies a registry/path-scoped token through npm's child-process environment. It preserves existing npm configuration and writes no credential files. It checks authentication with `npm whoami` before publishing; `--no-check-auth` skips that probe, and `--check-auth` enables it again. The credentials are passed only to the npm backend, including `ce bun --stage`; direct Bun publishing continues to use Bun's authentication. Native configuration retains its normal precedence, including scoped registry mappings. Ce's `--dry-run` previews the command without reading the token, authenticating, or running lifecycle scripts.
|
|
67
|
+
|
|
68
|
+
Forgejo's integrated npm registry can use the same Actions OIDC exchange as Docker login:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
enable-openid-connect: true
|
|
72
|
+
env:
|
|
73
|
+
AUD: your-authorized-integration-audience
|
|
74
|
+
steps:
|
|
75
|
+
- run: ce npm --forgejo-oidc publish ./package.tgz
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--forgejo-oidc` reads `AUD`, `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, fetches a JWT through ce's existing token exchange, and scopes it to the selected npm registry. Without a native `--registry`, ce publishes to the repository owner's registry on the server running the job, `$FORGEJO_SERVER_URL/api/packages/<owner>/npm/`. The registry and token endpoint must have the same HTTP(S) origin. Tokens expiring within five minutes are refused. The JWT is fetched immediately before authentication/publication; npm receives a fixed token for that invocation. No credentials are saved and no static-token fallback occurs. Forgejo has no npm staging or `whoami` endpoint, so this mode requires direct npm publication and skips the authentication probe by default. Use `--check-auth` only for a compatible endpoint. The two authentication modes are mutually exclusive; existing native `ce npm login` behavior is unchanged.
|
|
79
|
+
|
|
80
|
+
Without an explicit tag, ce reads the selected package's version and infers the first prerelease label: `1.2.3-dev.8` gives `dev`, `1.2.3-rc.1` gives `rc`. Stable versions receive no injected tag, preserving the client's normal defaults and npm's check against implicitly moving `latest` to an older version. Numeric labels, version-like labels and the prerelease label `latest` require an explicit `--tag`.
|
|
81
|
+
|
|
82
|
+
Native `--tag` arguments and `publishConfig.tag` take precedence over inference. With npm as the backend, `npm_config_tag`/`NPM_CONFIG_TAG` also take precedence over inference; Bun does not read those tag variables. `--tag-env=NAME` reads an optional override and supplies it as an explicit native tag, so it overrides package/environment configuration. Native `--tag` wins over this option. An empty or unset variable leaves normal configuration and inference in effect. Use `ce npm --no-auto-tag publish ...` to leave tag selection to native configuration, including a `tag` in `.npmrc`; a supplied `--tag-env` override remains explicit. Ce options must precede native arguments; a misplaced `--stage` is rejected rather than passed to a client that could ignore it and publish directly.
|
|
83
|
+
|
|
84
|
+
Package preparation and release checks can also run inside ce:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
node bin/ce npm --pack-output-env=GITHUB_OUTPUT pack
|
|
88
|
+
node bin/ce npm --token-env=NODE_AUTH_TOKEN --stage publish "$PACKAGE_TARBALL" \
|
|
89
|
+
--registry=https://registry.npmjs.org/
|
|
28
90
|
```
|
|
29
91
|
|
|
30
|
-
|
|
92
|
+
`--pack-output-env=NAME` names the environment variable holding an output-file path. Ce runs `npm pack` with captured JSON and `--foreground-scripts=false`, preserving lifecycle scripts while keeping their output out of the JSON protocol. It reads npm's configuration for the pack destination, validates the actual tarball and its version, and appends `tarball=...` and `version=...` records only after success. Both npm's array and keyed-object JSON formats are supported. Metadata preparation requires one package and a real tarball; workspace mode, multiple package specs and native `--dry-run` are rejected. Ce's own `--dry-run` previews the pack command without invoking npm or writing outputs. Plain `ce npm pack` retains native behavior.
|
|
93
|
+
|
|
94
|
+
When a CI run builds a SemVer tag such as `v1.2.3`, the same ref that `{version}` uses, publishing and metadata-enabled packing check that the package version matches it. Other runs skip the comparison, and so does a target ce cannot read locally, such as a remote package spec or workspaces; ce prints a warning and leaves that publish to the client. `--version-tag-env=NAME` replaces the CI ref with a tag supplied through that variable. It accepts a SemVer tag with an optional `v`, or a `refs/tags/` ref. An empty or unset variable skips comparison. With `--version-tag-env`, publish validation requires one local directory or tarball, including when `--tag` is explicit or inference is disabled. Metadata-enabled packing checks the produced tarball before exposing outputs, and publishing checks it before authentication. This option also enables metadata handling for `npm pack` without an output file, printing the tarball filename (or native JSON when `--json` is present).
|
|
95
|
+
|
|
96
|
+
Inference supports one local package directory or tarball; tarball metadata is read with the system `tar` command. Remote package specs, workspace publishing and native options whose value syntax ce does not know require `--tag` or `--no-auto-tag`. Bun-specific options such as `--cwd`, `--filter` and `--gzip-level` are rejected for Bun staging, because that mode runs npm; use npm-compatible arguments or run ce from the package directory. Other npm/Bun commands pass through unchanged.
|
|
97
|
+
|
|
98
|
+
Inference selects a channel, not a release ordering policy. Version-changing lifecycle hooks can change a directory package after ce reads it; publish a packed tarball or set `--tag` explicitly in that case. Native first-publication behavior also remains: [Bun documents that an initial package version receives `latest` in addition to its specified tag](https://bun.com/docs/pm/cli/publish#tag). `--stage` keeps approval separate from the upload.
|
|
31
99
|
|
|
32
100
|
## Tags from the Forgejo run
|
|
33
101
|
|
|
34
|
-
Tags and the `TAG_PREFIX`/`CACHE_PREFIX` values can use placeholders, filled from the Forgejo Actions environment and its event file. ce makes no API calls and
|
|
102
|
+
Tags and the `TAG_PREFIX`/`CACHE_PREFIX` values can use placeholders, filled from the Forgejo Actions environment and its event file. ce makes no API calls, and reads nothing from the checkout but the Containerfile, for a `{arg.NAME}` that needs its default.
|
|
35
103
|
|
|
36
|
-
| Placeholder
|
|
37
|
-
|
|
38
|
-
| `{image}`
|
|
39
|
-
| `{ref}`
|
|
40
|
-
| `{branch}`, `{pr}`, `{tag}`
|
|
41
|
-
| `{default}`
|
|
42
|
-
| `{revision}`, `{sha}`
|
|
43
|
-
| `{version}`, `{major}`, `{minor}`, `{patch}` | from a SemVer tag such as `v1.2.3` or `1.2.3-rc.1`
|
|
44
|
-
| `{stable}`
|
|
45
|
-
| `{env.NAME}`
|
|
104
|
+
| Placeholder | Value |
|
|
105
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
106
|
+
| `{image}` | registry host and repository, lowercased, e.g. `code.izy.sh/library/gateway` |
|
|
107
|
+
| `{ref}` | the branch name, `pr-<number>` for a pull request, or the tag name |
|
|
108
|
+
| `{branch}`, `{pr}`, `{tag}` | the branch, PR number or tag, set only when building that kind of ref |
|
|
109
|
+
| `{default}` | set only when building the default branch, never for a pull request or tag |
|
|
110
|
+
| `{revision}`, `{sha}` | the commit, in full and as its first 7 characters |
|
|
111
|
+
| `{version}`, `{major}`, `{minor}`, `{patch}` | from a SemVer tag such as `v1.2.3` or `1.2.3-rc.1` |
|
|
112
|
+
| `{stable}` | set only for a SemVer tag without a pre-release |
|
|
113
|
+
| `{env.NAME}` | the environment variable `NAME`, e.g. a value from an earlier step |
|
|
114
|
+
| `{arg.NAME}` | the build arg `NAME` as the build gets it, from `--build-arg` or the Containerfile |
|
|
46
115
|
|
|
47
116
|
`{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
117
|
|
|
49
|
-
|
|
118
|
+
Guards can also test absence, compare a value, or accept alternatives:
|
|
50
119
|
|
|
51
|
-
|
|
120
|
+
| Guard | Keeps the output when |
|
|
121
|
+
| ------------------------------- | --------------------------------------------------------- |
|
|
122
|
+
| `{!pr}` | this is not a pull request |
|
|
123
|
+
| `{?env.PUBLISH_DOCKERHUB=true}` | `PUBLISH_DOCKERHUB` is exactly `true` |
|
|
124
|
+
| `{!env.CHANNEL=internal}` | `CHANNEL` is absent or differs from `internal` |
|
|
125
|
+
| `{?branch\|tag}` | this is a branch or Git-tag build |
|
|
126
|
+
| `{!branch\|tag}` | neither `branch` nor `tag` has a value |
|
|
127
|
+
| `{?default\|env.PUBLISH=true}` | this is the default branch or `PUBLISH` is exactly `true` |
|
|
52
128
|
|
|
53
|
-
|
|
129
|
+
Comparisons are case-sensitive and use the raw value before tag encoding. The right side is a literal, with no placeholder expansion; it can contain commas, whitespace and `=`, but not braces or `|`. Empty values count as absent, so use `{!name}` to test absence. `{?env.PUBLISH}` still checks presence: the string `false` passes it. `!` negates the whole expression, including its alternatives. Adjacent guards require every condition to pass.
|
|
54
130
|
|
|
55
|
-
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
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. Guards before the image name apply to its whole `-t` argument; guards embedded inside the image name are an error.
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
Negation does not turn unavailable metadata into an absent value: `{!pr}` outside a run is still an error. An OR condition succeeds if any alternative matches, even if another cannot be worked out; otherwise unavailable metadata is an error. Names in every alternative are checked, including those that are not needed to decide the result.
|
|
138
|
+
|
|
139
|
+
Quote a template in a workflow's `env:`: YAML reads `CACHE_PREFIX: {ref}` as a mapping, and `{?pr}pr-{pr}` not at all.
|
|
140
|
+
|
|
141
|
+
A branch, PR and default-branch build, for an image deployed from its default branch with no releases yet, so `latest` follows that branch:
|
|
56
142
|
|
|
57
143
|
```yaml
|
|
58
144
|
- name: Container Build & Push
|
|
59
145
|
env:
|
|
60
|
-
CACHE_PREFIX:
|
|
146
|
+
CACHE_PREFIX: "{ref}"
|
|
61
147
|
CADDY_VERSION: ${{ steps.caddy.outputs.version }}
|
|
62
148
|
run: >
|
|
63
149
|
bunx @izykitten/ce docker buildx build --push --cache type=gha,mode=max
|
|
64
150
|
-t '{image}:{ref},{?default}latest,{?default}caddy-{env.CADDY_VERSION}' .
|
|
65
151
|
```
|
|
66
152
|
|
|
67
|
-
|
|
153
|
+
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` after the engine name and before its native arguments, 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.
|
|
154
|
+
|
|
155
|
+
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`.
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
### Conditional destinations and shared tag lists
|
|
160
|
+
|
|
161
|
+
A guard before the image name controls every tag in that `-t`. This keeps PR images in your registry while publishing the same branch and release tags to Docker Hub:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
run: |
|
|
165
|
+
npx -y @izykitten/ce docker buildx build --push \
|
|
166
|
+
--cache type=gha,mode=max \
|
|
167
|
+
-t '{image}:{ref},{?default}sysbox' \
|
|
168
|
+
-t '{!pr}docker.io/izykitten/forgejo-runner:{ref},{?default}sysbox' \
|
|
169
|
+
-f Dockerfile .
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The Docker Hub argument is omitted entirely on PRs, including `pull_request_target`. Otherwise its tag list is unchanged. Each remaining tag still applies its own guards, so only the default branch gets `sysbox`.
|
|
173
|
+
|
|
174
|
+
To share a tag list across destinations, define it once with `--tag-list NAME=TEMPLATES` immediately after the engine name and use `{tags.NAME}`:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
run: |
|
|
178
|
+
npx -y @izykitten/ce docker \
|
|
179
|
+
--tag-list 'runner={ref},{?default}sysbox' \
|
|
180
|
+
buildx build --push \
|
|
181
|
+
--cache type=gha,mode=max \
|
|
182
|
+
-t '{image}:{tags.runner}' \
|
|
183
|
+
-t '{!pr}docker.io/izykitten/forgejo-runner:{tags.runner}' \
|
|
184
|
+
-f Dockerfile .
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Add `{?env.PUBLISH_DOCKERHUB=true}` beside `{!pr}` to also require an explicit publishing choice. Every destination can have its own conditions and literal tags around the shared list, such as `app:{tags.runner},debug`.
|
|
188
|
+
|
|
189
|
+
Names use letters, digits and `_`, starting with a letter or `_`; each name can be defined only once. The attached form, `--tag-list=runner=...`, also works. Lists are comma/whitespace-separated tag templates and can reference other lists, defined in any order. A `{tags.NAME}` reference must be a whole entry after the image's colon; it cannot appear in an image name, a prefix, a guard or part of another tag. Ordinary `{env.NAME}` and `{arg.NAME}` values stay scalar and cannot introduce tags. Tags are deduplicated in the order they first appear.
|
|
190
|
+
|
|
191
|
+
Referenced lists and all their placeholder names are checked even when a destination is skipped. Undefined lists, cycles and malformed templates fail with exit 2 before the engine runs. Values inside skipped tags are not required, and ce reads no configuration file for tag lists.
|
|
192
|
+
|
|
193
|
+
### Recommended tags
|
|
194
|
+
|
|
195
|
+
For an image others pull by version:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
-t '{image}:{ref},{?version}{version},{?stable}{major}.{minor},{?stable}{major},{?stable}latest'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
| Build | Tags |
|
|
202
|
+
| ----------------- | --------------------------------------- |
|
|
203
|
+
| branch `main` | `main` |
|
|
204
|
+
| branch `ci/fix` | `ci-fix` |
|
|
205
|
+
| pull request 3 | `pr-3` |
|
|
206
|
+
| tag `v1.2.3` | `v1.2.3`, `1.2.3`, `1.2`, `1`, `latest` |
|
|
207
|
+
| tag `v1.3.0-rc.1` | `v1.3.0-rc.1`, `1.3.0-rc.1` |
|
|
208
|
+
| tag `v0.3.1` | `v0.3.1`, `0.3.1`, `0.3`, `0`, `latest` |
|
|
209
|
+
|
|
210
|
+
- `{ref}` names every build after what was pushed, so a branch or pull request can be pulled to try it, and a release keeps its git tag's name, `v` included.
|
|
211
|
+
- `{?version}{version}` adds the version without the `v`, as most images are tagged, for pre-releases too.
|
|
212
|
+
- `1.2` and `1` follow the newest stable release in their line, for those who want fixes but not the next major version. Before 1.0, SemVer lets any minor release break things, so `0` can move from 0.3 to a 0.4 that does; pin `0.3` to avoid that.
|
|
213
|
+
- `latest` is the newest stable release, as on most images. A repository with no release tag yet has no `latest`: deploy `main` instead, or use `{?default}latest` until the first release.
|
|
214
|
+
|
|
215
|
+
The guard on `latest` picks what it follows:
|
|
216
|
+
|
|
217
|
+
| Tag | `latest` follows |
|
|
218
|
+
| --------------------- | ----------------------------------------- |
|
|
219
|
+
| `{?stable}latest` | stable releases |
|
|
220
|
+
| `{?version}latest` | every SemVer tag, pre-releases included |
|
|
221
|
+
| `{?tag}latest` | every tag |
|
|
222
|
+
| `{?default}latest` | the default branch |
|
|
223
|
+
| `{?env.LATEST}latest` | builds where an earlier step set `LATEST` |
|
|
224
|
+
|
|
225
|
+
Guards go by the build, not by what is in the registry: `latest` and the version aliases move to the last build whose guards pass. A `v1.2.4` fix pushed after `v2.0.0`, or a rebuild of an older release, moves them back to it. For a rule such as "only the highest version" or "the default branch until the first release", have an earlier step check the registry and set the variable an `{?env.NAME}` guard reads.
|
|
226
|
+
|
|
227
|
+
`{ref}` moves with each push. To also keep every branch and pull request build under a name of its own, add `{?branch}{ref}-{sha},{?pr}{ref}-{sha}`, which gives `main-0123456` and `pr-3-0123456`.
|
|
228
|
+
|
|
229
|
+
### Build args and the Containerfile
|
|
230
|
+
|
|
231
|
+
`{arg.NAME}` is the value the build gets for the build arg `NAME`, so a tag can carry a version from the command line or the Containerfile. The Containerfile is only read when a `{arg.NAME}` needs it; builds without one never open it.
|
|
232
|
+
|
|
233
|
+
With `--build-arg`, it is that value:
|
|
68
234
|
|
|
69
235
|
```sh
|
|
70
|
-
-t '{image}:{
|
|
236
|
+
ce docker build --build-arg VERSION="$(cat VERSION)" -t '{image}:{arg.VERSION}' .
|
|
71
237
|
```
|
|
72
238
|
|
|
73
|
-
`
|
|
239
|
+
The last `--build-arg NAME` wins, and `--build-arg NAME` alone takes the environment's `NAME`. If that is not set, Docker keeps an earlier value, while podman and buildah fall back to the Containerfile's default. An empty value (`--build-arg NAME=`) is no value.
|
|
240
|
+
|
|
241
|
+
Without a `--build-arg`, it is the Containerfile's default:
|
|
242
|
+
|
|
243
|
+
```dockerfile
|
|
244
|
+
ARG CADDY_VERSION=2.11.7
|
|
245
|
+
FROM caddy:builder
|
|
246
|
+
ARG CADDY_VERSION
|
|
247
|
+
RUN xcaddy build v${CADDY_VERSION}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`-t '{image}:caddy-{arg.CADDY_VERSION}'` then tags `caddy-2.11.7`, with nothing on the command line.
|
|
251
|
+
|
|
252
|
+
**What ce reads**
|
|
253
|
+
|
|
254
|
+
- **The file:** the `-f` file (with Docker, the last one), else the context's `Dockerfile` (or `dockerfile`); podman and buildah look for `Containerfile` first.
|
|
255
|
+
- **The stage:** the one being built, `--target`'s or else the last. Other stages do not matter, unless this one is built `FROM` one of them, which it then starts from.
|
|
256
|
+
- **The default:** the stage's own `ARG NAME=…`, the last one winning, or that of the stage it is built from; else the global `ARG NAME=…` before the first `FROM`, which a bare `ARG NAME` in the stage, or a `FROM` line, also gets. No default anywhere, or an empty one, is no value, and a guard leaves its tag out.
|
|
257
|
+
- **`$` in a default:** worked out as the engine does, from the `ARG`s, `--build-arg`s and `ENV`s before it, with `$NAME`, `${NAME}`, `${NAME:-x}` and `${NAME:+x}` (and with Docker, `${NAME-x}` and `${NAME+x}`). In a stage from `scratch`, a name nothing sets is empty, and `PATH` is the engines' `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`, which with podman and buildah is an `ENV`, so it wins over an `ARG PATH`.
|
|
258
|
+
- **Builtins** such as `TARGETPLATFORM`, and `SOURCE_DATE_EPOCH` and proxies such as `HTTP_PROXY`, which the engine may set itself, are the builder's, unless a `--build-arg` sets them. podman and buildah pass their environment's proxies only to `RUN`, so with them a proxy is like any other name. With a `--platform` such as `linux/arm/v7`, the `TARGET` ones are that platform's, as the engine normalizes it: `linux/arm` is `linux/arm/v7`, `linux/aarch64` is `linux/arm64`, a bare architecture such as `arm64` is for Linux when ce runs on Linux, and with Docker `linux/arm64/v8` is `linux/arm64`. With several, in one `--platform` such as `linux/amd64,linux/arm64` or in several, each is built on its own and every one gets the same tags, so ce works the value out for each: one they share is used, and one that differs is refused, naming two platforms it differs between. podman and buildah set them as ce works them out from podman 5.2 and buildah 1.37, so ce asks the engine its version (`podman info`, which with `--remote` is the server's); older ones let a `--build-arg TARGETPLATFORM` win, and podman 3.4 and buildah 1.23 ignore `--platform`. Docker also takes a builtin's own `ARG` default, and works out a `SOURCE_DATE_EPOCH` that is not a plain number such as `1700000000` itself.
|
|
259
|
+
- **podman and buildah** differ inside a stage: an `ENV`, the base image's too, wins over an `ARG`, so on a base image ce only works out a reference to a name an `ENV` there sets; from `scratch`, the stage's `ARG`s count too. `--env` sets an `ENV` at the start of every stage (`--env NAME` and `--env NAME*` from the environment), and `--unsetenv` drops the base image's. `--from` replaces the first `FROM`'s image, so that stage is on a base image even when its `FROM` is `scratch`. A stage gets the `ARG`s of the stage its `FROM` names as written, so none through `FROM $BASE`. `TARGETPLATFORM` is always the platform they build, and before the first `FROM` the builtins are not the `--platform`'s, but the machine's, or empty before podman 5.7 and buildah 1.42.
|
|
260
|
+
|
|
261
|
+
**What ce refuses**, with exit 2 and the reason:
|
|
262
|
+
|
|
263
|
+
- **The file is not there to read:** `-f -`, a `-f` URL or a remote context; with podman or buildah, several `-f` files or a `.in` one for `cpp`.
|
|
264
|
+
- **Another frontend:** a `# syntax=` directive or `BUILDKIT_SYNTAX` naming anything but `docker/dockerfile` untagged, `:1`, or `:1.21` and later, which may be pinned by a digest.
|
|
265
|
+
- **A value ce cannot know:** a reference to a name the base image's `ENV` may set; a builtin of the machine that builds, such as `BUILDARCH`; a `TARGET` one without a `--platform`, with one ce does not read (such as `linux` alone, or an empty one in a list, which podman and buildah build for the machine), with one that differs between the platforms, with podman before 5.2 or buildah before 1.37, or with podman's `--arch`, `--os`, `--variant` or `--all-platforms`; a form like `${NAME#x}`; with podman or buildah, a `$` in a `--env` value, which they expand, or a character they escape there, such as a line end or a tab, and quotes in a `--unsetenv`.
|
|
266
|
+
- **Instructions ce does not read:** an `ARG` or `ENV` name with something to expand, such as `ENV $NAME=x`, or with flags.
|
|
267
|
+
- **Characters other than ASCII's:** letters in an instruction, a stage name or a name after a `$`; space in an `ARG`, `ENV` or `FROM` line or a heredoc's line. Also a line ending in more than one carriage return.
|
|
268
|
+
- **Heredocs ce cannot place:** a delimiter that is not a word, which may be quoted; on its line, a `${}` with space or quotes in its word outside double quotes, or with quotes, `$` or `<` in it anywhere.
|
|
269
|
+
- **Stages that change under it:** a `--build-context` replacing a stage the built one comes from (with podman and buildah, what a `FROM` names); with podman or buildah, a stage built from one with an `ONBUILD ARG` or `ONBUILD ENV`, which they run only with the `docker` image format, unless `--format` or `BUILDAH_FORMAT` says which (`containers.conf` may change the default).
|
|
270
|
+
|
|
271
|
+
Two places always need a `--build-arg NAME`: a `{arg.NAME}` in the cache prefix with `CACHE_FROM_DEFAULT`, as the default branch may have another Containerfile; and with podman's `--build-arg-file`, which ce does not read. ce does not see a base image's own `ONBUILD` lines, and does not model Docker's legacy builder (`DOCKER_BUILDKIT=0`). Every rule here has a test in `test/containerfile.test.ts`.
|
|
272
|
+
|
|
273
|
+
### Checking a template
|
|
274
|
+
|
|
275
|
+
`--dry-run` after the engine name, or `CE_DRY_RUN=true`, prints the build summary, skipped destinations and tags with the guards that excluded them, and the engine command ce would run. Explanations go to stderr without resolved variable values; stdout contains only the shell command. It exits 0 without running the command 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. The one exception is a `TARGET` arg from `--platform` with podman or buildah, which needs the engine's version: ce asks it, waiting for podman as a build does, and without an answer refuses the value, so the dry run exits 2:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
|
|
279
|
+
ce docker --dry-run buildx build -t '{image}:{ref},{?stable}{major}.{minor}' .
|
|
280
|
+
```
|
|
74
281
|
|
|
75
282
|
### OCI labels
|
|
76
283
|
|
|
77
|
-
|
|
284
|
+
Builds in a run get these `org.opencontainers.image.` labels, and the same keys as annotations:
|
|
285
|
+
|
|
286
|
+
| Key | Value |
|
|
287
|
+
| ------------- | -------------------------------------------------------------------------------------- |
|
|
288
|
+
| `source` | the repository's page, `{server}/{owner}/{repo}` |
|
|
289
|
+
| `url` | the repository's website from the event, or else its page |
|
|
290
|
+
| `revision` | the commit, `GITHUB_SHA` |
|
|
291
|
+
| `version` | the tag's version, such as `1.2.3`, or else the ref's name (`pr-N` for a pull request) |
|
|
292
|
+
| `title` | the repository's name |
|
|
293
|
+
| `description` | the repository's description from the event |
|
|
294
|
+
| `created` | when ce started the build, to the second, or the time `SOURCE_DATE_EPOCH` gives |
|
|
295
|
+
|
|
296
|
+
Keys the run cannot tell are left out, and outside a run there are none, `created` included. `OCI_LABELS=false`, or `--no-oci-labels` after the engine name (`ce docker --no-oci-labels buildx build …`, like the prefix options), turns them off. The labels override a Dockerfile `LABEL` with the same key, and a `--label` you pass for a key wins over ce's label for it, an `--annotation` over its annotation. Labels and annotations are applied after the `RUN` steps, so `created` does not change their cache, but each build's image differs. For one that does not, set the epoch: `created` follows it as the engine does. With Docker, that is a `--build-arg SOURCE_DATE_EPOCH`, or else the environment's `SOURCE_DATE_EPOCH`, or else the default of a global `ARG SOURCE_DATE_EPOCH` before the first `FROM`, which ce reads from the Dockerfile. With podman and buildah, it is `--timestamp`, which they refuse beside any of the others, or else `--source-date-epoch`, or else the environment's `SOURCE_DATE_EPOCH`, or else the build arg. Each is read as the engine reads it: a whole number of seconds, which may be negative, and for `--timestamp` also in Go's other bases, such as `0x10` or `010` (8). An epoch ce cannot read, such as `1.5` or a `SOURCE_DATE_EPOCH` the environment gives that is not a number, leaves `created` out, as do a podman or buildah `--build-arg-file` with none of the others, as the file may set it, and with Docker a global default with a `$` or a Dockerfile ce cannot read, such as a remote context's.
|
|
297
|
+
|
|
298
|
+
podman and buildah expand a `$NAME` in a `--label`'s value from the build args, and make one that is not a build arg empty, with no way to escape the `$`. They also write a label's value as Go's `%q` does, so a line end becomes `\n`, as do a tab and other characters Go escapes. So with them, a label whose value has a `$`, such as a description that mentions `$HOME`, or a character they escape, is left out; its annotation, which they keep as it is, stays.
|
|
299
|
+
|
|
300
|
+
With Docker, a build for two or more platforms gets the annotations on the image index too (`manifest,index:`). BuildKit refuses index annotations for one platform, so a build for one gets them on its manifest only. `DOCKER_BUILDKIT=0` gets the labels only, as the legacy builder it picks has no `--annotation`; `docker buildx build` ignores it and always uses BuildKit. podman and buildah put them on the image's manifest, not on a `--manifest` list. With the `docker` image format they drop them, as that format has no annotations.
|
|
301
|
+
|
|
302
|
+
### SBOM
|
|
303
|
+
|
|
304
|
+
`SBOM` adds a software bill of materials to builds. `true` (or `spdx`) gives SPDX on every engine, and `cyclonedx` gives CycloneDX with podman and buildah. It is off when unset, empty or `false`; it also takes `yes`, `no`, `1` and `0`, in any case, and any other value exits 2.
|
|
305
|
+
|
|
306
|
+
- **Docker:** `--sbom=true`, BuildKit's SBOM attestation, unless you pass `--sbom` or an `--attest type=sbom`. [It is always SPDX](https://docs.docker.com/build/metadata/attestations/sbom/), so `SBOM=cyclonedx` exits 2 before the build, unless your own flag means ce adds none. With `--load`, Docker's classic image store refuses the attestation and says what to switch to: another driver, or the containerd image store.
|
|
307
|
+
- **podman and buildah:** `--sbom=syft-spdx`, or `--sbom=syft-cyclonedx` for `SBOM=cyclonedx`, unless you pass a `--sbom` preset. (buildah's plain `syft` preset is CycloneDX, so ce names the format.) The SBOM is written into the image as `/root/buildinfo/content_manifests/sbom.spdx.json`, or `sbom.cdx.json` for a CycloneDX preset. A `--sbom` you pass without an `--sbom-output`, `--sbom-image-output`, `--sbom-purl-output` or `--sbom-image-purl-output` is written there too, even without `SBOM`. The scanner runs from an image, which the engine pulls if it is missing.
|
|
308
|
+
|
|
309
|
+
## Registry login
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
ce docker login
|
|
313
|
+
ce podman login
|
|
314
|
+
ce buildah login
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A bare container-engine login uses the existing `REGISTRY_HOST`, `REGISTRY_USER`, `REGISTRY_PASSWORD` groups and Forgejo OIDC settings, but runs only the named engine. A failure does not fall back to another engine. `ce login` remains compatible with the original fallback order: for each registry, try Docker, then Podman, then Buildah until one succeeds. With no registries configured, both fail instead of doing nothing.
|
|
318
|
+
|
|
319
|
+
Native login arguments pass through, so `ce docker login registry.example --username alice` keeps Docker's normal behavior. To use native login without arguments, write `ce docker -- login`. `ce npm login` also uses npm's native login; publishing continues to use the selected client's authentication configuration.
|
|
320
|
+
|
|
321
|
+
`ce docker --dry-run login` validates configured targets and prints the login commands without sending passwords, fetching OIDC tokens or writing credentials. Scoped Docker OIDC login installs the Docker credential helper; scoped Podman and Buildah login do not modify Docker's helper configuration.
|
|
78
322
|
|
|
79
323
|
## The credential helper
|
|
80
324
|
|
|
@@ -84,7 +328,7 @@ The package installs `docker-credential-ce-forgejo` next to `ce`. `ce` puts that
|
|
|
84
328
|
|
|
85
329
|
## Compatibility
|
|
86
330
|
|
|
87
|
-
These
|
|
331
|
+
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:
|
|
88
332
|
|
|
89
333
|
- commands, flags and the environment variables `login` reads;
|
|
90
334
|
- the arguments the engine receives after tag and cache rewriting, and its exit code, signals and PID (`ce` replaces itself with the engine);
|
|
@@ -94,19 +338,39 @@ These stay the same from release to release, and [test/contract.test.ts](test/co
|
|
|
94
338
|
|
|
95
339
|
The wording of messages is not part of that and may change.
|
|
96
340
|
|
|
341
|
+
In 0.0.3, builds in a run get the OCI labels, and annotations, by default; `OCI_LABELS=false` or `--no-oci-labels` turns them off. 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. podman and buildah are no longer run through `gosu` as `$USER` when it is on `PATH`: ce runs every engine directly, as the user ce runs as, so to drop root, run ce itself as another user, e.g. with a `USER` line in the image.
|
|
342
|
+
|
|
97
343
|
Compared with the Python version:
|
|
98
344
|
|
|
99
345
|
- 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.
|
|
100
346
|
- Replacing the process with the engine uses `process.execve`. Node aborts rather than report an `execve` failure, so `ce` first checks that the engine is an executable file on `PATH`.
|
|
101
347
|
- Messages are worded differently in places, and the `Usage:` line shows the absolute path of the script.
|
|
348
|
+
- podman and buildah are not run through `gosu`, which the runner image's `ce` does when `gosu` is on `PATH`.
|
|
102
349
|
- `docker-credential-ce-forgejo` runs the helper code directly instead of `exec ce credential-helper`, so `ce` does not have to be on `PATH`.
|
|
103
350
|
- JSON files keep the same structure and sorted keys. Numbers are written as JavaScript writes them (`4102444800` rather than `4102444800.0`), and non-ASCII characters are not `\u`-escaped.
|
|
104
351
|
|
|
105
352
|
## Development
|
|
106
353
|
|
|
354
|
+
### Build CI and npm staging
|
|
355
|
+
|
|
356
|
+
[The Forgejo build workflow](.forgejo/workflows/build.yml) builds, type-checks and tests PRs, pushes to `main` and `dev`, version tags (`v*`), and manual runs. Every successful build uploads the packed npm tarball as an artifact.
|
|
357
|
+
|
|
358
|
+
The workflow builds the checkout and runs `node bin/ce` for package preparation and release uploads, so CI exercises the ce code under review. npm 12.2.1 provides the native packing and staging commands.
|
|
359
|
+
|
|
360
|
+
Every push to `main` is a release run: if the packed version is new, CI publishes that exact tarball to the repository owner's Forgejo npm registry with `ce npm --forgejo-oidc publish`, then stages it on npmjs with `ce npm --token-env=NODE_AUTH_TOKEN --stage publish`. Bumping the version in `package.json` is the release. Both steps pass `--tolerate-republish`, so a push that keeps the version skips each registry that already has it, published or staged. Ce infers a prerelease's channel from the packed version; stable versions keep npm's native default and version checks. On a tag build, ce checks the tag against the packed version before exposing artifact outputs. For a manual run, select **stage** to request both release uploads from the selected branch. PRs, `dev`, tags and manual runs without **stage** build, check and upload the package artifact.
|
|
361
|
+
|
|
362
|
+
The job enables Forgejo OIDC. Its public `AUD` identifies the Authorized Integration whose backend permissions must allow publication to this repository's package registry. The Forgejo step requires no static registry token. For npmjs, use a granular `NPM_TOKEN` with read/write access to `@izykitten/ce`; a [stage-only token](https://docs.npmjs.com/about-access-tokens#about-stage-only-tokens) keeps final publication dependent on maintainer approval. Ce configures the token and verifies authentication without interactive `npm login` or temporary credential files. `NPM_TOKEN` is available only to the npmjs staging step. Neither release step runs on PRs or pushes to other branches.
|
|
363
|
+
|
|
364
|
+
CI runs `npm stage publish`, which [uploads a pending version without publishing it](https://docs.npmjs.com/cli/v12/commands/npm-stage). Review and approve the staged version in npm's dashboard with your own authentication. Staged and published versions share the same version namespace, so while a version waits for approval, later pushes skip it. Rejecting it frees the version, and the next push to `main` stages it again from that commit. To release a different build while one is pending, bump `package.json` to an unused version. The workflow uses npm 12.2.1 for staging and never runs the approval command.
|
|
365
|
+
|
|
366
|
+
### Local checks
|
|
367
|
+
|
|
107
368
|
```sh
|
|
108
369
|
bun install
|
|
109
370
|
bun test # builds dist/, then unit tests plus the contract and process tests under Node and Bun
|
|
110
371
|
bun check
|
|
111
372
|
bun run build # dist/ce.js, which the bin scripts load; npm pack runs it
|
|
373
|
+
node bin/ce npm --stage publish # stage the current version for approval, with npm authentication configured
|
|
112
374
|
```
|
|
375
|
+
|
|
376
|
+
`bun run check-engines` builds each case in [scripts/engine-cases.ts](scripts/engine-cases.ts) with real Docker, podman and buildah, and compares the build arg each build gets with the one ce works out. It fails if ce would tag with a wrong value. podman and buildah run in a privileged container each, from `quay.io/podman/stable` and `quay.io/buildah/stable` unless `PODMAN_IMAGE` or `BUILDAH_IMAGE` names another, such as an older version's; `bun run check-engines docker` checks Docker alone. Run it after changing how ce reads a Containerfile.
|