Skip to content

Web Deploy Workflow

mdeguzis edited this page Jul 21, 2026 · 5 revisions

Web Deploy Workflow

This page covers the end-to-end release process for proton-pulse-web -- from local changes through staging review to production.

Contents


Branch and issue workflow

Every change needs a GitHub issue. This is the paper trail and the anchor for staging/prod comments.

All in-progress code goes to the staging branch. Production (main) only receives code through a reviewed and approved PR.

Features

  1. Create a GitHub issue: gh issue create --repo mdeguzis/proton-pulse-web --title "..." --label enhancement
  2. Create a feature branch off staging: git checkout staging && git checkout -b feature/<short-name>
  3. Do all work on the feature branch. Reference the issue in commits: feat: ... (closes #N)
  4. Merge the feature branch into staging when ready: git checkout staging && git merge feature/<short-name> && git push origin staging
  5. Go through the staging review flow. Open a PR staging -> main once satisfied.
  6. After the issue closes, delete the branch: git push origin --delete feature/<short-name>

Bugs

  1. Create a GitHub issue: gh issue create --repo mdeguzis/proton-pulse-web --title "..." --label bug
  2. Commit the fix to the staging branch (or a fix/<short-name> branch merged into staging).
  3. Reference the issue in the commit: fix: ... (closes #N)

Branch naming: feature/<short-name> for features, fix/<short-name> for standalone bug branches.


Overview

All changes follow this path:

local changes -> pre-push checks -> push to staging -> staging deploy -> staging review -> PR staging->main -> production

Production (main) is never touched until staging has been reviewed and the PR has been approved. Branch protection on main requires 1 approved review and a passing test CI check before any merge can proceed.

Hosting: Cloudflare Pages (as of #362)

The site was migrated off GitHub Pages onto Cloudflare Pages. Two projects:

Environment CF Pages project Custom domain
Staging proton-pulse-web-staging staging.proton-pulse.com
Production proton-pulse-web www.proton-pulse.com

Per-game data buckets (~187k files) live in the R2 bucket proton-pulse-data, served at data.proton-pulse.com. The Pages projects only host the shell + a few dozen small top-level JSON files.

Every push to staging or main that touches a user-facing file (*.html, css/**, js/**, gh-pages-manifest.txt, scripts/publish-cloudflare.sh, scripts/preserve-cert-monitor.sh, or .github/workflows/publish-shell.yml) auto-triggers publish-shell.yml, which deploys the shell to the matching CF Pages project within ~1-2 minutes. No manual step required for typical UI changes.

The retired GitHub Pages deploy path (gh-pages branch + pages-build-deployment) is kept as a rollback option only; see Rollback to GitHub Pages.


Before you start

The web app lives at:

~/src/decky-proton-pulse-project/proton-pulse-web/

All deploy targets require gh (GitHub CLI) and a valid session. Confirm before starting:

gh auth status

Make sure you are on the staging branch before making changes:

git checkout staging

Step 1: make changes and pre-push

After editing JS or CSS, run:

make pre-push

This runs three things in sequence:

  1. cache-bust (make build) - recomputes ?v= hashes on all JS/CSS imports based on file content. Every file that changed gets a new hash; unchanged files keep theirs.
  2. Jest tests (make coverage) - unit tests, manifest completeness check, and coverage gate.
  3. Smoke render (make smoke) - headless render check that catches missing imports before they hit CI.

If any step fails, fix it before committing. Do not commit with a failing make pre-push.

Adding a new JS or CSS file

Two extra steps are required when adding a new file:

  1. Add it to gh-pages-manifest.txt -- the pages-only deploy only copies files listed there. A missing entry means the import 404s silently on production.
  2. Add it to every relevant deploy step in .github/workflows/update-data.yml.

The manifest test in Jest catches rule 1 automatically -- npm test will fail if a manifest JS file imports something not listed in the manifest.


Step 2: commit and push to staging

git add <files>
git commit -m "feat: ..."
git push origin staging

Commit messages must follow Conventional Commits: feat:, fix:, chore:, ci:, docs:, etc.

On push, CI runs automatically:

  • CI (ci.yml) - Jest tests with coverage, Supabase schema/RLS integration tests
  • Sync staging repo (sync-staging-main.yml) - mirrors changes to proton-pulse-web-staging repo

Watch CI at: https://github.com/mdeguzis/proton-pulse-web/actions


Step 3: deploy to staging site

Nothing to run. The push to staging in Step 2 auto-triggers publish-shell.yml for any user-facing file change. Within ~1-2 minutes the shell lands on staging.proton-pulse.com.

Check the run via the Deployments admin tab (admin.html) or:

gh run list --repo mdeguzis/proton-pulse-web --workflow="Publish Shell to Cloudflare Pages" --limit 3

Live staging: https://staging.proton-pulse.com/

If you need to force a re-deploy without a new commit (e.g. after fixing a CF secret out-of-band):

make cf-staging      # dispatches publish-shell.yml against staging

Legacy targets

make gh-staging still exists and deploys to the retired mdeguzis.github.io/proton-pulse-web-staging GitHub Pages repo (that URL now 301-redirects, so the deploy has no visible effect). Kept as a rollback path only -- see Rollback to GitHub Pages.


Step 4: review on staging

Open the staging site and verify:

  • The change you made works as expected
  • No regressions on pages you did not touch (home, app, admin if relevant)
  • No console errors in browser DevTools

Post the staging run URL and staging preview URL as a comment on the relevant GitHub issue.

Do not make further commits or trigger another staging deploy until the user confirms the staging looks correct. One staging deploy per iteration.


Step 5: open a PR and merge to main

This step must always be performed by a human. Never merge the PR yourself.

Once staging is confirmed good, open a PR from staging to main:

gh pr create --base main --head staging --repo mdeguzis/proton-pulse-web --title "..." --body "..."

Branch protection on main requires:

  • At least 1 approved review
  • Passing test CI status check

The user reviews the PR, approves it, and merges it. Only after the merge does production get updated.


Step 6: promote to production

Nothing to run. The merge into main from Step 5 is itself a push that auto-triggers publish-shell.yml against main. The shell lands on www.proton-pulse.com within ~1-2 minutes.

Check the run via the admin Deployments tab or:

gh run list --repo mdeguzis/proton-pulse-web --workflow="Publish Shell to Cloudflare Pages" --limit 3

Live site: https://www.proton-pulse.com/

Confirm on the about page that the SHA matches main's tip. Post the run URL as a follow-up comment on the issue and close it.

Version guard

scripts/publish-cloudflare.sh fetches the live version.json before deploying and compares the deployed_at timestamp to the git commit's ISO (normalized to UTC via date -d -u). If the commit is OLDER than what is already live, the deploy is SKIPPED with exit 0. This catches the pipeline-with-stale-checkout race where a long finalize run finishes after a fresh shell push and would otherwise overwrite it.

Manual force

If you need to dispatch prod manually (e.g. after fixing a CF secret and no new commit is available):

make cf-prod        # dispatches publish-shell.yml against main

Legacy target

make gh-pages-only still exists and deploys to the gh-pages branch of the main repo. GitHub Pages will still build that branch, but the DNS for www.proton-pulse.com now points at CF Pages so the gh-pages deploy is only reachable via the mdeguzis.github.io/proton-pulse-web/ URL. Kept as a rollback option -- see Rollback to GitHub Pages.


Full pipeline deploy

Use this when game data also needs refreshing (new pipeline features, data format changes, or a scheduled full refresh):

make gh-run

This runs the full data pipeline (Steam catalog fetch, ProtonDB probe, finalize, search index) and then deploys to production. It also requires the staging site to be in sync with origin/staging.


Staging deploy paths (pick the smallest one that fits)

Staging has three deploy targets. Pick the smallest one that actually exercises the change so verification stays fast.

flowchart TD
    A[Change on staging] --> B{Touches JS / CSS / HTML only?}
    B -- yes --> C[make gh-staging<br/>~2 min<br/>shell only, staging reads prod data]
    B -- no --> D{Touches finalize.py or<br/>search-index / recent-reports /<br/>stats / most_played shape only?}
    D -- yes --> E[make gh-staging-finalize<br/>~5 min<br/>skip probe + build,<br/>restore prod chunk state,<br/>rerun finalize + stats,<br/>push to staging repo]
    D -- no --> F[make gh-staging-pipeline<br/>~30 min<br/>full pipeline against staging,<br/>push to staging repo]
    C --> G[Verify at staging URL]
    E --> G
    F --> G
    G --> H{Looks right?}
    H -- yes --> I[Open PR staging to main]
    H -- no --> A
Loading

Rules for picking:

  • If the change is UI-only, gh-staging is enough. Staging falls back to prod data through fetchDataWithProdFallback, so cards render exactly as they will on prod.
  • If the change adds or renames a field in a pipeline JSON output (search-index column, recent-reports field, stats bucket), gh-staging-finalize regenerates the affected files against prod chunk state. This is the right target for schema-only shape changes.
  • Reach for gh-staging-pipeline only when the change needs fresh probe data (new backfill logic, changed probe cadence, a new signal source).

The staging_with_finalize mode reuses the sharded chunk state saved to gh-pages/.pipeline-state/ (see Data-Pipeline). No probing happens on this path, which is why it lands in about the wall time of a normal finalize step.


CI workflows

Workflow Trigger What it does
CI (ci.yml) push to main/staging, daily Jest + coverage, Supabase schema/RLS tests
Publish Shell to Cloudflare Pages (publish-shell.yml) push to main/staging (path-filtered), manual dispatch Fast shell-only CF Pages deploy (~1-2 min). Handles the day-to-day "my push is now live" path.
Update ProtonDB Data (update-data.yml) manual dispatch, cron, staging/prod deploy targets Full data pipeline. Deploys to CF Pages by default (deploy_target=cloudflare) via publish-cloudflare.sh. ghpages option kept as rollback.
Deploy Cloudflare Workers (deploy-worker.yml) push to workers/**, manual dispatch Wrangler deploy of the pp-edge-status worker
Deploy Cloudflare Functions (deploy-functions.yml) push to supabase/functions/**, manual dispatch Deploys Supabase edge functions
Cert Monitor (cert-monitor.yml) every 6 hours Reads the served TLS cert, writes cert-status.json + cert-history.json to gh-pages (still the source of truth; publish-cloudflare.sh copies these into the CF Pages deploy)
Content Moderation (content-moderation.yml) scheduled Scans submitted reports for banned phrases
Sync staging repo (sync-staging-main.yml) push to main/staging, daily Mirrors branch to the retired staging repo (kept as rollback)
pages-build-deployment automatic on gh-pages push GitHub Pages build for the retired live site (rollback only)

publish-shell.yml and the finalize job in update-data.yml share a concurrency group cf-pages-<project> with cancel-in-progress: false. Deploys to the same CF Pages project queue behind each other rather than racing.

Badges for all workflows are in the README.

Supabase schema tests

tests/supabaseSchema.test.js runs against the live Supabase Management API using SUPABASE_URL and SUPABASE_TOKEN secrets. Tests use describeIfCreds and skip automatically when credentials are absent (forked PRs). The tests include 2-attempt retry logic with 3s backoff for transient Supabase 5xx responses.


Skipping staging

Staging review may be skipped only for:

  • Typo-only README or docs changes
  • CI/workflow-only changes with no site impact
  • Hotfixes the user has already verified on the live site

For everything else, go through staging.


Emergency overrides

If you need to promote to production without going through the staging sync check (e.g. a hotfix that was already verified live):

FORCE_DEPLOY=1 make gh-pages-only

Use this sparingly. It bypasses the staging SHA check entirely.


Rollback to GitHub Pages

The GitHub Pages deploy path is preserved as a rollback if Cloudflare Pages goes down or a CF-side regression forces us to revert. Steps:

  1. Dispatch the pipeline with deploy_target=ghpages via workflow_dispatch (Actions -> Update ProtonDB Data -> Run workflow). This deploys shell + data to the gh-pages branch of the main repo.
  2. Repoint DNS at Cloudflare so www.proton-pulse.com -> the GitHub Pages IPs (185.199.108-111.153) instead of the CF Pages worker route.
  3. Set Cloudflare SSL/TLS mode back to Full (non-strict). Full (Strict) will 526 against GitHub Pages' Let's Encrypt cert because Cloudflare cannot validate it while proxied.
  4. If the GH Pages cert has expired during the CF-only window, follow the retired GitHub-Pages-Cert-Renewal walkthrough (kept in the wiki for exactly this scenario).

Reverse the steps to return to CF Pages. Keep the rollback window short -- staying on GH Pages long-term reintroduces the origin-cert saga that motivated the migration.

Clone this wiki locally