Files
moments/.gitea/workflows/refresh.yml
rob thijssen bb2f5b1f9b
Some checks failed
deploy / Build prerendered web (push) Successful in 7m14s
deploy / Deploy web to oolon (push) Successful in 18s
deploy / Build api + worker (static musl) (push) Successful in 5m41s
deploy / Deploy moments-worker to frootmig (push) Successful in 19s
deploy / Deploy moments-api to nikola (push) Successful in 24s
refresh / Rebuild prerendered web (push) Successful in 7m15s
refresh / Deploy refreshed web to oolon (push) Failing after 26s
fix(web): stop binding 443 behind the edge SNI router; verify the reload
oolon's TCP 443 belongs to the stream SNI router, which ssl_prereads the
handshake and forwards to the local https tier on 127.0.0.1:14443 with
PROXY protocol. site.conf.tmpl predates that and still bound 443 itself,
so every deploy and every daily refresh rsynced a vhost that collides
with the router.

Nothing in the pipeline caught it. `nginx -t` only detects duplicate
listeners within a context, not across http{} and stream{}, and
`systemctl reload` merely sends SIGHUP, so it exits 0 while nginx logs
"bind() to 0.0.0.0:443 failed (98: Address already in use) ... still
could not bind()", aborts the reconfiguration and keeps its old cycle.
The deploy went green while oolon's running config was frozen. It stayed
frozen for a day, stranding every cert the step@ timers renewed on disk
until eleven internal vhosts were serving expired certs. A cold start
would have failed outright, taking the whole public edge down.

Template the listen line from WEB_LISTEN (manifest web.config.listen for
script/deploy.sh, which renders the same template), and assert that the
reload landed by requiring a fresh worker generation, dumping the nginx
error log when it did not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182wzZE8DguMPWhxD21gfP2
2026-07-21 11:42:39 +03:00

117 lines
4.6 KiB
YAML

name: refresh
# Daily re-bake of the prerendered site. The crawler-visible HTML is a static
# snapshot taken at build time; this job rebuilds it from the current gist (CV)
# and activity API and redeploys *only* the web tier — so edits propagate to
# what crawlers and AI screeners see without a code push and without bouncing
# the api/worker. Humans already get live data via post-hydration refetch.
#
# Uses the same gitea_ci + scoped-sudo path as deploy.yml's deploy-web job;
# see asset/sudoers.d/web-host.conf and script/infra-setup.sh.
on:
schedule:
# 04:17 UTC daily — off-peak, arbitrary minute to avoid the top-of-hour herd.
- cron: '17 4 * * *'
workflow_dispatch:
concurrency:
group: deploy # share the deploy lock so a refresh never races a push deploy
cancel-in-progress: false
env:
WEB_HOST: oolon.kosherinata.internal
SERVER_NAME: rob.tn
WEB_ROOT: /var/www/rob.tn
API_PORT: "42424"
API_UPSTREAM_SCHEME: http
API_UPSTREAM_ADDR: nikola.kosherinata.internal:42424
# Internal mesh API base (the runner can't reach the public rob.tn); see
# deploy.yml. The api serves its routes under /v1.
VITE_API_BASE: http://nikola.kosherinata.internal:42424/v1
DEPLOY_KEY: |
${{ secrets.RSYNC_SSH_KEY }}
jobs:
build-web:
name: Rebuild prerendered web
runs-on: fedora-44 # image bakes in node + pnpm; that's all we need here
steps:
- uses: actions/checkout@v4
# Install without the pnpm 10 build-script gate, then rebuild the native
# deps vite needs (see deploy.yml build-web for the rationale).
- name: Build web (vite client + prerender)
working-directory: ui
run: |
pnpm install --frozen-lockfile --ignore-scripts
pnpm rebuild @swc/core esbuild
pnpm run build
- uses: actions/upload-artifact@v3
with: { name: web-dist, path: ui/dist, retention-days: 1 }
deploy-web:
name: Deploy refreshed web to oolon
needs: build-web
runs-on: fedora-44
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with: { name: web-dist, path: dist }
- name: SSH init
run: |
mkdir -p ~/.ssh
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
gitea_ci@"${WEB_HOST}" 'hostname -f'
- name: Render nginx vhost
run: |
mkdir -p rendered
python3 - <<'PY'
import os
t = open("asset/nginx/site.conf.tmpl").read()
for k in ("SERVER_NAME", "API_UPSTREAM_SCHEME", "API_UPSTREAM_ADDR"):
t = t.replace("{{%s}}" % k, os.environ[k])
t = t.replace("{{DOCROOT}}", os.environ["WEB_ROOT"])
open("rendered/site.conf", "w").write(t)
PY
- name: Sync static site (prerendered)
run: |
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/bin/install -d -m 0755 '"${WEB_ROOT}"
rsync -az --delete --mkpath --rsync-path='sudo rsync' \
--chown=root:root --chmod=D755,F644 \
dist/ gitea_ci@"${WEB_HOST}":"${WEB_ROOT}/"
ssh gitea_ci@"${WEB_HOST}" 'sudo /usr/sbin/restorecon -R '"${WEB_ROOT}"
- name: Sync nginx vhost + reload
run: |
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
rendered/site.conf \
gitea_ci@"${WEB_HOST}":/etc/nginx/conf.d/"${SERVER_NAME}".conf
ssh gitea_ci@"${WEB_HOST}" '
set -euo pipefail
sudo /usr/sbin/setsebool -P httpd_can_network_connect on
if ! sudo /usr/sbin/semanage port -l | grep -E "^http_port_t" | grep -qw '"${API_PORT}"'; then
sudo /usr/sbin/semanage port -a -t http_port_t -p tcp '"${API_PORT}"'
fi
sudo /usr/sbin/restorecon -R /etc/nginx/conf.d/'"${SERVER_NAME}"'.conf
sudo /usr/sbin/nginx -t
# Verify the reload actually landed — see deploy.yml for why neither
# `nginx -t` nor `systemctl reload`s exit code is enough.
master=$(cat /run/nginx.pid)
before=$(pgrep -P "$master" | sort | tr "\n" " ")
sudo /usr/bin/systemctl reload nginx
for _ in $(seq 1 10); do
sleep 1
after=$(pgrep -P "$master" | sort | tr "\n" " ")
[ "$before" = "$after" ] || break
done
if [ "$before" = "$after" ]; then
echo "nginx reload did not take effect: worker generation unchanged" >&2
sudo /usr/bin/tail -n 50 /var/log/nginx/error.log >&2 || true
exit 1
fi'