2026-08-24 21:55:48 +03:00
|
|
|
|
# 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.
|
|
|
|
|
|
|
2026-08-24 22:25:35 +03:00
|
|
|
|
## 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.
|
|
|
|
|
|
|
2026-08-24 21:55:48 +03:00
|
|
|
|
`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.
|
|
|
|
|
|
|
2026-08-24 22:27:39 +03:00
|
|
|
|
`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.
|
2026-08-24 21:55:48 +03:00
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-08-24 22:27:39 +03:00
|
|
|
|
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`.
|
2026-08-24 21:55:48 +03:00
|
|
|
|
- **`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.
|
|
|
|
|
|
|
2026-08-24 22:27:39 +03:00
|
|
|
|
`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.
|
2026-08-24 21:55:48 +03:00
|
|
|
|
|
|
|
|
|
|
## 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.
|