♻️ Tighten the development/ prose

Same decisions, rationale, rejected alternatives and known issues, said with
less padding: ~5,530 -> ~4,730 words (-15%). Every fact from the first draft is
kept; only the wording, duplicated lead-ins and restated context are cut.
This commit is contained in:
tmu committed 2026-09-15 13:56:49 +00:00
1 parent 7ea84b66fa
commit 74c39e1346
7 files changed
+341 -409

No files matched your search

+47 -57
View File
@@ -1,70 +1,63 @@
# CI
The pipeline definition is [.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml);
that file, not this prose, is the source of truth for the job graph. This file
records why the pipeline and its runner image are shaped the way they are.
[.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml) is the source of truth for
the job graph; this file records why it is shaped the way it is.
## Pipeline
- **`build`** (push to `main` / tag) — build + correctness + packaging.
- **`maintain`** (push to `main`, non-blocking) — `npm run maintain`; it reports
and never fails the build.
- **`maintain`** (push to `main`, non-blocking) — `npm run maintain`; reports,
never fails the build.
- **`publish`** (tag) — packaging checks + `publish:publint` / `publish:attw`,
then the Gitea release page and `npm publish`. See
[publishing.md](./publishing.md).
- **`release-gate`** — recognizes a `:rocket: Release x.y.z` commit and skips the
`build`/`maintain` jobs, because `create:release` pushes the tag for the exact
same commit right after and the tag run is authoritative. It uses no Node and
stays on the runner's default image.
then the Gitea release page and `npm publish` (see
[publishing.md](./publishing.md)).
- **`release-gate`** — on a `:rocket: Release x.y.z` commit it skips
`build`/`maintain`, because `create:release` pushes the tag for the same commit
right after and the tag run is authoritative. It uses no Node and stays on the
default image.
## Runner image
#### Decision (2026-09)
The `build` / `maintain` / `publish` jobs run in
`gitea.e1nsnull.de/tmu/act-ci:<version>`
([docker/Dockerfile](../docker/Dockerfile)) — the runner's default act image
with the Node distribution overlaid at the exact `/opt/hostedtoolcache` layout
`actions/setup-node` probes before downloading.
`build` / `maintain` / `publish` run in `gitea.e1nsnull.de/tmu/act-ci:<version>`
([docker/Dockerfile](../docker/Dockerfile)): the default act image with Node
overlaid at the exact `/opt/hostedtoolcache` layout `actions/setup-node` probes.
#### Why
- No job pays the ~50 MB Node fetch, because the probe hits the baked entry.
- The image tag must equal the exact version pinned in
[.node-version](../.node-version), and the image is rebuilt only as part of a
Node bump — there is no other trigger.
- Building requires a docker daemon and registry credentials, so it belongs to
no feedback tier. This is why it is deliberately **not** an `npm run` script:
per the [script prefix convention](./workflow.md#script-prefix-convention), no
existing prefix fits and that is the signal.
- The tag must equal the exact [.node-version](../.node-version) pin, and the
image is rebuilt only as part of a Node bump — there is no other trigger.
- Building needs a docker daemon and registry credentials, so it belongs to no
feedback tier. That is why it is **not** an `npm run` script: no
[prefix](./workflow.md#script-prefix-convention) fits, and that is the signal.
#### Rejected
- Downloading Node in every job: the ~50 MB fetch per job was the original
problem.
- Gitea Pages / Codecov for coverage: neither was confirmed available or wanted
- Downloading Node in every job — the ~50 MB fetch was the original problem.
- Gitea Pages / Codecov for coverage — neither was confirmed available or wanted
(see [Coverage serving](#coverage-serving)).
## Bumping Node
Bumping Node is one coordinated change, committed as a unit:
1. Edit [.node-version](../.node-version) to the new exact `x.y.z` — floats like
`26` resolve to the latest patch at runtime and silently bust the baked
entry; [scripts/runner-image.sh](../scripts/runner-image.sh) refuses them.
1. Edit [.node-version](../.node-version) to the exact `x.y.z` — floats like `26`
resolve to the latest patch at runtime and bust the baked entry, so
[scripts/runner-image.sh](../scripts/runner-image.sh) refuses them.
2. `docker login gitea.e1nsnull.de` (user + package/access token), then
`./scripts/runner-image.sh --push` — it reads the version from `.node-version`
and builds/pushes `<IMAGE_REPO>:<version>`.
`./scripts/runner-image.sh --push`, which reads the version and pushes
`<IMAGE_REPO>:<version>`.
3. Repoint the three `container.image` tags in
[.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml) to the same version.
[.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml) to that version.
Skipping step 2 fails CI at image pull; skipping step 3 silently reverts to the
per-job download.
## Image invariants
Two invariants the image must satisfy for the `setup-node` probe to hit, both
easy to break:
For the `setup-node` probe to hit, two things must hold — both easy to break:
### The `x64.complete` marker
@@ -75,10 +68,9 @@ Bake a `<version>/<arch>.complete` marker next to the Node directory.
#### Why
- `actions/tool-cache` accepts a cached tool only when
`<version>/<arch>.complete` exists next to the directory (`tc.find()` checks
it). A plausible-looking `node/<version>/x64/` alone is ignored and the
download happens anyway. See the comment in
[docker/Dockerfile](../docker/Dockerfile).
`<version>/<arch>.complete` exists beside it (`tc.find()` checks). A bare
`node/<version>/x64/` is ignored and the download happens anyway. See the
comment in [docker/Dockerfile](../docker/Dockerfile).
### Tag freshness, with force-pull deliberately off
@@ -89,9 +81,8 @@ Leave `act_runner`'s `force_pull` disabled.
#### Why
- The tag encodes only the Node version, and the image is rebuilt only when that
version changes — so the normal flow always yields a new tag and the runner
pulls it.
- Forcing a pull would re-pull the image on every job for no benefit.
changes — so the normal flow always yields a new tag and the runner pulls it.
- Forcing a pull re-pulls the image on every job for no benefit.
#### Rejected
@@ -101,10 +92,10 @@ Leave `act_runner`'s `force_pull` disabled.
#### Known issue
- A `Dockerfile`-only change (like the marker above) re-pushed under an
unchanged tag is invisible to the runner — it keeps the old image while the
registry shows the new digest. If that ever matters, remove the stale tag on
the runner host (`docker rmi gitea.e1nsnull.de/tmu/act-ci:<version>`); do not
reach for force-pull.
unchanged tag is invisible to the runner, which keeps the old image while the
registry shows the new digest. Remove the stale tag on the runner host
(`docker rmi gitea.e1nsnull.de/tmu/act-ci:<version>`); do not reach for
force-pull.
## Coverage serving
@@ -115,9 +106,8 @@ CI.
#### Why
- The webserver just exposes the shared directory; the Gitea docker setup reuses
the existing reverse proxy. There is no upload artifact and no external
service.
- The webserver exposes the shared directory and the Gitea docker setup reuses
the existing reverse proxy — no upload artifact, no external service.
- Coverage is written to a shared volume keyed by project and tag (for example
`/docs/tiny-pattern-ts/<tag>/`).
@@ -127,20 +117,20 @@ CI.
#### Known issue
- Coverage is currently served for tag pushes only. Serving it for non-tag
pushes (for example `main/coverage`) is tracked separately.
- Coverage is served for tag pushes only; non-tag pushes (for example
`main/coverage`) are tracked separately.
[scripts/precompress.ts](../scripts/precompress.ts) emits `.br` / `.gz` / `.zst`
sidecars next to every text asset under the served directories. The Gitea pages
service (`static-web-server` with `SERVER_COMPRESSION_STATIC=true`) serves the
sidecar matching `Accept-Encoding` and falls back to the original for the rest.
sidecars next to text assets. The Gitea pages service (`static-web-server` with
`SERVER_COMPRESSION_STATIC=true`) serves the sidecar matching `Accept-Encoding`
and falls back to the original.
#### Decision (2026-09)
Precompress text assets into sidecars rather than compressing on each request.
Precompress into sidecars rather than per request.
#### Why
- The files are static and change only on deploy, so the work is paid once.
- Images, fonts and archives are already compressed; a sidecar would only make
them bigger, which is why only text extensions are emitted.
- The assets are static and change only on deploy, so the work is paid once.
- Images, fonts and archives are already compressed; a sidecar would only grow
them, so only text extensions are emitted.