Files
qapi/.gitea/workflows/deploy.yaml
rob thijssen 93641f27c4
Some checks failed
deploy / build (push) Failing after 14s
deploy / deploy-web (push) Has been skipped
fix: preflight the sudoers grants by destination, not by command path
oolon is a shared edge proxy. blackbeard.observer and others already grant
gitea_ci `/usr/bin/rsync` into their own webroots, so checking for the bare
command path passed on somebody else's grant — and the deploy would then fail at
the rsync, halfway, having already been told everything was fine.

The grants are destination-qualified in script/infra-setup.sh for exactly this
reason; the check now matches them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 08:42:55 +03:00

185 lines
7.7 KiB
YAML

name: deploy
# The workflow is the source of infra truth (architecture/deployment-gitea-actions.md):
# hosts, paths and the component→host mapping live here and nowhere else. There
# is no separate deployment manifest.
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
# Serialise deploys; never half-apply two at once.
group: deploy
cancel-in-progress: false
env:
# --- infra truth -----------------------------------------------------------
# oolon is the kosherinata edge proxy. There is no application host: the
# console is a static SPA that opens WebSockets straight from the browser to
# the chains' own RPC endpoints, so nothing is deployed beside a node.
EDGE_HOST: oolon.kosherinata.internal
PUBLIC_NAME: qapi.blackbeard.observer
WEBROOT: /var/www/qapi.blackbeard.observer
DEPLOY_KEY: |
${{ secrets.RSYNC_SSH_KEY }}
jobs:
build:
# fedora-44 carries node + npm + pnpm (gitea-runners.md §4) and nothing else
# is needed — there is no Rust half here.
runs-on: fedora-44
steps:
- uses: actions/checkout@v4
- name: install
# No `corepack enable`: no runner image bundles it, and pnpm is already
# on PATH from `npm i -g pnpm` (gitea-runners.md §4).
#
# `--frozen-lockfile` is what makes the patch in patches/ load-bearing
# rather than advisory: pnpm applies patchedDependencies during install,
# so a lockfile drift that dropped the patch would fail here instead of
# shipping a console that rejects every Quantus signature at run time.
run: pnpm install --frozen-lockfile
- name: the patch is applied
# Cheap, and it is the one thing about this fork that a normal build
# would not notice going missing. Without it every signing attempt dies
# with `Unkown signer` — in the browser, after deploy, to a user.
run: |
set -euo pipefail
# Resolve the package entry and walk to its sibling: the package's
# `exports` map has no `./dist/*` subpath, so resolving the file
# directly throws ERR_PACKAGE_PATH_NOT_EXPORTED.
resolved=$(node -e "
const path = require('node:path');
const entry = require.resolve('@polkadot-api/signers-common');
console.log(path.join(path.dirname(entry), 'v4.js'));
")
echo "resolved: $resolved"
grep -q 'Quantus patch' "$resolved" || {
echo "the signers-common patch is NOT applied to the resolved copy" >&2
echo "see patches/@polkadot-api__signers-common.patch and papi-console#1" >&2
exit 1
}
echo "signers-common carries the Quantus patch"
- name: build
# `pnpm build` is `tsc -b && vite build`, so the typecheck is the gate.
#
# `pnpm lint` is deliberately NOT run: it is broken in upstream and was
# before this fork touched anything — typescript-eslint 8.69 refuses to
# load against the TypeScript 7.0 this repo resolves ("typescript-eslint
# does not support TS 7.0"). Adding it here would fail every deploy for
# a reason that has nothing to do with the change being deployed. Put it
# back when upstream's dependency skew resolves.
run: pnpm build
- uses: actions/upload-artifact@v3
with:
name: qapi-console
path: dist
deploy-web:
needs: build
runs-on: fedora-44
steps:
- uses: actions/download-artifact@v3
with:
name: qapi-console
path: dist
- name: ssh key and reachability
run: |
set -euo pipefail
install -d -m 0700 ~/.ssh
printf '%s' "$DEPLOY_KEY" > ~/.ssh/id_deploy
chmod 0600 ~/.ssh/id_deploy
cat > ~/.ssh/config <<EOF
Host *
IdentityFile ~/.ssh/id_deploy
StrictHostKeyChecking accept-new
User gitea_ci
EOF
ssh "$EDGE_HOST" hostname -f
- name: preflight the sudoers grants
# Fail up front, naming what is missing, rather than halfway through a
# deploy. The grants live in script/infra-setup.sh and nothing else keeps
# the two in step — so when this fails the fix is to re-run that script,
# not to widen anything here.
run: |
set -euo pipefail
allowed=$(ssh "$EDGE_HOST" sudo -n -l 2>/dev/null || true)
missing=0
# Destination-qualified, not just the command path. oolon is a SHARED
# proxy: blackbeard.observer and others already grant gitea_ci
# /usr/bin/rsync into *their* webroots, so a bare command check passes
# on somebody else's grant and the deploy then fails at the rsync.
for cmd in \
"/usr/bin/rsync * $WEBROOT/" \
"/usr/sbin/restorecon -R $WEBROOT" \
"/usr/sbin/nginx -t" \
"/usr/bin/systemctl reload nginx"
do
if ! grep -qF -- "$cmd" <<<"$allowed"; then
echo "missing sudo grant: $cmd" >&2
missing=1
fi
done
[ "$missing" -eq 0 ] || {
echo "re-run script/infra-setup.sh --role edge on a workstation" >&2
exit 1
}
echo "sudoers grants present"
- name: ship the bundle
# --delete: Vite's hashed asset filenames accumulate forever otherwise.
# The webroot holds only build output, so there is nothing else to lose.
# --checksum rather than the default mtime+size: artifact download
# rewrites every mtime, which would otherwise re-send the whole bundle.
run: |
set -euo pipefail
rsync -a --checksum --delete --mkpath --rsync-path='sudo rsync' \
dist/ "$EDGE_HOST:$WEBROOT/"
- name: label and reload
run: |
set -euo pipefail
ssh "$EDGE_HOST" bash -euo pipefail <<REMOTE
# SELinux: rsynced files inherit the directory's type, but a file
# arriving into an unlabelled tree makes nginx return 403 with
# nothing in its error log to explain it.
sudo restorecon -R "$WEBROOT"
sudo nginx -t
sudo systemctl reload nginx
REMOTE
- name: fetch the page
# `nginx -t` parses without binding and `systemctl reload` exits 0 even
# when nginx aborted the reconfiguration and kept what it had
# (architecture/reverse-proxies.md §4). Neither is evidence. Fetching is.
#
# --resolve to loopback: from inside the mesh the public name resolves
# to the site's WAN address and dead-ends on the OPNsense LAN interface
# (reverse-proxies.md §2). Pinning it to 127.0.0.1 still exercises the
# real path — the :443 stream router, SNI, the https tier, the vhost.
run: |
set -euo pipefail
ssh "$EDGE_HOST" "curl -sSf --max-time 20 --resolve $PUBLIC_NAME:443:127.0.0.1 https://$PUBLIC_NAME/" > index.html
# A 200 serving the wrong thing is a broken deploy that a status code
# alone would pass: nginx falls back to /index.html for anything it
# cannot find, so an empty webroot answers 200 with the SPA shell of
# whatever was there before.
grep -q '<title>qapi console</title>' index.html || {
echo "the page served is not this console:" >&2
head -c 400 index.html >&2
exit 1
}
bundle=$(grep -oE '/assets/[A-Za-z0-9_.-]+\.js' index.html | head -1)
[ -n "$bundle" ] || { echo "index.html references no bundle" >&2; exit 1; }
ssh "$EDGE_HOST" "curl -sSf -o /dev/null -w '%{http_code} %{size_download}\n' --max-time 20 --resolve $PUBLIC_NAME:443:127.0.0.1 https://$PUBLIC_NAME$bundle"
echo "served $bundle"