diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 10c71cb..2cbf0a3 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -1,5 +1,16 @@ name: Deploy web overlay (lab.usersfirst.games) +# Publishes the production site to the ROOT of the `gh-pages` branch. We use a +# branch-based Pages source (rather than the GitHub-Actions Pages source) so +# the per-PR previews from pr-preview.yml — which live under gh-pages +# `pr-preview/pr-/` via rossjrw/pr-preview-action — can coexist with prod on +# the same site. `keep_files: true` means a prod deploy never wipes the +# `pr-preview/` umbrella (or vice-versa). +# +# ⚠️ One-time repo setting required: Settings → Pages → "Build and deployment" +# → Source = "Deploy from a branch" → Branch = `gh-pages` / `(root)`. +# The custom domain (lab.usersfirst.games) is preserved via the CNAME file +# that build-web.js writes into _site (and the `cname:` below). on: push: branches: [main] @@ -13,23 +24,18 @@ on: - '.github/workflows/deploy-pages.yml' workflow_dispatch: -# Required for the GitHub Actions Pages deployment. +# Pushing to the gh-pages branch needs write to repo contents. permissions: - contents: read - pages: write - id-token: write + contents: write -# One in-flight deploy at a time; don't cancel a running publish. +# Serialize gh-pages writes so a prod deploy and a preview deploy don't race. concurrency: - group: pages + group: gh-pages-write cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -46,9 +52,12 @@ jobs: node scripts/stamp-build-version.js apps/overlay/src/index.html web/index.html - name: Assemble _site (landing + /overlay + CNAME) run: node scripts/build-web.js - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 + - name: Publish to gh-pages root + uses: peaceiris/actions-gh-pages@v4 with: - path: _site - - id: deploy - uses: actions/deploy-pages@v5 + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./_site + # Preserve the pr-preview/ umbrella (and anything else) on gh-pages. + keep_files: true + # Re-assert the custom domain on every deploy so it survives keep_files. + cname: lab.usersfirst.games diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..94833fd --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,221 @@ +name: PR preview (web + desktop) + +# Per-PR preview so a change can be tested fully and in isolation (issue #77), +# mirroring the PR-preview workflow in petegordon/tandemonium but extended with +# the desktop installers: +# +# • Web preview — the browser overlay deployed to the gh-pages `pr-preview/` +# umbrella at https://lab.usersfirst.games/pr-preview/pr-/ +# (overlay at …/pr-preview/pr-/overlay/). Torn down on close. +# • Desktop — Windows .exe + macOS .dmg/zip published as a per-PR +# *prerelease* tagged `pr-`, deleted when the PR closes. +# • One sticky comment links the web URL and the desktop downloads, refreshed +# on every push. +# +# Scope: INTERNAL branches only. Fork PRs get a read-only GITHUB_TOKEN (and no +# secrets), so they can't publish to gh-pages / Releases anyway — every job is +# guarded on the head repo being this repo to avoid noisy guaranteed failures. +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +# Supersede an in-flight preview when the PR is pushed again; each PR is its own +# group so PRs never cancel each other. +concurrency: + group: pr-preview-${{ github.event.number }} + cancel-in-progress: true + +permissions: + contents: write # gh-pages preview branch + per-PR prerelease + pull-requests: write # sticky preview comment + +jobs: + # ── Web preview → gh-pages /pr-preview/pr-/ (and teardown on close) ── + web-preview: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + # Share the repo-wide gh-pages serialization group with deploy-pages.yml so + # a prod deploy and a preview deploy never push to gh-pages concurrently. + concurrency: + group: gh-pages-write + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + # Build steps are skipped on `closed` — the deploy action then only runs + # its teardown (removes this PR's preview dir from gh-pages). + - name: Assemble _site for the preview subpath + if: github.event.action != 'closed' + run: | + node apps/overlay/scripts/copy-workspace.js + HASH=$(git rev-parse --short HEAD) + export BUILD_STAMP="PR #${{ github.event.number }} · v.${HASH} · $(date -u +'%Y-%m-%d %H:%M UTC')" + node scripts/stamp-build-version.js apps/overlay/src/index.html web/index.html + node scripts/build-web.js + # Scope the landing page's relative URLs to the preview subpath so its + # links (e.g. → /overlay) resolve under /pr-preview/pr-/. + sed -i 's|||' _site/index.html + - name: Deploy / teardown PR web preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: ./_site/ + preview-branch: gh-pages + umbrella-dir: pr-preview + action: auto + # We post a single combined sticky comment (web + desktop) ourselves. + comment: false + + # ── Desktop installers (skipped on close) ── + build-macos: + if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: macos-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Stamp build version + shell: bash + run: | + HASH=$(git rev-parse --short HEAD) + export BUILD_STAMP="PR #${{ github.event.number }} · v.${HASH} · $(date -u +'%Y-%m-%d %H:%M UTC')" + node scripts/stamp-build-version.js apps/overlay/src/index.html + - name: Build macOS (zip + dmg) + working-directory: apps/overlay + shell: bash + run: | + node scripts/copy-three.js + node scripts/copy-workspace.js + npx electron-forge make --arch universal --targets @electron-forge/maker-zip + bash scripts/make-dmg.sh + - uses: actions/upload-artifact@v7 + with: + name: macos + if-no-files-found: error + path: | + apps/overlay/out/WebHID-Controller-Overlay.dmg + apps/overlay/out/make/zip/darwin/**/*.zip + + build-windows: + if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Stamp build version + shell: bash + run: | + HASH=$(git rev-parse --short HEAD) + export BUILD_STAMP="PR #${{ github.event.number }} · v.${HASH} · $(date -u +'%Y-%m-%d %H:%M UTC')" + node scripts/stamp-build-version.js apps/overlay/src/index.html + - name: Build Windows (squirrel + zip) + working-directory: apps/overlay + shell: bash + run: | + node scripts/copy-three.js + node scripts/copy-workspace.js + npx electron-forge make --targets @electron-forge/maker-squirrel,@electron-forge/maker-zip + - uses: actions/upload-artifact@v7 + with: + name: windows + if-no-files-found: error + path: | + apps/overlay/out/make/squirrel.windows/**/*.exe + apps/overlay/out/make/zip/win32/**/*.zip + + # ── Per-PR prerelease so the installers have stable, clickable URLs ── + prerelease: + needs: [build-macos, build-windows] + if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + path: artifacts + - name: Gather release files + run: | + mkdir -p release-assets + find artifacts -type f \( -name '*.dmg' -o -name '*.exe' -o -name '*.zip' \) -exec cp {} release-assets/ \; + ls -lh release-assets + - name: Publish PR prerelease + uses: softprops/action-gh-release@v3 + with: + tag_name: pr-${{ github.event.number }} + name: "PR #${{ github.event.number }} preview (${{ github.event.pull_request.head.sha }})" + # Prerelease + NOT latest so it never displaces the rolling + # `webhid-overlay-latest` release the landing page tracks. + prerelease: true + make_latest: false + target_commitish: ${{ github.event.pull_request.head.sha }} + files: release-assets/* + body: | + Automated **preview build** for PR #${{ github.event.number }} — for testing in isolation only. + + - **Commit:** `${{ github.event.pull_request.head.sha }}` + - **Windows:** `*-Setup.exe` (installer) or `.zip` (portable) + - **macOS:** `WebHID-Controller-Overlay.dmg` or `.zip` (unsigned — right-click → Open on first launch) + + This prerelease is deleted automatically when the PR closes. + + # ── One combined sticky comment (web URL + desktop downloads) ── + comment: + needs: [web-preview, prerelease] + if: always() && github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Post / update preview comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-preview + message: | + ### 🔎 PR #${{ github.event.number }} preview — test it in isolation + + **🌐 Web overlay:** https://lab.usersfirst.games/pr-preview/pr-${{ github.event.number }}/overlay/ + _(landing page: https://lab.usersfirst.games/pr-preview/pr-${{ github.event.number }}/)_ + + **💻 Desktop installers (prerelease):** https://github.com/${{ github.repository }}/releases/tag/pr-${{ github.event.number }} + - **Windows** — `…-Setup.exe` (installer) or `.zip` (portable) + - **macOS** — `WebHID-Controller-Overlay.dmg` or `.zip` (unsigned: right-click → Open) + + Updated for ${{ github.event.pull_request.head.sha }}. Web preview and prerelease are torn down when this PR closes. + + # ── Teardown on close: delete the per-PR prerelease + tag, note the comment ── + cleanup: + if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - name: Delete PR prerelease and tag + uses: actions/github-script@v7 + with: + script: | + const tag = `pr-${context.issue.number}`; + try { + const rel = await github.rest.repos.getReleaseByTag({ ...context.repo, tag }); + await github.rest.repos.deleteRelease({ ...context.repo, release_id: rel.data.id }); + core.info(`Deleted release ${tag}`); + } catch (e) { + core.info(`No release ${tag} to delete (${e.status})`); + } + try { + await github.rest.git.deleteRef({ ...context.repo, ref: `tags/${tag}` }); + core.info(`Deleted tag ${tag}`); + } catch (e) { + core.info(`No tag ${tag} to delete (${e.status})`); + } + - name: Update sticky comment to "torn down" + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-preview + message: | + ### 🔎 PR #${{ github.event.number }} preview — torn down + + This PR is closed; its web preview and desktop prerelease have been removed. diff --git a/apps/overlay/README.md b/apps/overlay/README.md index 4e4c774..7d0c180 100644 --- a/apps/overlay/README.md +++ b/apps/overlay/README.md @@ -26,6 +26,17 @@ npm --workspace @usersfirst/overlay run make # ZIP + Windows Squirrel npm --workspace @usersfirst/overlay run make:dmg # macOS DMG (uses hdiutil) ``` +## PR previews (test a PR in isolation) + +Every internal PR gets a preview built by [`.github/workflows/pr-preview.yml`](../../.github/workflows/pr-preview.yml), with a single sticky comment linking: + +- **Web overlay** — deployed to `gh-pages` under `https://lab.usersfirst.games/pr-preview/pr-/overlay/`. +- **Desktop installers** — Windows `.exe` + macOS `.dmg`/zip, published as a per-PR **prerelease** tagged `pr-`. + +Both are torn down automatically when the PR closes. Previews run for branches in this repo only (fork PRs get a read-only token and can't publish). + +> **One-time setup:** the site is served from the **`gh-pages` branch** (Settings → Pages → Source = "Deploy from a branch" → `gh-pages` / `(root)`) so prod and the `pr-preview/` umbrella can share one Pages site. The custom domain is preserved via the `CNAME` that `build-web.js` writes. + ## How it talks to the packages | File | Imports from |