diff --git a/README.md b/README.md index f29dbc3..e2bc9f4 100644 --- a/README.md +++ b/README.md @@ -1368,6 +1368,23 @@ Publishing is a separate step from building, and the separation is the point: th the site server reads is only touched once a build has succeeded, so a failed build leaves the previous site up rather than replacing it with a half-written one. +**Two gates before the build starts.** The first waits for `/api/health` to answer 200, +because `After=` orders a start and does not wait for a port. The second counts +`/api/mints` and refuses to build below `MIN_MINTS_FOR_BUILD`, default 20. + +The second exists because health answering 200 and the index being complete are +different claims. A year of ~31-event backfills left a perfectly healthy API serving a +real, correct, complete list of eight mints; a build against that succeeds, prerenders +eight cards, and `rsync --delete-after` replaces fifty-five with eight. The gate runs as +an `ExecStartPre`, so a refusal aborts the unit before `pnpm build` — and publishing is +`ExecStartPost`, after the build — which means the previously published site is never +touched. The `OnFailure=` alert says why. + +```bash +# Raise or lower it for this host without editing the unit: +sudo systemctl edit cashumints-web # [Service] / Environment=MIN_MINTS_FOR_BUILD=40 +``` + ```ini # /etc/systemd/system/cashumints-web.service [Unit] @@ -1401,6 +1418,40 @@ Environment=PUBLIC_API_URL= # rather than letting the first fetch die on ECONNREFUSED. /api/health answers 503 until # it is genuinely ready, and curl -f treats that as a failure, so the loop keeps waiting. ExecStartPre=/usr/bin/timeout 90 /bin/sh -c 'until curl -sf -o /dev/null http://127.0.0.1:8788/api/health; do sleep 1; done' + +# Then: does the API actually have an index to build a site out of? +# +# Health answering 200 says the process is up and its last backfill read something. It +# does not say how many mints are in the table, and those are different questions — the +# year of ~31-event backfills had a healthy API serving a real, complete, correct list of +# eight mints. A build against that succeeds, prerenders eight cards, and rsync happily +# replaces fifty-five with eight. +# +# So count the list before spending twenty minutes building from it. Below the floor +# this exits non-zero, systemd abandons the unit at ExecStartPre, and — because publishing +# is ExecStartPost, after the build — the previously published site is never touched. The +# site stays exactly as it was and the OnFailure alert says why. +# +# Counted by the `"host":` key, one per item, rather than by counting `{`: the list +# payload carries a nested object per mint (its NUT capability switches), so brace +# counting would report roughly double. No jq: it is not installed on this host and a +# build gate should not add a dependency to run. +# +# `Q` is a double-quote character, built with printf rather than written literally, +# because this whole command is already inside systemd's single quotes and a quote of +# either kind in the grep pattern would end the argument early. +# +# A curl that fails for any reason leaves `n` empty, `$${n:-0}` reads that as zero, and +# zero is below every floor — so an API that fell over between the health check above and +# this line refuses the build rather than sailing through it. +Environment=MIN_MINTS_FOR_BUILD=20 +ExecStartPre=/bin/sh -c 'Q=$$(printf "\\042"); \ + n=$$(curl -sf --max-time 30 http://127.0.0.1:8788/api/mints | grep -o "$${Q}host$${Q}:" | wc -l); \ + if [ "$${n:-0}" -lt "$$MIN_MINTS_FOR_BUILD" ]; then \ + printf "<3>%s\\n" "refusing to build: /api/mints returned $${n:-0} mints, floor is $$MIN_MINTS_FOR_BUILD. Previous site left untouched."; \ + exit 1; \ + fi; \ + printf "%s\\n" "build gate: $$n mints, floor $$MIN_MINTS_FOR_BUILD"' # Check `which pnpm` on the host: a corepack or pnpm-home install sits outside /usr/bin, # and systemd's PATH does not include it. ExecStart=/usr/bin/pnpm build diff --git a/deploy/cashumints-web.service b/deploy/cashumints-web.service index efe9020..627a79b 100644 --- a/deploy/cashumints-web.service +++ b/deploy/cashumints-web.service @@ -52,6 +52,40 @@ Environment=PUBLIC_API_URL= # rather than letting the first fetch die on ECONNREFUSED. /api/health answers 503 until # it is genuinely ready, and curl -f treats that as a failure, so the loop keeps waiting. ExecStartPre=/usr/bin/timeout 90 /bin/sh -c 'until curl -sf -o /dev/null http://127.0.0.1:8788/api/health; do sleep 1; done' + +# Then: does the API actually have an index to build a site out of? +# +# Health answering 200 says the process is up and its last backfill read something. It +# does not say how many mints are in the table, and those are different questions — the +# year of ~31-event backfills had a healthy API serving a real, complete, correct list of +# eight mints. A build against that succeeds, prerenders eight cards, and rsync happily +# replaces fifty-five with eight. +# +# So count the list before spending twenty minutes building from it. Below the floor +# this exits non-zero, systemd abandons the unit at ExecStartPre, and — because publishing +# is ExecStartPost, after the build — the previously published site is never touched. The +# site stays exactly as it was and the OnFailure alert says why. +# +# Counted by the `"host":` key, one per item, rather than by counting `{`: the list +# payload carries a nested object per mint (its NUT capability switches), so brace +# counting would report roughly double. No jq: it is not installed on this host and a +# build gate should not add a dependency to run. +# +# `Q` is a double-quote character, built with printf rather than written literally, +# because this whole command is already inside systemd's single quotes and a quote of +# either kind in the grep pattern would end the argument early. +# +# A curl that fails for any reason leaves `n` empty, `$${n:-0}` reads that as zero, and +# zero is below every floor — so an API that fell over between the health check above and +# this line refuses the build rather than sailing through it. +Environment=MIN_MINTS_FOR_BUILD=20 +ExecStartPre=/bin/sh -c 'Q=$$(printf "\\042"); \ + n=$$(curl -sf --max-time 30 http://127.0.0.1:8788/api/mints | grep -o "$${Q}host$${Q}:" | wc -l); \ + if [ "$${n:-0}" -lt "$$MIN_MINTS_FOR_BUILD" ]; then \ + printf "<3>%s\\n" "refusing to build: /api/mints returned $${n:-0} mints, floor is $$MIN_MINTS_FOR_BUILD. Previous site left untouched."; \ + exit 1; \ + fi; \ + printf "%s\\n" "build gate: $$n mints, floor $$MIN_MINTS_FOR_BUILD"' # Check `which pnpm` on the host: a corepack or pnpm-home install sits outside /usr/bin, # and systemd's PATH does not include it. ExecStart=/usr/bin/pnpm build