Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion .github/workflows/staging-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ permissions:
# Manual dispatch queries workflow runs for the selected main commit.
actions: read
contents: read
# Waits for the staging-sync commit status set by the VPS sync.
statuses: read

concurrency:
group: protected-staging-e2e-main
cancel-in-progress: false

jobs:
staging-e2e:
# Staging is one shared backend that the VPS sync (docs/staging.md) moves to
# each new main commit and marks with a staging-sync commit status. Test a
# commit only once staging runs it, and skip a commit that main has already
# moved past: staging runs the newer backend, and the newer commit's run
# tests it.
staging-sync-gate:
if: >-
(
(github.event_name == 'workflow_run' &&
Expand All @@ -26,8 +33,63 @@ jobs:
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
) && vars.STAGING_E2E_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 25
outputs:
current: ${{ steps.wait.outputs.current }}
steps:
- name: Wait for staging to sync this commit
id: wait
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
shell: bash
run: |
for _ in $(seq 1 40); do
main="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha)"
if [ "$main" != "$SHA" ]; then
echo "::notice::main moved on to ${main}; staging runs that backend, and its own run tests it. Skipping ${SHA}."
echo "current=false" >> "$GITHUB_OUTPUT"
exit 0
fi
state="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${SHA}/statuses" \
--jq '[.[] | select(.context == "staging-sync")][0].state // "missing"')"
case "$state" in
success)
echo "Staging is synced with ${SHA}."
echo "current=true" >> "$GITHUB_OUTPUT"
exit 0 ;;
failure|error)
echo "::error::Staging sync failed for ${SHA}. See the open 'Staging is out of sync with main' issue."
exit 1 ;;
esac
sleep 30
done
echo "::error::No staging-sync status for ${SHA} after 20 minutes. Is debatelab-staging-sync.timer running on the VPS? See docs/staging.md."
exit 1

staging-e2e:
needs: staging-sync-gate
if: needs.staging-sync-gate.outputs.current == 'true'
runs-on: ubuntu-latest
# Bounds how long the VPS sync holds off for this run (ops/staging-sync/run.sh).
timeout-minutes: 30
environment: staging
steps:
# "Re-run failed jobs" skips the gate, so check again on every attempt.
- name: Confirm staging still runs this commit
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
shell: bash
run: |
main="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha)"
state="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${SHA}/statuses" \
--jq '[.[] | select(.context == "staging-sync")][0].state // "missing"')"
if [ "$main" != "$SHA" ] || [ "$state" != "success" ]; then
echo "::error::Staging no longer runs ${SHA} (main is ${main}, staging-sync is ${state}). Rerun the newest main commit's run instead."
exit 1
fi

- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/staging-preview-note.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Staging preview note

# Staging runs main's backend (docs/staging.md). A PR that changes supabase/
# gets a comment saying its staging preview still uses main's migrations and
# Edge Functions until it merges.

on:
pull_request:
types: [opened, reopened, synchronize]
paths:
- "supabase/migrations/**"
- "supabase/functions/**"

permissions:
pull-requests: write

jobs:
note:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Comment once
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
shell: bash
run: |
marker="<!-- staging-preview-note -->"
# Read every comment first: grep -q exiting early would SIGPIPE gh, and
# under pipefail that reads as "no marker" and posts a duplicate.
bodies="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq '.[].body')"
if grep -qF "$marker" <<< "$bodies"; then
exit 0
fi
gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --body "$marker
This PR changes \`supabase/\`. Its **debatelab-staging** preview talks to staging, which runs main's migrations and Edge Functions, so this PR's backend changes are not there yet. The VPS staging sync applies them within about 5 minutes of the merge. To try them before merging, use the \`verify-debatelab\` skill against a local stack. See docs/staging.md."
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The local Supabase stack still uses shared default ports and container names. In

## Deployment

The frontend is hosted on Vercel at `https://debate.pi-stack.org`. Merging to `main` starts a Vercel production deployment, so a merge ships the frontend. Supabase migrations and Edge Functions are not deployed by that merge; they are separate authorized operations. `npm run backend:status` (read-only) lists what the linked project is missing; run it before any backend deploy.
The frontend is hosted on Vercel at `https://debate.pi-stack.org`. Merging to `main` starts a Vercel production deployment, so a merge ships the frontend. Supabase migrations and Edge Functions are not deployed to production by that merge; production deploys are separate authorized operations. Staging syncs itself from `main` within about 5 minutes through a VPS timer; [docs/staging.md](docs/staging.md) is the one place for staging access, seed data, and the sync. `npm run backend:status` (read-only) lists what the linked project is missing; run it before any backend deploy.

## Normal ports

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Start with the smallest document that answers the task instead of loading every
## Operate or deploy

- [Operations guide](operations.md), current. Open before changing CI, Vercel hosting, staging, CORS, migrations, Edge Function deployment, rollback, or production state.
- [Staging](staging.md), current. How to get into staging, the seeded accounts, and the VPS timer that syncs it with `main`.
- [`.github/workflows/`](../.github/workflows/) is the CI source of truth.

