♻️ 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:
1 parent
7ea84b66fa
commit
74c39e1346
7 files changed
+341
-409
No files matched your search
+47
-57
@@ -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.
|
||||
Reference in new issue
Block a user