fix(ci): prerender the web bundle after the api/worker deploy, not beside it
Some checks failed
deploy / Build api + worker (static musl) (push) Waiting to run
deploy / Build prerendered web (push) Has been cancelled
deploy / Deploy moments-api to nikola (push) Has been cancelled
deploy / Deploy moments-worker to frootmig (push) Has been cancelled
deploy / Deploy web to oolon (push) Has been cancelled

`build-web` had no `needs:`, so it ran in parallel with the binary build
and both deploy jobs. The prerender fetches VITE_API_BASE at build time,
which means the job baked whatever the api happened to be serving at the
moment it ran — old binary or new, depending on which runner finished
first.

Both outcomes showed up on consecutive runs. Run 68 won by ~25 seconds
and the repo-visibility fix reached the api and the crawler snapshot
together. Run 69 lost: the api served the new activity/summary private
aggregate while the baked /activity/ snapshot had none, so the 15 August
card read "47 changes in 3 repositories" where the api said 54 with 7
private. Browsers hydrate and refetch, so a visitor saw the right page —
but curl, which is what crawlers and AI screeners get, saw the stale one
until refresh.yml was dispatched by hand. Nothing in the pipeline noticed.

deploy-worker is in the needs list alongside deploy-api because the
worker owns migrations: a schema change isn't live until it has
restarted, so an api that depends on one isn't answering correctly before
then either.

The graph is now serial — binaries -> api/worker -> web — which costs the
overlap between the web build and the binary build. Worth it: the
alternative is a published snapshot whose correctness depends on runner
scheduling, failing silently and for a whole day.

Job graph verified acyclic with every `needs:` resolving to a real job.

Closes #8
This commit is contained in:
2026-08-17 12:58:17 +03:00
parent 12e9d4097f
commit ef8521b9fb
2 changed files with 20 additions and 0 deletions

View File

@@ -101,6 +101,19 @@ jobs:
build-web:
name: Build prerendered web
# The prerender fetches VITE_API_BASE at build time, so this job bakes
# whatever the api is serving when it runs. Without these dependencies it
# raced the api/worker deploys and the crawler snapshot silently disagreed
# with the live api for a whole day, until the nightly refresh: it won the
# race once (a visibility fix reached both together) and lost it once (an
# api field the snapshot then lacked). deploy-worker is in the list too,
# not just deploy-api — the worker owns migrations, so a schema change
# isn't applied until it has restarted.
#
# The cost is a serial deploy: binaries -> api/worker -> web. Worth it;
# the alternative is a snapshot whose correctness depends on which runner
# finished first.
needs: [deploy-api, deploy-worker]
runs-on: fedora-44
steps:
- uses: actions/checkout@v4

View File

@@ -119,6 +119,13 @@ CI-driven via **Gitea Actions** (`.gitea/workflows/`), the source of infra truth
prerendered web bundle, then deploy each component over SSH as the `gitea_ci`
user with scoped sudo (`asset/sudoers.d/`). Services run under systemd with
hardened units; the api/worker reach postgres over mTLS using the host cert.
The job graph is deliberately serial — binaries → api/worker → web — because
the prerender fetches `VITE_API_BASE` at build time and so bakes whatever the
api is serving when `build-web` runs. `build-web` therefore `needs:` both
deploy jobs (the worker owns migrations, so a schema change isn't live until
it restarts). When it ran in parallel instead, whether the crawler snapshot
matched the api came down to which runner finished first, and a lost race
meant a stale snapshot until the nightly refresh.
- `refresh.yml` — daily `schedule:` (+ manual): rebuilds and redeploys only the
web tier, re-baking the prerendered crawler snapshot from the current gist (CV)
and activity API without bouncing the api/worker.