## Plans
Expand Down
19 changes: 3 additions & 16 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,15 @@ The staging browser suite disables trace, screenshot, and video capture. It crea

Do not enable the staging repository variable before every prerequisite is present. A skipped staging job is not a pass.

`STAGING_E2E_ENABLED` has been `true` since 2026-09-18. The job runs after every successful `Quality` run from a push to `main`. Quality runs from pull requests also trigger it, and those show as skipped by design. Check the latest real result with `gh run list --workflow staging-e2e.yml` and `npm run ci:failures -- <run-id>`, not this page. The run uploads no artifact, so the job log is the only evidence. On failure the log names the failed step and the URL path, for example `Staging step "add a source to the debate" failed at path /app/library`.
`STAGING_E2E_ENABLED` has been `true` since 2026-09-18. The job runs after every successful `Quality` run from a push to `main`, and it first waits up to 20 minutes for a successful `staging-sync` status on that commit. Quality runs from pull requests also trigger it, and those show as skipped by design. Check the latest real result with `gh run list --workflow staging-e2e.yml` and `npm run ci:failures -- <run-id>`, not this page. The run uploads no artifact, so the job log is the only evidence. On failure the log names the failed step and the URL path, for example `Staging step "add a source to the debate" failed at path /app/library`.

No staging run passed after 2026-09-23. Until 2026-10-01 the job did not run, because the `Quality` run it waits for never started. Once Actions came back, it failed on every `main` push through 2026-10-02. The cause was the spec, not staging: since the Current Debate change (#72), Library disables **Add Source** until a debate is current, and the spec never started one. The spec now starts a uniquely named debate and deletes it afterwards. The suite drives the real UI, so a change to a flow it covers can break it with no backend drift; run it locally before blaming staging.

## Staging sync

Staging is project `debate-trainer-staging` (`brifeyypjauyusmkptkx`). Production is `debate-trainer` (`uziwdtkzofptihahfooo`). Every backend change goes to staging first, passes the staging browser suite, and only then goes to production. Steps 1 and 2 are read-only. Steps 3 and 4 change staging and need Adam's authorization each time.
Staging is project `debate-trainer-staging` (`brifeyypjauyusmkptkx`). Production is `debate-trainer` (`uziwdtkzofptihahfooo`). Staging syncs itself: a timer on the VPS applies migrations and deploys changed Edge Functions to staging within about 5 minutes of every commit on `main`. It sets the `staging-sync` commit status, and on failure it opens the **Staging is out of sync with main** issue. No per-run authorization is needed for staging. [staging.md](staging.md) covers the timer, its credential, the seed, and how to get in.

1. Confirm the target. `supabase projects list` must show the staging ref under the staging name. Stop if a command, link, or environment variable points at the production ref.
2. Find what is pending. Run `npm run backend:status -- --project-ref brifeyypjauyusmkptkx --check` from a checkout of the commit you mean to ship. It compares staging with the committed tree and exits 1 when a migration is not applied. It also lists expected function secrets the project lacks (names only) and prints the Auth site URL and redirect allowlist. Where that script is not available, run its two read-only queries directly:

```bash
supabase migration list --project-ref brifeyypjauyusmkptkx
supabase functions list --project-ref brifeyypjauyusmkptkx
```

A migration with a local version and no remote version is pending. A function whose folder changed (`git log -- supabase/functions/<name> supabase/functions/_shared`) after its remote `updated_at` is stale. Save both lists as the rollback baseline.
3. Apply migrations. Run `supabase db push --project-ref brifeyypjauyusmkptkx --dry-run` and check that it lists exactly the pending files from step 2. Then run it without `--dry-run`. Never use `--include-all` to paper over divergent history.
4. Deploy functions, one at a time, only the ones step 2 marked stale: `supabase functions deploy <name> --project-ref brifeyypjauyusmkptkx --use-api`. [`supabase/config.toml`](../supabase/config.toml) supplies each function's JWT setting. Never use `--prune`.
5. Verify. Rerun step 2; it must show nothing pending. Then start the staging browser suite: dispatch `Protected staging E2E` from `main` with `gh workflow run staging-e2e.yml --ref main`, or, when Actions cannot run, run it locally with the protected values as described in [testing.md](testing.md#protected-staging). For a manual check in the browser, an admin can switch the deployed frontend to staging from Settings and sign in there with a staging demo code. Google and email sign-in on staging work only if the staging Auth redirect URLs include the frontend origin. Since 2026-10-02 they include tailnet previews (`https://codex-remote.tailab0ab3.ts.net:91*/**`); see [development.md](development.md#preview-on-the-tailnet).

Production then follows [Supabase changes](#supabase-changes) with the same list of migrations and functions.
Before a production backend deploy, check that the commit's `staging-sync` status is green and that `Protected staging E2E` passed for it. Then follow [Supabase changes](#supabase-changes) with the list of migrations and functions that `npm run backend:status` reports for production.

## Switching the frontend to staging

Expand Down
Loading
Loading