Files
tiny-pattern-ts/development/ci.md
T
tmu dc7ce22fc5 📝 Add development/ docs for decisions and known issues
Category files under development/ replace the decision prose that was
scattered through README.md and CONTRIBUTING.md. Each decision is a block with
Decision (YYYY-MM) / Why / Rejected / Known issue; the new library.md records
the public API contract and its limitations. AGENTS.md and backlog.tasks now
point at the new home.
2026-09-15 13:26:59 +00:00

147 lines
5.4 KiB
Markdown

# 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.
## 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.
- **`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.
## 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.
#### 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.
#### 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
(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.
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>`.
3. Repoint the three `container.image` tags in
[.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml) to the same 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:
### The `x64.complete` marker
#### Decision (2026-09)
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).
### Tag freshness, with force-pull deliberately off
#### Decision (2026-09)
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.
#### Rejected
- Enabling `force_pull`: it is acceptable to miss a runner-side image change,
and a `Dockerfile`-only change is not worth a per-job pull.
#### 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.
## Coverage serving
#### Decision (2026-09)
Serve CI coverage from a shared directory on the runner, with no deploy step in
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.
- Coverage is written to a shared volume keyed by project and tag (for example
`/docs/tiny-pattern-ts/<tag>/`).
#### Rejected
- Gitea Pages and Codecov: neither was confirmed available or wanted.
#### Known issue
- Coverage is currently served for tag pushes only. Serving it for non-tag
pushes (for example `main/coverage`) is 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.
#### Decision (2026-09)
Precompress text assets into sidecars rather than compressing on each 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.