Adds CONTINUOUS_INTEGRATION.md (mirrors HOP's CI doc structure), links it from README, and records the CI changelog entry plus the follow-up review's open findings (scheduler crash-loop, System.gc(), connection pooling, the PostgresService field-routing invariant, and the mislabeled main-method "tests") in the roadmap's Phase 2/Phase 5 checklists.
5.1 KiB
Continuous integration
Last verified: 2026-08-24
Current topology
WeatherTool uses Gitea Actions for continuous integration:
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_datavolume.
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:
/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:
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; andweathertool-ci-rocky-01:Idlein Gitea when no job is queued.
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.
One known, non-fatal warning in the runner logs: cannot init cache server, it will be disabled: mkdir /.cache: permission denied — the runner
container runs as uid 1000 with no writable $HOME, so the actions/cache
action type is unavailable. Nothing in the current workflow uses it. Fixable
later by setting HOME=/data in the runner's environment if caching
becomes worth adding.
WeatherTool workflow
The version-controlled workflow is:
.gitea/workflows/ci.yml
It runs for:
- pushes to
codex/staging-baseline; - pull requests targeting
codex/staging-baseline; and - manual
workflow_dispatchrequests.
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): 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 inDEVELOPMENT_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.
The first complete green run was verified on 2026-08-24 for commit
5b88e69 (ci.yml #1, 33s).
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.