# Web Deploy Workflow This page covers the end-to-end release process for [proton-pulse-web](https://github.com/mdeguzis/proton-pulse-web) -- from local changes through staging review to production. ## Contents - [Branch and issue workflow](#branch-and-issue-workflow) - [Overview](#overview) - [Before you start](#before-you-start) - [Step 1: make changes and pre-push](#step-1-make-changes-and-pre-push) - [Step 2: commit and push to staging](#step-2-commit-and-push-to-staging) - [Step 3: deploy to staging site](#step-3-deploy-to-staging-site) - [Step 4: review on staging](#step-4-review-on-staging) - [Step 5: open a PR and merge to main](#step-5-open-a-pr-and-merge-to-main) - [Step 6: promote to production](#step-6-promote-to-production) - [Full pipeline deploy](#full-pipeline-deploy) - [Staging deploy paths (pick the smallest one that fits)](#staging-deploy-paths-pick-the-smallest-one-that-fits) - [CI workflows](#ci-workflows) - [Skipping staging](#skipping-staging) - [Emergency overrides](#emergency-overrides) --- ## 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/` 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/ && 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/` ### 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/` branch merged into `staging`). 3. Reference the issue in the commit: `fix: ... (closes #N)` Branch naming: `feature/` for features, `fix/` 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](#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: ```bash gh auth status ``` Make sure you are on the `staging` branch before making changes: ```bash git checkout staging ``` --- ## Step 1: make changes and pre-push After editing JS or CSS, run: ```bash 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 ```bash git add git commit -m "feat: ..." git push origin staging ``` Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): `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: ```bash 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): ```bash 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](#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`: ```bash 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: ```bash 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): ```bash 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](#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): ```bash 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. ```mermaid flowchart TD A[Change on staging] --> B{Touches JS / CSS / HTML only?} B -- yes --> C[make gh-staging
~2 min
shell only, staging reads prod data] B -- no --> D{Touches finalize.py or
search-index / recent-reports /
stats / most_played shape only?} D -- yes --> E[make gh-staging-finalize
~5 min
skip probe + build,
restore prod chunk state,
rerun finalize + stats,
push to staging repo] D -- no --> F[make gh-staging-pipeline
~30 min
full pipeline against staging,
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 ``` 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](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-` 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): ```bash 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.