Skip to content

feat(staging): sync staging from main automatically, seed it, one way in - #105

Merged
Programcode merged 13 commits into
mainfrom
feat/staging-loop
Oct 3, 2026
Merged

Programcode merged 13 commits into
mainfrom
feat/staging-loop

Conversation

@Programcode

@Programcode Programcode commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Staging now follows main by itself. Within about 5 minutes of each commit on main, a timer on the VPS applies migrations and deploys changed Edge Functions to staging, reseeds invented but realistic data, and reports the result on the commit. docs/staging.md is the one place that says how to get in.

Plan and audit: plans/2026-10-03-staging-loop.md

Split with other PRs

What changes

  • scripts/staging-sync.mjs refuses to run unless the Supabase CLI sees brifeyypjauyusmkptkx named debate-trainer-staging, and it refuses when SUPABASE_PROJECT_ID points anywhere else. It passes --project-ref to every CLI call. It applies pending migrations only after db push --dry-run lists exactly those files, then deploys missing, stale or wrong-JWT functions (never workspace-invites) and rechecks. It stops if staging's migration history diverges from the commit. With --report it sets a staging-sync commit status and opens, comments on or closes the issue "Staging is out of sync with main".
  • ops/staging-sync/ holds a user systemd timer that fires every 5 minutes, run.sh, which keeps a dedicated clone of main and never runs npm install, and install.sh. A synced commit is rechecked hourly, so hand deploys show up as drift.
  • scripts/staging/seed.mjs and fixtures.mjs create seed-admin, seed-pro, seed-trial and seed-expired, and make Adam's Google address a Pro admin. The seed-pro demo code is kept in ~/.config/debatelab/staging-access.env on the VPS. Each account gets copies of three invented debates with sources and saved results. Rows have fixed ids and are upserted, so a rerun is safe. The seed refuses to write unless the fetched keys carry staging's JWT ref. --session <account> signs agents in as any seeded account (a demo code would make it Pro), and --owner-tier moves Adam's account to trial or expired until the next seed.
  • Protected staging E2E has a gate job that waits up to 20 minutes for staging-sync success on its commit and skips commits main has moved past. The browser job rechecks this on every attempt and times out after 30 minutes. run.sh holds a sync while a browser run is testing. The workflow gains statuses: read.
  • staging-preview-note.yml posts one comment on any PR that touches supabase/migrations or supabase/functions. The comment says the PR's staging preview runs main's backend until merge.
  • docs/staging.md is new. operations.md drops the manual sync procedure, because needing approval for every run is why staging drifted. AGENTS.md, testing.md, the docs index and the plans index now point to docs/staging.md.

Credential and blast radius

Adam chose to have the timer use the Supabase CLI token already on the VPS. That is a classic token, and it can reach production. The ref checks keep the sync on staging, but they are not a security boundary. Whoever merges to main controls the script the timer runs, and it runs within 5 minutes. The follow-up is a Supabase token scoped to staging only.

Outside the repo (done)

  • Installed debatelab-staging-sync.timer on the VPS. Linger is on, and node, supabase and gh all resolve under the unit's PATH. The first run on main exited cleanly, because main doesn't have the script yet.
  • Created a Vercel Protection Bypass for Automation on debatelab-staging. It is stored as VERCEL_STAGING_BYPASS in the GitHub staging environment and in the VPS access file. Without it a preview returns 302; with it the preview returns 200.
  • Seeded staging twice. Both runs left 8 seeded debates and 19 seeded sources.

Verification

All checks ran locally. GitHub Actions jobs fail in 2 to 3 seconds with zero steps because the account is out of minutes, so there is no CI pass. Copilot is out of quota, so Codex (gpt-6.1-sol, high) did the reviews: five rounds, each finding checked against the code and fixed (see the commits).

