diff --git a/.github/workflows/deploy-frontend-pages.yml b/.github/workflows/deploy-frontend-pages.yml index d487d343..f2b2229d 100644 --- a/.github/workflows/deploy-frontend-pages.yml +++ b/.github/workflows/deploy-frontend-pages.yml @@ -7,6 +7,15 @@ on: types: - completed workflow_dispatch: + inputs: + channel: + description: Channel to deploy. Stable is an explicit promotion from trusted main. + required: true + default: dev + type: choice + options: + - dev + - stable env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -63,11 +72,54 @@ jobs: - name: Install run: npm ci - - name: Build + - name: Prepare Pages dist + run: rm -rf dist && mkdir -p dist + + - name: Build dev channel + env: + VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }} + VITE_PHASERFORGE_DEPLOY_CHANNEL: dev + VITE_E2E_TEST_BRIDGE: '1' + run: npm run build -- --outDir dist/dev + + - name: Build stable channel + if: github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' env: VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL }} + VITE_PHASERFORGE_DEPLOY_CHANNEL: stable VITE_E2E_TEST_BRIDGE: '1' - run: npm run build + run: | + npm run build -- --outDir dist/stable + node -e "const fs=require('fs'); fs.writeFileSync('dist/stable/version.json', JSON.stringify({ channel: 'stable', commit: process.env.GITHUB_SHA, promotedAt: new Date().toISOString() }, null, 2) + '\n')" + + - name: Preserve current stable channel + if: github.event_name != 'workflow_dispatch' || inputs.channel != 'stable' + run: | + set -euo pipefail + pages_root_url="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/" + pages_stable_url="${pages_root_url}stable/" + mkdir -p dist/stable + fetch_pages_path() { + local url="$1" + local target="$2" + wget \ + --recursive \ + --level=2 \ + --page-requisites \ + --no-parent \ + --no-host-directories \ + --cut-dirs=1 \ + --directory-prefix="$target" \ + "$url" + } + fetch_pages_path "$pages_stable_url" dist || true + if [ ! -f dist/stable/index.html ]; then + echo "No existing /stable/ channel found; seeding stable from the currently published root editor." + mkdir -p /tmp/phaserforge-pages-root + fetch_pages_path "$pages_root_url" /tmp/phaserforge-pages-root + cp -a /tmp/phaserforge-pages-root/. dist/stable/ + fi + test -f dist/stable/index.html - name: Build docs run: npm run docs:build @@ -75,6 +127,12 @@ jobs: - name: Stage docs under /docs run: npm run docs:stage + - name: Create root redirect + run: npm run pages:root + + - name: Verify Pages artifact shape + run: npm run pages:verify + - name: Upload Pages artifact uses: actions/upload-pages-artifact@v5 with: diff --git a/.plans/stable-dev-github-pages-deployment-plan.md b/.plans/stable-dev-github-pages-deployment-plan.md new file mode 100644 index 00000000..42bf617e --- /dev/null +++ b/.plans/stable-dev-github-pages-deployment-plan.md @@ -0,0 +1,78 @@ +# Stable / Dev GitHub Pages Deployment Plan + +Status: implemented and locally verified. + +Current editor URL: + +- `https://bcorfman.github.io/phaserforge/` + +Target editor URLs: + +- Stable: `https://bcorfman.github.io/phaserforge/stable/` +- Dev: `https://bcorfman.github.io/phaserforge/dev/` +- Root: `https://bcorfman.github.io/phaserforge/` must not host an editor build. + +## Durable Rule + +The public editor should deploy only to explicit stable and development channels. `/stable/` serves the user-safe release build. `/dev/` can track active development. The root `/phaserforge/` must not host an editor build; it may only redirect to `/stable/` or show a minimal channel chooser. No additional preview, release, or per-branch URL scheme is required for this increment. + +## Non-Goals + +- Do not change the existing game publish method. +- Do not add per-PR preview deployments. +- Do not introduce a release archive beyond `/stable/` and `/dev/`. +- Do not require users to understand Git branches or build IDs. + +## Proposed Channel Semantics + +- `/dev/` updates from `main` after CI passes. +- `/stable/` updates only from an explicit stable promotion action. +- The root `/phaserforge/` should either redirect to `/stable/` or show a tiny channel chooser, with `/stable/` preferred as the default user-safe destination. It must not contain a runnable editor bundle. +- Both channel builds should use relative asset paths or channel-aware Vite base paths so direct reloads work under their subdirectories. +- Persistence should use a hybrid channel model: `/stable/` keeps the existing production persistence namespace so saves from the former root URL carry forward automatically, while `/dev/` uses a separate development namespace unless an explicit compatibility gate says sharing is safe. + +## Phase 1 — Deployment Inventory + +- [x] Inventory the current GitHub Pages workflow in `.github/workflows/deploy-frontend-pages.yml`. +- [x] Confirm how `dist/`, docs staging, and asset paths behave under nested `/stable/` and `/dev/` paths. +- [x] Confirm whether docs remain at `/docs/` or move under one editor channel. +- [x] Identify any hard-coded `/phaserforge/` or root-relative asset URLs that would break under channel subpaths. + +## Phase 2 — Build and Stage Channels + +- [x] Build the editor once for `/dev/` and stage it under `dist/dev/`. +- [x] Build or copy the stable editor artifact under `dist/stable/`. +- [x] Add a root `index.html` redirect or chooser pointing users to `/stable/`, without staging editor JS/CSS/assets at the root. +- [x] Preserve existing docs staging without interfering with `/stable/` and `/dev/`. + +## Phase 3 — Stable Promotion + +- [x] Add an explicit workflow dispatch input or separate workflow for promoting stable. +- [x] Decide whether stable is promoted from: + - the current `main` SHA after validation; + - a Git tag; + - a named stable branch. +- [x] Ensure stable promotion does not happen automatically on every `main` push. +- [x] Record the promoted commit SHA in the Pages artifact, a small `stable/version.json`, or workflow summary. + +## Phase 4 — Dev Deployment + +- [x] Keep automatic deployment from CI-passing `main` to `/dev/`. +- [x] Make `/dev/` visibly identify itself as the development channel somewhere low-friction, without adding a disruptive workflow. +- [x] Keep `/stable/` on the current production IndexedDB/localStorage namespace so existing root saves migrate by continuity. +- [x] Move `/dev/` local persistence to a separate development namespace, or require an explicit schema compatibility gate before `/dev/` can write the shared production namespace. +- [x] Ensure cloud persistence cannot let `/dev/` corrupt `/stable/` projects without clear schema compatibility rules. + +## Phase 5 — Verification + +- [x] Add a workflow-level smoke or artifact check that `dist/stable/index.html` and `dist/dev/index.html` exist. +- [x] Add an artifact check proving the root does not contain the editor bundle entrypoints. +- [x] Run a deployed-path smoke test locally, serving the staged `dist` and visiting `/stable/` and `/dev/`. +- [x] Confirm deep reloads work on both channel URLs. +- [x] Confirm published games still use the existing publish path unchanged. + +## Open Decisions + +- Stable promotion uses a manual workflow dispatch selecting the `stable` channel from trusted `main`. +- Root `/phaserforge/` redirects immediately to `/stable/`. +- Docs stay at `/docs/`. diff --git a/.repo-memory/product-memory.md b/.repo-memory/product-memory.md index 8de0cd70..87d95914 100644 --- a/.repo-memory/product-memory.md +++ b/.repo-memory/product-memory.md @@ -20,6 +20,7 @@ Compact durable rules that should survive refactors and feature work. - The latest valid IndexedDB-backed project head wins over stale cached snapshots or legacy localStorage state. - Restore sequencing is a product contract and must remain stable. - Project history should preserve user intent across reload/rebuild, not just restoreable state. +- The public editor should deploy only to explicit stable/development channels: `/stable/` serves the user-safe release, `/dev/` can track active development, and the root must not host an editor build. ### Serialization and project data diff --git a/package.json b/package.json index 787ff6fa..9986eda6 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "docs:screenshots:app": "node --import tsx scripts/docs/capture-docs-screenshots.mjs", "docs:screenshots": "npm run docs:screenshots:storybook && npm run docs:screenshots:app", "docs:stage": "node scripts/docs/stage-docs-for-pages.mjs", + "pages:root": "node scripts/pages/create-root-index.mjs", + "pages:verify": "node scripts/pages/verify-pages-artifact.mjs", "server:dev": "node --import tsx server/src/index.ts", "server:start": "node --import tsx server/src/index.ts", "prisma:generate": "prisma generate", diff --git a/scripts/pages/create-root-index.mjs b/scripts/pages/create-root-index.mjs new file mode 100644 index 00000000..eff61cdd --- /dev/null +++ b/scripts/pages/create-root-index.mjs @@ -0,0 +1,31 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '..', '..'); + +export function buildRootIndexHtml({ target = './stable/' } = {}) { + return ` + +
+ + + +