chore(deploy): expose the export rebuild hatch in the UI; rehearse migration 014

Three deploy-readiness gaps, none of them in the export invariant itself.

1. The escape hatch was unreachable. `POST /host/export/rebuild` was added as the fix
   for "a failed export is terminal", but nothing in the frontend called it — so recovery
   required a terminal, the API docs and a valid JWT. The host page instead told the host
   to reopen uploads and re-release, which is the destructive act the endpoint exists to
   avoid (it unlocks the gallery to every guest and retracts the release). The failed
   state now offers "Erneut versuchen"; a ready keepsake can be rebuilt behind a confirm,
   since a rebuild makes it briefly undownloadable.

2. Migration 014 had never been run against anything. It is the repo's first destructive
   migration and its backfill has to carry the old notion of "downloadable" across exactly
   — get it wrong and a released event's keepsake silently 404s, on data that cannot be
   re-created. backend/scripts/rehearse-014.sh proves it on a throwaway postgres, against
   a real pg_dump or a synthetic DB covering every pre-014 state, asserting up, down and
   up-again all preserve downloadability. Verified non-vacuous: retiring nothing (ELSE -1
   -> ELSE e.export_epoch) fails it, naming the three keepsakes it would resurrect.

   It also surfaced that the down migration is an inverse UP TO DRIFT: a ready flag that
   disagreed with its job row comes back FALSE. That heals corruption rather than
   restoring it, and costs nothing (no file_path to serve), so the assertion checks what
   actually matters — no genuinely downloadable keepsake loses its flag — and reports the
   normalisation instead of failing on it.

3. No advisory scanning. cargo audit + npm audit, in .github/workflows/ because Gitea
   Actions scans it too and e2e.yml already resolves actions/* from GitHub. The runner is
   not assumed to have cargo. RUSTSEC-2023-0071 (rsa/Marvin) is ignored SPECIFICALLY, not
   globally: it is unreachable here (HS256 + bcrypt, Postgres only) and unpatched, so
   failing on it would mean a permanently red pipeline everyone learns to ignore.

Tests: e2e pins that a failed keepsake recovers via rebuild while the event stays
released and uploads stay locked — so a future refactor implementing rebuild as an
internal reopen+re-release fails — and that rebuild cannot publish an unreleased gallery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-14 21:09:53 +02:00
parent 0447a6ad0e
commit 3c683247c0
4 changed files with 380 additions and 2 deletions

73
.github/workflows/audit.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
# Dependency advisory scan.
#
# Lives in .github/workflows/ because Gitea Actions scans BOTH .gitea/workflows and
# .github/workflows, and e2e.yml already proves this instance resolves `actions/*` from GitHub
# (DEFAULT_ACTIONS_URL). Keeping one directory avoids a split where half the CI is invisible
# depending on which convention you look under.
#
# Unlike the GitHub-hosted runners this workflow does NOT assume a preinstalled Rust toolchain —
# the common Gitea runner images (gitea/runner-images, catthehacker/ubuntu) ship Node and Docker
# but not cargo. So we install it explicitly instead of relying on the ambient environment.
name: Audit
on:
pull_request:
push:
branches: [main]
schedule:
# New advisories land against unchanged code, so a push-only trigger would never see them.
- cron: '0 6 * * 1'
jobs:
cargo-audit:
name: cargo audit (backend)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
run: |
if ! command -v cargo > /dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/bin
key: cargo-audit-${{ hashFiles('backend/Cargo.lock') }}
restore-keys: cargo-audit-
- name: Install cargo-audit
run: cargo install cargo-audit --locked || true
# `rsa` 0.9 (RUSTSEC-2023-0071, "Marvin") is a transitive dep of the SQLx MySQL driver that
# this app never exercises: auth is HS256 + bcrypt, and the only database is Postgres. There
# is no patched release, so failing the build on it would mean a permanently red pipeline
# that everyone learns to ignore — which is worse than not scanning at all. Ignore it
# SPECIFICALLY, so that any OTHER advisory still fails the job.
- name: Audit
working-directory: ./backend
run: cargo audit --deny warnings --ignore RUSTSEC-2023-0071
npm-audit:
name: npm audit (frontend)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install deps
working-directory: ./frontend
run: npm install
# Production dependencies only: a dev-only advisory (build tooling, test runners) can't be
# reached by a guest at the party, and gating merges on it just trains people to skip the gate.
- name: Audit
working-directory: ./frontend
run: npm audit --omit=dev --audit-level=high