On the branch, after merging main (#95, #100, #101):

  • docs:check, lint, typecheck, unit tests (278), test:edge (344) and build pass at c6a90c5. At de705f8, before ci: three-minute PR check and two previews per PR #95 cut the unit suite, the full unit suite with coverage (1232), collaboration (30), edge, build and test:sw also passed. Desktop and mobile browser E2E passed at 34d2ec0 (250 passed, 6 skipped).
  • The protected staging suite passes locally at 097aeb3 against synced staging (2 passed, 28.4 s, including one real AI request). It ran on port 18080 with the seeded Pro code, because the local copy of the CI code has expired.
  • A real staging-sync.mjs --report run outside an agent environment deployed 13 functions to staging, rechecked, and set staging-sync to success. The code was byte-identical to main, which the ci: three-minute PR check and two previews per PR #95 thread had already deployed. This branch's merge commit dated the functions as changed.
  • The PR's debatelab-staging Vercel preview works on a phone viewport. Every request went to staging, the seeded Pro sign-in worked, and the seeded debate showed (screenshots and video in the comments).
  • A real run with SUPABASE_PROJECT_ID set to production is refused with exit code 2, and with --report the refusal sets a failed status and opens the drift issue.
  • Drift-reporting drills on PR commits opened and closed the issue. The duplicates Staging is out of sync with main #102 and Staging is out of sync with main #103 came from GitHub's search lag. The script now lists issues instead of searching. Staging is out of sync with main #104 and Staging is out of sync with main #106 were drills.
  • The timer is installed. Linger is on, and its GitHub queries work under systemd.
  • Staging Auth has Google enabled, the staging hosts are allowlisted, and Adam's seeded account is email-confirmed.

Not yet proven:

  • That a merge to main syncs staging with nobody touching it. This PR's merge is the first test (status plus reseed). The next merge that changes Edge Functions proves the deploy step.
  • Adam's own Google sign-in on staging. Only Adam can test it.

🤖 Generated with Claude Code

Programcode and others added 2 commits October 3, 2026 05:51
…ne way in

- scripts/staging-sync.mjs applies migrations and deploys changed Edge
  Functions to brifeyypjauyusmkptkx only, then sets a staging-sync commit
  status and opens or closes a drift issue.
- ops/staging-sync/ holds the user systemd timer that runs it every 5 minutes
  from a dedicated clone of main, without npm install.
- scripts/staging/seed.mjs seeds invented debates, sources and saved results
  for Pro, trial and expired accounts plus Adam's. It is idempotent.
- Protected staging E2E waits for staging-sync on its commit.
- PRs that change supabase/ get a note that staging runs main's backend.
- docs/staging.md is the one way in; the manual sync procedure is gone.

Plan and audit: plans/2026-10-03-staging-loop.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…its body

The search index lagged during a drill and a retry opened a second issue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 3, 2026 05:54
@vercel

vercel Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Oct 3, 2026 7:37am UTC
debatelab-staging Ready Ready Preview Oct 3, 2026 7:37am UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 24 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Free
  • Run ID: 9db93e42-4eb8-4b9a-8dfd-a55fafab841a
📥 Commits

Reviewing files that changed from the base of the PR and between f511b29 and e8282df.

📒 Files selected for processing (17)
  • .github/workflows/staging-e2e.yml
  • .github/workflows/staging-preview-note.yml
  • AGENTS.md
  • docs/README.md
  • docs/operations.md
  • docs/staging.md
  • docs/testing.md
  • ops/staging-sync/debatelab-staging-sync.service
  • ops/staging-sync/debatelab-staging-sync.timer
  • ops/staging-sync/install.sh
  • ops/staging-sync/run.sh
  • plans/2026-10-03-staging-loop.md
  • plans/README.md
  • scripts/staging-sync.mjs
  • scripts/staging/fixtures.mjs
  • scripts/staging/seed.mjs
  • src/test/scripts/staging-sync.test.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

- Pass --agent no to every Supabase CLI call and ask for JSON from the
  migration dry run. Outside an agent the CLI printed text, so the timer would
  have refused every migration.
- Fail the sync while a function is deployed on staging but gone from the
  commit, instead of reporting success.
- Split staging E2E into a gate job that waits for staging-sync on its commit
  and skips commits main has moved past, so it never tests another commit's
  backend.
- seed.mjs --session <account> signs agents in as trial or expired accounts,
  which demo codes would turn into Pro; --owner-tier switches Adam's plan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Seeded staging, driven through a local build pinned to staging (VITE_STAGING_PREVIEW). I signed in on the real login page with the seeded Pro demo code. Recording started after sign-in, so the code doesn't appear. The debatelab-staging Vercel preview gets the same pin once #95 merges.

Trial and expired accounts, signed in with seed.mjs --session, keep their plans.

Desktop: seeded Current Debate

Mobile: Library

Mobile: History

Trial account

Expired account

seeded-desktop.webm
seeded-mobile.webm

…er run

- Give each seeded chat message its own created_at. Chats load ordered by
  that column alone, and a bulk insert gave every message the same time.
- run.sh holds a new commit's sync while Protected staging E2E is still
  testing an older commit, so no run passes against a backend that changed
  under it. The browser job now times out after 30 minutes, which bounds
  the hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; report refusals

A failed run-list query no longer counts as no active run. With --report, a
refusal before syncing (expired login, wrong target) now sets a failed
staging-sync status and opens the drift issue instead of failing silently.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
run.sh queried the last 20 runs, so a busy PR stream could hide an older run
that was still testing. It now asks per active status. The browser job checks
that main and staging still run its commit on every attempt, because
re-running failed jobs skips the gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… runs

Runs for the synced commit were exempt, so an hourly drift repair could
redeploy under them. Only runs still waiting in the gate are exempt now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

The real PR preview works on a phone viewport (Pixel 7). This is debatelab-staging-zzo5t7b05, the debatelab-staging Vercel preview of this branch at c6a90c5, which includes #95's pin. Playwright opened it through the VERCEL_STAGING_BYPASS header and signed in on the login page with the seeded Pro demo code; nothing was recorded during sign-in. Every Supabase request went to brifeyypjauyusmkptkx (staging), and the page shows the Staging backend strip, the seeded Current Debate, and the Pro plan.

Also on this head, a real scripts/staging-sync.mjs --report run, outside an agent environment, deployed 13 functions to staging, rechecked, and set staging-sync to success (097aeb3). The code was byte-identical to main. My branch's merge-of-main commit dated them as changed, so this was the first real run of the deploy path. Afterwards, preview hosts pass CORS, a lookalike host is refused, and demo-code sign-in returns a handoff.

Phone: preview home

Phone: seeded debate on the preview

Phone: Pro on staging

phone-preview.webm

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nning browser jobs

- Deploy dates only say when a function was deployed. The sync now downloads
  what staging runs and compares it byte for byte with the commit, so an older
  checkout deployed after a merge no longer passes as current. Proven against
  a checkout from before #95: dates said nothing was pending, and the content
  check flagged the 13 functions that run the newer cors.ts.
- run.sh holds a sync only while a browser job is running. A queued or gated
  run isn't using staging, and the job timeout doesn't cover queue time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A browser job could start between run.sh's check and a drift repair while the
old success status still stood. The sync now sets staging-sync to pending
before any change, so new browser jobs refuse to test. It then checks once
more for a running job, and if one started it restores the status, changes
nothing and exits 3, which run.sh retries on the next tick.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
grep -q exiting early sent gh SIGPIPE, and under pipefail the step read that
as no marker and posted a duplicate note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode
Programcode merged commit 9b013ec into main Oct 3, 2026
6 of 9 checks passed
@Programcode
Programcode deleted the feat/staging-loop branch October 3, 2026 07:40

This branch was successfully deployed

2 active deployments
Preview – debate-navigator — e8282dfa Deployed Oct 3, 2026 by vercel[bot]
Preview – debatelab-staging — e8282dfa Deployed Oct 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants