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
185 lines
7.7 KiB
YAML
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"
|