Files
moments/.gitea/workflows/refresh.yml
rob thijssen 3260bfb35b fix(ci): share one nginx-vhost renderer; drop unused deploy.sh
The nightly refresh.yml and deploy.yml each substituted asset/nginx/
site.conf.tmpl with their own inline python. When bb2f5b1 templated the
listen line as {{WEB_LISTEN}} (moving the vhost behind oolon's stream SNI
router), it added the substitution to the template and deploy.yml but not
to refresh.yml. The daily refresh then rsynced a literal
`listen {{WEB_LISTEN}};` into /etc/nginx/conf.d/rob.tn.conf, `nginx -t`
failed for the whole edge, and — because the file is written into the live
conf.d before it is tested — every vhost's reload (including the step@
cert renewals) stayed frozen. Internal vhosts, cichlid.internal among
them, served certs that had expired days earlier while the renewed certs
sat unused on disk.

- Replace both inline renderers with script/render-site-conf.py, shared by
  deploy.yml and refresh.yml so they cannot drift on what they substitute.
- Guard rails: the renderer fails if any {{PLACEHOLDER}} lacks an env value
  or survives substitution, so a forgotten/misnamed variable is a red build
  on the runner instead of a broken vhost on the edge.
- Add the missing WEB_LISTEN to refresh.yml's env (the immediate drift).
- Rename the template's {{DOCROOT}} to {{WEB_ROOT}} so every placeholder
  maps to the env var of the same name.
- Remove script/deploy.sh: the third, unused renderer of the same template
  (superseded by the Actions workflows) and a standing source of drift.
- Docs (readme, CLAUDE.md) updated to the Actions-only deploy path.

Known follow-up (needs a sudoers change + infra-setup re-run on oolon, so
out of scope here): the rendered vhost is still rsynced straight into the
live conf.d and only then `nginx -t`'d, so a valid-but-wrong config could
still wedge nginx. Stage-validate-swap with rollback would close that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsH1rcWQYtRVhvaftiKm22
2026-07-26 15:52:18 +03:00

119 lines
5.1 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
# TCP 443 on oolon is owned by the stream SNI router; this vhost sits behind it
# on 127.0.0.1:14443 with PROXY protocol. Must match deploy.yml's WEB_LISTEN —
# render-site-conf.py fails the build if it is missing rather than shipping a
# literal placeholder (see the render step below and deploy.yml for the history).
WEB_LISTEN: 127.0.0.1:14443 ssl proxy_protocol
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
# Shared with deploy.yml so the two pipelines can't drift on what they
# substitute; fails loudly if any {{PLACEHOLDER}} lacks an env value or
# survives substitution, so a broken vhost never reaches oolon. (This
# step is exactly where the drift bit: WEB_LISTEN was added to the
# template and deploy.yml but not here, and the nightly refresh shipped
# a literal `listen {{WEB_LISTEN}};` that froze every reload on oolon.)
run: python3 script/render-site-conf.py rendered/site.conf
- 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'