@izykitten/ce 0.0.3-dev.7 → 0.0.3-dev.9

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.
Files changed (3) hide show
  1. package/README.md +137 -9
  2. package/dist/ce.js +2948 -2296
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -1,18 +1,28 @@
1
1
  # @izykitten/ce
2
2
 
3
- Container engine wrapper for CI: registry login (including Forgejo OIDC), tag and cache rewriting for `docker`, `podman` and `buildah`, and a Docker credential helper that refreshes the Forgejo token.
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,7 +32,7 @@ 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
37
  run: >
28
38
  bunx @izykitten/ce docker buildx build --push
@@ -31,7 +41,49 @@ In a Forgejo workflow:
31
41
 
32
42
  [Recommended tags](#recommended-tags) says which tags that gives, and how to pick what `latest` follows.
33
43
 
34
- Only 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`. Everything else, such as `docker run -t`, reaches the engine unchanged.
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
+ 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.
56
+
57
+ For npm token authentication, name the environment variable that holds the token and pass an explicit native registry:
58
+
59
+ ```sh
60
+ ce npm --token-env=NODE_AUTH_TOKEN --stage publish ./package.tgz \
61
+ --registry=https://registry.npmjs.org/
62
+ ```
63
+
64
+ 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.
65
+
66
+ Forgejo's integrated npm registry can use the same Actions OIDC exchange as Docker login:
67
+
68
+ ```yaml
69
+ enable-openid-connect: true
70
+ env:
71
+ AUD: your-authorized-integration-audience
72
+ steps:
73
+ - run: >-
74
+ ce npm --forgejo-oidc publish ./package.tgz
75
+ --registry=${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/npm/
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. 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. With npm as the backend, `npm_config_tag`/`NPM_CONFIG_TAG` also take precedence; Bun does not read those tag variables. Use `ce npm --no-auto-tag publish ...` to leave all tag selection to native configuration, including a `tag` in `.npmrc`. The ce `--stage` and `--auto-tag` 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
+ 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.
85
+
86
+ 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.
35
87
 
36
88
  ## Tags from the Forgejo run
37
89
 
@@ -51,12 +103,27 @@ Tags and the `TAG_PREFIX`/`CACHE_PREFIX` values can use placeholders, filled fro
51
103
 
52
104
  `{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.
53
105
 
106
+ Guards can also test absence, compare a value, or accept alternatives:
107
+
108
+ | Guard | Keeps the output when |
109
+ | ----- | --------------------- |
110
+ | `{!pr}` | this is not a pull request |
111
+ | `{?env.PUBLISH_DOCKERHUB=true}` | `PUBLISH_DOCKERHUB` is exactly `true` |
112
+ | `{!env.CHANNEL=internal}` | `CHANNEL` is absent or differs from `internal` |
113
+ | `{?branch\|tag}` | this is a branch or Git-tag build |
114
+ | `{!branch\|tag}` | neither `branch` nor `tag` has a value |
115
+ | `{?default\|env.PUBLISH=true}` | this is the default branch or `PUBLISH` is exactly `true` |
116
+
117
+ 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.
118
+
54
119
  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.
55
120
 
56
- 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.
121
+ 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.
57
122
 
58
123
  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.
59
124
 
125
+ 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.
126
+
60
127
  Quote a template in a workflow's `env:`: YAML reads `CACHE_PREFIX: {ref}` as a mapping, and `{?pr}pr-{pr}` not at all.
61
128
 
62
129
  A branch, PR and default-branch build, for an image deployed from its default branch with no releases yet, so `latest` follows that branch:
@@ -71,12 +138,46 @@ A branch, PR and default-branch build, for an image deployed from its default br
71
138
  -t '{image}:{ref},{?default}latest,{?default}caddy-{env.CADDY_VERSION}' .
72
139
  ```
73
140
 
74
- 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.
141
+ 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.
75
142
 
76
143
  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`.
77
144
 
78
145
  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.
79
146
 
147
+ ### Conditional destinations and shared tag lists
148
+
149
+ 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:
150
+
151
+ ```yaml
152
+ run: |
153
+ npx -y @izykitten/ce docker buildx build --push \
154
+ --cache type=gha,mode=max \
155
+ -t '{image}:{ref},{?default}sysbox' \
156
+ -t '{!pr}docker.io/izykitten/forgejo-runner:{ref},{?default}sysbox' \
157
+ -f Dockerfile .
158
+ ```
159
+
160
+ 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`.
161
+
162
+ To share a tag list across destinations, define it once with `--tag-list NAME=TEMPLATES` immediately after the engine name and use `{tags.NAME}`:
163
+
164
+ ```yaml
165
+ run: |
166
+ npx -y @izykitten/ce docker \
167
+ --tag-list 'runner={ref},{?default}sysbox' \
168
+ buildx build --push \
169
+ --cache type=gha,mode=max \
170
+ -t '{image}:{tags.runner}' \
171
+ -t '{!pr}docker.io/izykitten/forgejo-runner:{tags.runner}' \
172
+ -f Dockerfile .
173
+ ```
174
+
175
+ 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`.
176
+
177
+ 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.
178
+
179
+ 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.
180
+
80
181
  ### Recommended tags
81
182
 
82
183
  For an image others pull by version:
@@ -159,11 +260,11 @@ Two places always need a `--build-arg NAME`: a `{arg.NAME}` in the cache prefix
159
260
 
160
261
  ### Checking a template
161
262
 
162
- `--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. 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:
263
+ `--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:
163
264
 
164
265
  ```sh
165
266
  GITHUB_REF=refs/tags/v1.2.3 GITHUB_SERVER_URL=https://code.example GITHUB_REPOSITORY=org/app \
166
- ce --dry-run docker buildx build -t '{image}:{ref},{?stable}{major}.{minor}' .
267
+ ce docker --dry-run buildx build -t '{image}:{ref},{?stable}{major}.{minor}' .
167
268
  ```
168
269
 
169
270
  ### OCI labels
@@ -180,7 +281,7 @@ Builds in a run get these `org.opencontainers.image.` labels, and the same keys
180
281
  | `description` | the repository's description from the event |
181
282
  | `created` | when ce started the build, to the second, or the time `SOURCE_DATE_EPOCH` gives |
182
283
 
183
- Keys the run cannot tell are left out, and outside a run there are none, `created` included. `OCI_LABELS=false`, or `--no-oci-labels` before the engine (`ce --no-oci-labels docker 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.
284
+ 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.
184
285
 
185
286
  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.
186
287
 
@@ -193,6 +294,20 @@ With Docker, a build for two or more platforms gets the annotations on the image
193
294
  - **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.
194
295
  - **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.
195
296
 
297
+ ## Registry login
298
+
299
+ ```sh
300
+ ce docker login
301
+ ce podman login
302
+ ce buildah login
303
+ ```
304
+
305
+ 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. No configured registries remains a successful no-op.
306
+
307
+ 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.
308
+
309
+ `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.
310
+
196
311
  ## The credential helper
197
312
 
198
313
  With `AUD` set (and `enable-openid-connect: true` on the job), `ce login` logs in to the Forgejo registry with a short-lived OIDC token. It then writes `"credHelpers": {"<host>": "ce-forgejo"}` to the Docker config. From then on, Docker gets credentials for that host by running `docker-credential-ce-forgejo get`, which returns the saved token or fetches a fresh one when it is within five minutes of expiry. That is what lets a long build still push.
@@ -224,11 +339,24 @@ Compared with the Python version:
224
339
 
225
340
  ## Development
226
341
 
342
+ ### Build CI and npm staging
343
+
344
+ [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.
345
+
346
+ Version-tag pushes publish that exact tarball to the repository owner's Forgejo npm registry with `ce npm --forgejo-oidc publish`, then stage it on npmjs with `ce npm --token-env=NODE_AUTH_TOKEN --stage publish`. The Git tag must match `v` followed by the version in `package.json`. Ce infers a prerelease's channel from the packed version; stable versions keep npm's native default and version checks. For a manual run, select **stage** to request both release uploads and optionally set **npm_tag** to override the dist-tag. Manual runs without **stage** build, check and upload the package artifact.
347
+
348
+ 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 ordinary branch pushes.
349
+
350
+ 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: bump `package.json` to an unused version before staging, and use the matching `v...` Git tag for an automatic release run. The workflow uses npm 12.2.1 for staging and never runs the approval command.
351
+
352
+ ### Local checks
353
+
227
354
  ```sh
228
355
  bun install
229
356
  bun test # builds dist/, then unit tests plus the contract and process tests under Node and Bun
230
357
  bun check
231
358
  bun run build # dist/ce.js, which the bin scripts load; npm pack runs it
359
+ npm run stage -- --tag dev # stage the current version for approval, with npm authentication configured
232
360
  ```
233
361
 
234
362
  `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.