ea583ed57d
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.
171 lines
7.1 KiB
Markdown
171 lines
7.1 KiB
Markdown
# 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.
|