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

5.4 KiB

CI

The pipeline definition is .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.
  • 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) — 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, 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, 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).

Bumping Node

Bumping Node is one coordinated change, committed as a unit:

  1. Edit .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 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 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.

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 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.