Files
WeatherTool/docs/CONTINUOUS_INTEGRATION.md
T
b0txec ea583ed57d
CI / backend (push) Successful in 1m27s
CI / frontend (push) Successful in 52s
Correct two doc sections written before the real CI bugs were found
The changelog claimed ci.yml #1 verified green (it didn't -- that was a
wrong WebFetch summary of the Gitea Actions page, reported as fact without
checking it visually) and CONTINUOUS_INTEGRATION.md described the
HOME/cache permission warning as harmless (it wasn't -- it blocked
resolving actions/checkout entirely). Both corrected with what actually
happened: three real, different bugs, found only by running the workflow
and reading user-provided screenshots of the actual failures.
2026-08-24 22:27:39 +03:00

171 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Continuous integration
Last verified: 2026-08-24
## Current topology
WeatherTool uses Gitea Actions for continuous integration:
```text
bot/WeatherTool repository on gitea.packet.garden
→ repository-scoped weathertool-ci-rocky-01 runner on the coding PC
→ dedicated Docker-in-Docker daemon
→ disposable per-job containers (backend: hseeberger/scala-sbt, frontend: node:20-bookworm)
```
Gitea does not execute workflow commands itself. The runner polls Gitea for
jobs and executes them. The runner is registered only to `bot/WeatherTool`;
it is not a user-, organization-, or instance-wide runner.
The runner host is the Rocky Linux coding PC — the same machine already used
for all compile/build/test/staging work. This is a second, independent
runner instance on that host: HOP already runs its own Forgejo runner there
(`hop-forgejo-runner-*`, registered to a different Forgejo instance's
`bot/hop` repo). The two don't conflict — each is a separate long-polling
client registered to a different server with its own token, own Docker
network, and own Docker-in-Docker daemon; they only share host CPU/RAM,
which is a non-issue for occasional CI runs.
## Isolation boundary
Same pattern as HOP's Forgejo runner: the runner uses a dedicated
Docker-in-Docker daemon rather than exposing the coding PC's normal Docker
socket to workflow containers. The nested daemon:
- runs in its own privileged container because a nested Docker daemon
requires that capability;
- exposes no port on the host or LAN;
- is reachable only through the runner's private Compose network; and
- stores its images and state in the `weathertool-gitea-runner_dind_data`
volume.
This prevents ordinary WeatherTool workflow jobs from enumerating or
mutating the coding PC's normal development containers (including the
Rocky staging stack itself).
## Host-local runner files
The active runner is operated from:
```text
/home/sandbox/.local/share/weathertool-gitea-runner/
compose.yml
runner-token
```
The directory is mode `0700`. `runner-token` is mode `0600`. It is
deliberately outside the WeatherTool Git repository and was written directly
to disk from an interactive prompt — it was never printed, pasted into
chat, or copied into project documentation.
Normal operator commands are run from the directory above:
```bash
docker compose up -d
docker compose ps
docker compose logs --tail=100 runner
docker compose pull
```
The expected steady state is:
- `docker-in-docker`: `Up (healthy)`;
- `runner`: `Up`; and
- `weathertool-ci-rocky-01`: `Idle` in Gitea when no job is queued.
## Triggering CI
CI only runs on a push (or PR) reaching the `gitea` remote — pushing to
`rocky` alone does not trigger it, since Gitea has no visibility into that
private bare repo. To avoid needing two separate `git push` commands (and
forgetting one), a combined `all` remote pushes to both `rocky` and `gitea`
in one command:
```bash
git push all codex/staging-baseline
```
`all` deliberately does not include `origin` (the GitHub
`guntisdev/WeatherTool` repo) — see `DEVELOPMENT_AND_STAGING.md`'s commit
workflow section for why.
`Idle` is healthy. It means the runner is authenticated and polling for
work. The runner is available only while the coding PC, Docker, and these
containers are running.
`compose.yml` sets `HOME: /data` on the runner container. Without it, the
first real run failed every job, both times it was tried, with `Unable to
clone https://github.com/actions/checkout ...: mkdir /.cache: permission
denied` — this was initially misdiagnosed (both in the runner's own startup
log, which only calls it "cache server disabled", and in this doc's first
draft) as merely disabling the optional `actions/cache` action type. It
actually blocks resolving *any* remote action at all: with uid 1000 and no
writable `$HOME`, the runner has nowhere to git-clone an action's source
into before running it, and `actions/checkout` is exactly that. `HOME=/data`
(the already-writable bind-mounted data directory) fixed it.
## WeatherTool workflow
The version-controlled workflow is:
```text
.gitea/workflows/ci.yml
```
It runs for:
- pushes to `codex/staging-baseline`;
- pull requests targeting `codex/staging-baseline`; and
- manual `workflow_dispatch` requests.
Two independent jobs, each with its own container image rather than one
shared runner-label image with ad-hoc installs — this reuses images already
proven for this project rather than bootstrapping a second language runtime
into a single shared container:
- **`backend`** (`hseeberger/scala-sbt:17.0.2_1.6.2_2.13.8`, the same image
used all session for local Rocky builds): installs a real Node 20 via
NodeSource's setup script (see below for why), checkout, `sbt -batch
test`.
- **`frontend`** (`node:20-bookworm`): checkout, `npm ci`, `npm run
typecheck`, `npm run build`, `npm audit`, `npm audit --omit=dev` — the
same five checks already documented as the manual Rocky verification
routine in `DEVELOPMENT_AND_STAGING.md`.
Verified before writing the workflow that `sbt test` compiles and passes
with no `.env` file present at all (via a `git archive HEAD` dry run into a
clean scratch directory) — matching exactly what a checkout-only CI job
actually has, since real credentials must never reach CI. The sbt-dotenv
plugin logs a graceful warning and continues; nothing in the current test
suite (`FileNameServiceSpec`; `ParserSpec` has no live assertions) touches
`sys.env` or a live database.
`actions/checkout` is a JavaScript action — it needs a Node runtime inside
the job container to execute at all, which `hseeberger/scala-sbt` doesn't
have. `frontend`'s image already has one, so it passed as soon as the
`HOME` fix landed. `backend` needed an explicit install step first
(`apt-get install nodejs` alone wasn't enough either: `hseeberger/scala-sbt`
is Debian **bullseye**-based, whose default apt `nodejs` package is a stale
Node 12, too old to parse `actions/checkout@v4`'s modern JS —
`SyntaxError: Unexpected token '{'` on a class static block. NodeSource's
`setup_20.x` script installs an actual current Node 20 regardless of the
distro's packaged version).
None of the three bugs above were caught by writing the workflow carefully
or by the pre-write `sbt test` dry run — all three only surfaced by actually
running it and reading the real failure in the Gitea Actions UI, once per
bug, in order: the `HOME`/cache fix, then the missing-Node fix, then the
wrong-Node-version fix. The first four runs (`ci.yml #1``#4`) all failed.
The first complete green run was `ci.yml #5`, commit `2c78ae8`, 2m24s,
verified 2026-08-24.
## Current boundary
This workflow is CI only. A green result proves the committed source
compiles, passes its (currently minimal) test suite, typechecks, builds,
and has no known frontend dependency vulnerabilities. It does not deploy to
Rocky staging or the VPS, publish an image, or access any staging/production
secret — the runner's job containers never see `.env`, `.env.staging`, or
any real provider credential. Continuous delivery, and expanding actual test
coverage (see the roadmap's Phase 2), remain separate, not yet started work.