diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..aed6237 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,51 @@ +name: Deploy to GitHub Pages + +# Publish the demonstrator's static surfaces (component gallery + wireframes, +# assembled by `npm run build:site`) to the gh-pages branch on every push to +# main. Per-PR previews live under pr-preview/ on the same branch and are +# preserved here via clean-exclude, so a main deploy never wipes open previews. + +on: + push: + branches: [main] + workflow_dispatch: + +# Serialise deploys to the same branch; do not cancel an in-flight publish. +concurrency: + group: pages-deploy + cancel-in-progress: false + +permissions: + contents: write + +jobs: + deploy: + name: Build and deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Assemble static site + run: npm run build:site + env: + GITHUB_SHA: ${{ github.sha }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + + - name: Deploy to gh-pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: site + # Preserve per-PR preview subtrees when refreshing the main site. + clean-exclude: pr-preview/ diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..06a5d0d --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,57 @@ +name: PR Preview + +# For each pull request, publish a preview of the assembled static site to +# gh-pages under pr-preview/pr-/ and link it from a PR comment. When the PR +# is merged or closed, the preview subtree is removed and the comment updated. +# +# Uses GITHUB_TOKEN, which has write access only for same-repository branches; +# previews are therefore not produced for pull requests from forks. + +on: + pull_request: + types: [opened, reopened, synchronize, closed] + +# One preview build per PR at a time; newer pushes supersede in-flight builds. +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + preview: + name: Build and deploy preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Skip the build on close: the deploy step only tears the preview down. + - name: Set up Node.js + if: github.event.action != 'closed' + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + if: github.event.action != 'closed' + run: npm ci + + - name: Assemble static site + if: github.event.action != 'closed' + run: npm run build:site + env: + GITHUB_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + + - name: Deploy / tear down preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: site + umbrella-dir: pr-preview + # Deploy on open/reopen/synchronize; remove on close/merge. + action: auto diff --git a/.gitignore b/.gitignore index 6c557dd..2640e54 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,6 @@ vite.config.ts.timestamp-* # Claude Code local settings (may contain machine-specific state) .claude/settings.local.json + +# Assembled static site for GitHub Pages (generated by npm run build:site) +site/ diff --git a/package.json b/package.json index 55b9a8a..6fd35d8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "vitest run", "typecheck": "tsc --noEmit", "gallery": "tsx scripts/build-gallery.ts", + "build:site": "npm run gallery && tsx scripts/build-site.ts", "bench": "tsx scripts/bench-canonical.ts" }, "dependencies": { diff --git a/scripts/build-site.ts b/scripts/build-site.ts new file mode 100644 index 0000000..627ad67 --- /dev/null +++ b/scripts/build-site.ts @@ -0,0 +1,89 @@ +/** + * Assembles the publishable static site into `site/` for GitHub Pages. + * + * ASSAY has no compiled SPA — the demonstrator's shippable surfaces are static + * HTML: the fixture-backed component gallery (SPEC-14, `npm run gallery`) and + * the role-surface wireframes (a peer of the canonical set). This script + * gathers those into one directory with a small landing page, so the whole + * thing can be served from a single Pages root (or a per-PR preview subtree). + * + * Run via `npm run build:site` (which regenerates the gallery first). Output is + * generated and git-ignored; the CI workflows publish it, nothing is committed. + * + * Build metadata (commit, ref, PR number) is read from the environment when + * present so the landing page can label a preview honestly — no value is + * invented when a variable is absent. + */ +import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const root = new URL('../', import.meta.url); +const site = new URL('site/', root); +const gallery = new URL('gallery/', site); + +mkdirSync(fileURLToPath(gallery), { recursive: true }); + +// The demonstrator's two static surfaces, copied verbatim. +copyFileSync( + fileURLToPath(new URL('docs/assets/gallery/index.html', root)), + fileURLToPath(new URL('index.html', gallery)), +); +copyFileSync( + fileURLToPath(new URL('docs/assay-ui-wireframes.html', root)), + fileURLToPath(new URL('wireframes.html', site)), +); + +const esc = (s: string): string => + s.replace(/&/g, '&').replace(//g, '>'); + +// Honest build labelling: only render what the environment actually gives us. +const sha = process.env.GITHUB_SHA?.slice(0, 7); +const ref = process.env.GITHUB_REF_NAME; +const pr = process.env.PR_NUMBER; +const server = process.env.GITHUB_SERVER_URL ?? 'https://github.com'; +const repo = process.env.GITHUB_REPOSITORY ?? 'DeepBlueCLtd/assay'; + +const badge = pr + ? `preview of PR #${esc(pr)}` + : ref + ? `${esc(ref)}` + : 'local build'; +const commitLink = sha + ? ` · ${esc(sha)}` + : ''; + +const card = ( + href: string, + title: string, + body: string, + tag: string, +): string => + ` +
${tag}
+
${title} →
+
${body}
+
`; + +const html = ` + + + + + +ASSAY — demonstrator preview + + +
+
${badge}${commitLink}
+

ASSAY

+

Assessment Semantics & Scenario Analysis — a docs-first demonstrator. These are the demonstrator's live static surfaces, rendered over the Meridian Archipelago vignette (engineered fiction, ASSAY-DEC-8). No service, no plan, and no real operational picture sits behind them.

+${card('gallery/', 'Component gallery', "The demonstrator's actual components — band pills, provenance chips, the minimal S1 knowledge table and its four discipline moments — rendered over the real fixtures (SPEC-05/SPEC-14).", 'live components')} +${card('wireframes.html', 'UI wireframes', 'The four role surfaces on Meridian data, at the D+2 tableau moment (K9 supersedes K5; K12 contested). Frozen vignette identifiers only.', 'role surfaces')} +

Banded honesty holds throughout: no scalar from an assessed source appears unbanded (constitution II). Identifiers are frozen per assay-vignette.md §8.

+
+ + +`; + +writeFileSync(fileURLToPath(new URL('index.html', site)), html); +console.log(`wrote site/ (${badge})`);