Skip to content

feat(staging): staging environment for beta.sona.fast (#166) - #171

Open
sparkyfen wants to merge 2 commits into
mainfrom
feat/staging-env-166
Open

feat(staging): staging environment for beta.sona.fast (#166)#171
sparkyfen wants to merge 2 commits into
mainfrom
feat/staging-env-166

Conversation

@sparkyfen

@sparkyfen sparkyfen commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

A staging deployment of the canonical sona-fast/sona repo at https://beta.sona.fast, behind Cloudflare Access (invite-only beta testers), for exercising changes against real Cloudflare infra before they reach a fork.

Pages project sona-staging, D1 sona-staging-db, R2 sona-staging-images.

Architecture

  • .github/workflows/staging-deploy.yml — push to main + manual dispatch, gated if: github.repository == 'sona-fast/sona' (the exact inverse of deploy.yml's fork gate, so the two never both fire on one repo). Mirrors deploy.yml's step sequence unchanged: check/test/build → create-Pages-project-if-needed → tracked schema_migrations D1 migrations → optional secret/var syncs (each skips cleanly when unset) → wrangler pages deploy. The CLOUDFLARE_PAGES_PROJECT / D1_DATABASE_NAME fallbacks default to the staging names, not deploy.yml's prod sona/sona-db, so a missing repo var can never point this pipeline at production.
  • .github/workflows/staging-cron.yml — sticker re-sync (daily 06:00), artist sync (daily 06:30), orphan cleanup (weekly Mon 07:00), each gated to the canonical repo, POSTing beta.sona.fast with Bearer CRON_SECRET, mirroring the fork cron curls. Per-schedule job gating; workflow_dispatch runs all three.
  • scripts/staging-seed.sql — synthetic data only (fake artist, /staging/* placeholder URLs that 404 harmlessly, setupComplete=true) plus a placeholder adminPasswordHash.
  • scripts/hash-admin-password.ts — reads a password from stdin, prints the pbkdf2$sha256$... hash, reusing reset-password.ts's already-parity-tested hasher (one PBKDF2 impl for the CLI tooling).
  • docs/staging.md — one-time setup checklist.

Review catches honored

From the proposal round:

  • Admin hash never baked in — the seed carries a non-verifiable REPLACE_ME_WITH_pbkdf2_HASH placeholder; the operator generates their own via the stdin hasher and sed-substitutes at load time.
  • Beta-testers policy — docs specify a new reusable "Sona beta testers" Access policy (explicitly not the team allowlist), created account-level and attached to a new Access app covering both beta.sona.fast and sona-staging.pages.dev, sequenced before the first deploy so sona-staging is never briefly public.
  • Pages bindings (#115 lesson) — docs call out the one-time DBsona-staging-db, IMAGESsona-staging-images, nodejs_compat, and compatibility_date config that a CI-first Pages project needs.

From this fix round:

  • Duplicate name: key removedstaging-cron.yml had two top-level name: keys (invalid YAML that GitHub's validator rejects, silently never scheduling); now a single name.
  • Silent-lockout warning — the seed's adminPasswordHash INSERT now documents that loading without the sed substitution locks out admin (wizard skipped, no password verifies), with the npm run reset-password / wizard-reset recovery path.

pull_request_target is forbidden in a prominent top-of-file comment (that trigger would run untrusted fork-PR code with CLOUDFLARE_API_TOKEN / SETUP_TOKEN / CRON_SECRET in scope).

Attaching beta.sona.fast to the sona-staging Pages project is exactly the Pages-domain rung of the SONA-6 connect-domains/doctor tooling (in flight on feat/connect-domains-doctor-11), so this doubles as its first live test bed.

Setup

See docs/staging.md — split into manual (token/dashboard) vs scriptable steps, ordered so the Access app+policy exist before the first deploy.

Gates

  • npx vitest run → 84 files / 885 tests pass, incl. 3 new staging-seed tests (migrations+seed apply cleanly to a fresh better-sqlite3 DB; the committed seed carries no real hash; a generated hash round-trips through the app's verifyPasswordHash).
  • npm run check → 0 errors (79 warnings, all pre-existing baseline in .svelte files not touched here).
  • Both workflows validated as parsing YAML with unique top-level keys.
  • Not verifiable in-worktree: the actual Cloudflare deploy and the cron POSTs (runtime infra). actionlint is not installed in this environment.

Closes #166

Deploy + cron workflows gated to the canonical repo (inverse of the fork
gate), a synthetic seed with a placeholder admin hash + stdin hasher, and a
one-time setup runbook.

- staging-deploy.yml: push/dispatch, `if: github.repository == 'sona-fast/sona'`,
  mirrors deploy.yml's step sequence; name fallbacks default to the staging
  project/DB so a missing repo var can't target production. Forbids
  pull_request_target (secret exposure to untrusted PR code).
- staging-cron.yml: sticker re-sync (daily), artist sync (daily), orphan
  cleanup (weekly) POSTing beta.sona.fast with Bearer CRON_SECRET.
- staging-seed.sql: synthetic artist + placeholder images, setupComplete=true,
  and a placeholder adminPasswordHash (loading without substitution is a
  documented silent lockout).
- hash-admin-password.ts: stdin -> pbkdf2 hash, reusing reset-password.ts's
  parity-tested hasher; staging-seed.test.ts proves seed+migrations apply and
  the hash round-trips through verifyPasswordHash.
- docs/staging.md: manual vs scriptable setup, the "Sona beta testers" reusable
  Access policy created before the first deploy, Pages bindings (#115), and the
  wizard-reset procedure.

Closes #166
@sparkyfen sparkyfen added enhancement New feature or request ops CI, deployment, infra, bots labels Jul 10, 2026
# otherwise) are put so the panel has them at runtime. Unset = logged skip.
- name: Sync Cloudflare analytics secrets to Pages project
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have these values set for production so how do we know that they're not gonna clash with staging?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can't clash, for two reasons that stack. First, GitHub Actions secrets are per repository: the production values you're thinking of live in each fork's own repo (sparkyfen/sparky.ink and friends), and this workflow only ever reads sona-fast/sona's secrets, which as of right now are completely empty (I checked: zero secrets, zero variables set on canonical). Whatever you add here per the docs feeds staging alone, because the fork-facing workflows (deploy.yml and the crons) are gated off this repo. Second, even if a wrong value slipped in, the runtime targets resolve from canonical's own variables with fallbacks that fail toward staging names, never a production project or database (the adversarial review verified no fallback resolves to prod). For this analytics block specifically: the zone id you'd set is the sona.fast zone, which is exactly what beta.sona.fast needs, and the account-scoped analytics token from the fork rollout is safe to reuse since it lands only on the sona-staging Pages project. I've also added a note to docs/staging.md spelling out the per-repo isolation (commit dfc7483).

Comment thread docs/staging.md Outdated
Do these **in order**. The Access policy + app (step 3) must exist **before the
first deploy** (step 6) so the site is never briefly public.

### Manual — needs Sparky's Cloudflare token / dashboard

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having my name in these documents feels a little weird, should we just make a generic as if anybody is the owner?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done in dfc7483: the heading now says "the account owner's Cloudflare token / dashboard". That was the only place in the PR that named you (checked the workflows and seed too), so the docs now read correctly for any self-hoster running their own staging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ops CI, deployment, infra, bots

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Staging environment at beta.sona.fast behind Zero Trust

1 participant