From ac8b519a75ad46333bf28f6a039853ba6f946b1b Mon Sep 17 00:00:00 2001 From: Ben Richards Date: Thu, 8 Oct 2026 21:55:40 +0300 Subject: [PATCH 1/2] ci: build docs with the shared GitHub-ActionTemplates docs workflow --- .github/workflows/ci.yml | 2 +- .github/workflows/docs-ci.yml | 152 ------------------------------ .github/workflows/docs-deploy.yml | 121 ------------------------ .github/workflows/docs.yml | 23 +++++ .github/workflows/host-image.yml | 4 +- Dockerfile | 13 +-- RELEASE.md | 6 +- agent.md | 11 ++- build/ConvertTo-Changelog.ps1 | 95 ------------------- src/host/README.md | 2 +- 10 files changed, 43 insertions(+), 386 deletions(-) delete mode 100644 .github/workflows/docs-ci.yml delete mode 100644 .github/workflows/docs-deploy.yml create mode 100644 .github/workflows/docs.yml delete mode 100644 build/ConvertTo-Changelog.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2411c71..2384e3ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI # Branches are covered by `pull_request`; `main` by `push`. Restricting push to # main is what stops a branch with an open PR producing two runs of this -# workflow -- the same approach the installed docs-ci.yml takes, and the reason +# workflow -- the same approach docs.yml takes, and the reason # it needs no concurrency group either. # # An earlier version triggered on every push *and* pull_request, deduplicated by diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml deleted file mode 100644 index cf1049f4..00000000 --- a/.github/workflows/docs-ci.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: Docs CI - -# Everything documentation needs except deploy: check the authored Markdown, -# then build the combined site (docs + landing page) to prove it still builds. -# No path filters -- both jobs are meant to be required status checks, and a -# required check that never runs leaves a pull request permanently blocked. -# -# The gate and the build are not the same check. Docusaurus fails on unresolved -# routes inside the site; the gate covers what the site build never sees -- -# README.md, which is not part of the site at all, plus relative link targets, -# heading anchors, product-name casing, and drift between a generated file and -# its source. Neither one checks that the landing page (site/) still merges -# cleanly onto the docs build -- that is what the verify job's extra steps are -# for, so a PR proves the deploy path before it can run for real. - -"on": - pull_request: - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - packages: read - -defaults: - run: - shell: pwsh - -jobs: - # --- DocumentationGate:start --- - # The start/end markers are load-bearing: setup-docs.ps1 locates this exact - # block by them to remove the job entirely when -SkipGate is passed. Keep - # them if you edit this job by hand. - documentation: - name: Documentation links and terminology - # Skipped on push to main, same as the verify job below: a push only - # reaches main through a PR where this already ran as a required - # check, so branch protection is what makes this skip safe rather - # than a silent gap. - if: github.event_name != 'push' - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v7 - - # verification: true - - name: Validate Markdown links, terminology, and generated files - run: ./build/Test-Documentation.ps1 - # --- DocumentationGate:end --- - - verify: - name: Verify Documentation Build - # Skipped on push to main: docs-deploy.yml builds and deploys there, so - # building again here would duplicate work every deploy performs anyway. - if: github.event_name != 'push' - runs-on: ubuntu-latest - # Run inside the published base image, which already carries the - # template under /template with node_modules and PowerShell installed. - container: - image: ghcr.io/the-running-dev/docs-template:latest - credentials: - username: ${{ github.repository_owner }} - password: ${{ secrets.REGISTRY_TOKEN || github.token }} - - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - # Full history, not the default shallow clone -- the changelog step - # below reads the whole `git log`. - fetch-depth: 0 - - # Regenerated fresh for every build, never committed -- replaces the old - # recurring "chore: update changelog" pull request. `docs/docs/engine/ - # CHANGELOG.md` is still committed as a static fallback for anyone reading - # the repository directly on GitHub; only the deployed site is guaranteed - # current. Local `docs.ps1` preview still shows the committed (possibly - # stale) copy -- its Docker build context is `docs/`, which cannot reach - # this repo-root generator. - - name: Regenerate changelog (build-only, not committed) - run: | - # This job runs inside a container, so the checkout is owned by a - # different uid than the one running it -- git refuses to touch it - # ("dubious ownership") until explicitly trusted. - git config --global --add safe.directory "$env:GITHUB_WORKSPACE" - $body = & ./build/ConvertTo-Changelog.ps1 -RepoRoot . -RepositorySlug '${{ github.repository }}' - $docsPage = @('---', 'slug: changelog', '---', '') + $body - Set-Content -Path 'docs/docs/engine/CHANGELOG.md' -Value $docsPage -Encoding utf8NoBOM - - # verification: true - - name: Build documentation - run: /template/scripts/docs-build.ps1 -SourceDocs ./docs -OutputPath artifacts/docs - - - name: Cache Alpine Chromium packages - uses: actions/cache@v4 - with: - path: /tmp/apk-cache - key: alpine-chromium-${{ runner.arch }}-v1 - - # The landing page is a second, independent project (site/) sharing - # this one GitHub Pages deployment: it serves "/", the docs serve - # "/docs". The base image already has Node -- no setup-node needed. - # - # `check` runs format:check, lint, typecheck, the jsdom suite, the - # real-browser suite (W65), and the build (site/package.json), not - # just the build. A bare `build` would let a broken component - # test, a real-browser regression, or a missing Open Graph tag - # merge and deploy as long as the bundle still compiled -- `check` - # is what a contributor runs locally before pushing, made a - # required step here too, not a weaker CI-only substitute for it. - # The real-browser suite needs Chromium installed first -- it is - # not part of the base image, unlike Node. `playwright install - # --with-deps` shells out to apt-get, but this container - # (docs-template) is Alpine, so that step just fails outright - # ("apt-get: not found"). Install Alpine's own `chromium` package - # instead and point vitest.browser.config.ts at it via - # PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH. - # verification: true - - name: Build and verify landing page - env: - PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/chromium-browser - run: | - mkdir -p /tmp/apk-cache - apk add --cache-dir /tmp/apk-cache chromium - npm --prefix src/engine ci - npm --prefix src/engine run build - npm --prefix site ci - npm --prefix site run check - - # Overlays the landing page onto the docs build -- overwrites the - # docs build's generated-from-README root index.html, merges in - # the landing page's assets, and refuses to proceed if anything - # under artifacts/docs/docs/ changed. The package-backed `merge` - # command (subzerodev-platform-ui-landing-page@0.2.0), invoked via - # the site/package.json script site/scripts/verify-merge.mjs - # already proved above in `npm --prefix site run check`. - # verification: true - - name: Merge landing page into documentation build - run: npm --prefix site run merge - - # Run the same archiving step docs-deploy.yml uses, so the deploy - # path is covered before merge rather than after it. This action - # tars the site with `tar --hard-dereference`, which fails outright - # on an image without GNU tar. Uploading an artifact publishes - # nothing; deploy-pages does that, and it is not in this job. - - name: Archive Pages artifact (verify deploy path) - uses: actions/upload-pages-artifact@v5 - with: - path: artifacts/docs diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml deleted file mode 100644 index 1fe05669..00000000 --- a/.github/workflows/docs-deploy.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Docs Deploy - -# Build the documentation and deploy it to GitHub Pages. Triggers directly on -# push to main -- there is no longer a caller workflow multiplexing verify vs. -# deploy, so each half of that split (docs-ci.yml, this file) carries its own -# triggers and only the permissions it actually needs. Deploy is the only job -# that needs pages/id-token, which is why it stayed a separate file rather than -# folding into docs-ci.yml: a single workflow can never grant a job more -# permission than the workflow itself declares, so combining them would hand -# the gate and verify jobs credentials they have no use for. -"on": - push: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - packages: read - pages: write - id-token: write - -concurrency: - group: github-pages - cancel-in-progress: false - -defaults: - run: - shell: pwsh - -jobs: - deploy: - name: Build and Deploy Documentation - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - # Run the build inside the published base image (template under /template - # with node_modules and PowerShell installed). - container: - image: ghcr.io/the-running-dev/docs-template:latest - credentials: - username: ${{ github.repository_owner }} - password: ${{ secrets.REGISTRY_TOKEN || github.token }} - - steps: - - name: Check out Repository - uses: actions/checkout@v7 - with: - # Full history, not the default shallow clone -- the changelog step - # below reads the whole `git log`. - fetch-depth: 0 - - - name: Configure GitHub Pages - uses: actions/configure-pages@v6 - - # Regenerated fresh for every deploy, never committed -- replaces the old - # recurring "chore: update changelog" pull request. See the identical step - # in docs-ci.yml's verify job for the full rationale. - - name: Regenerate changelog (build-only, not committed) - run: | - # This job runs inside a container, so the checkout is owned by a - # different uid than the one running it -- git refuses to touch it - # ("dubious ownership") until explicitly trusted. - git config --global --add safe.directory "$env:GITHUB_WORKSPACE" - $body = & ./build/ConvertTo-Changelog.ps1 -RepoRoot . -RepositorySlug '${{ github.repository }}' - $docsPage = @('---', 'slug: changelog', '---', '') + $body - Set-Content -Path 'docs/docs/engine/CHANGELOG.md' -Value $docsPage -Encoding utf8NoBOM - - - name: Build Documentation - run: /template/scripts/docs-build.ps1 -SourceDocs ./docs -OutputPath artifacts/docs - - # The landing page is a second, independent project (site/) sharing - # this one GitHub Pages deployment: it serves "/", the docs serve - # "/docs". The base image already has Node -- no setup-node needed. - # - # `check`, not a bare build -- see docs-ci.yml's verify job, which - # runs the same command. This job is the one that actually runs on - # a push to main (verify skips push events), so it is also the - # last thing standing between a broken landing page and a real - # deploy if a check was ever bypassed getting there. - - name: Cache Alpine Chromium packages - uses: actions/cache@v4 - with: - path: /tmp/apk-cache - key: alpine-chromium-${{ runner.arch }}-v1 - - # The real-browser suite needs Chromium installed first -- it is - # not part of the base image, unlike Node. `playwright install - # --with-deps` shells out to apt-get, but this container - # (docs-template) is Alpine, so that step just fails outright - # ("apt-get: not found"). Install Alpine's own `chromium` package - # instead and point vitest.browser.config.ts at it via - # PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH -- identical to docs-ci.yml's - # verify job; that copy proved the fix before it landed here. - - name: Build and verify landing page - env: - PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: /usr/bin/chromium-browser - run: | - mkdir -p /tmp/apk-cache - apk add --cache-dir /tmp/apk-cache chromium - npm --prefix src/engine ci - npm --prefix src/engine run build - npm --prefix site ci - npm --prefix site run check - - # Overlays the landing page onto the docs build -- the package-backed - # `merge` command (subzerodev-platform-ui-landing-page@0.2.0), invoked - # via the same site/package.json script docs-ci.yml's verify job runs - # and proves on every PR before this ever runs here on push to main. - - name: Merge landing page into documentation build - run: npm --prefix site run merge - - - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@v5 - with: - path: artifacts/docs - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..da78e51b --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,23 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +jobs: + docs: + permissions: { contents: read, pages: write, id-token: write } + uses: The-Running-Dev/GitHub-ActionTemplates/.github/workflows/docs.yml@v0 + with: + output: artifacts/docs + changelog: docs/docs/engine/CHANGELOG.md + changelog-front-matter: 'slug: changelog' + pre-build: ./build/Test-Documentation.ps1 + # The landing page is built, browser-tested and merged into the documentation build. + node-project: site + node-scripts: check merge + node-dependencies: src/engine + browser: true diff --git a/.github/workflows/host-image.yml b/.github/workflows/host-image.yml index 0247b035..5a38b419 100644 --- a/.github/workflows/host-image.yml +++ b/.github/workflows/host-image.yml @@ -59,8 +59,8 @@ jobs: uses: docker/setup-buildx-action@v3 # Needed to pull ghcr.io/the-running-dev/docs-template, the base image the docs - # stage builds from -- docs-ci.yml and docs-deploy.yml authenticate to it the same - # way. Not needed to *push* anything; this job never publishes. + # stage builds from. (docs.yml no longer needs it: the shared docs workflow builds + # on the build-agent image.) Not needed to *push* anything; this job never publishes. - name: Log in to GHCR (pull docs-template) uses: docker/login-action@v3 with: diff --git a/Dockerfile b/Dockerfile index 9b55eae3..7483c6b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,19 +5,19 @@ # 1. the standalone landing page (site/dist); # 2. the Docusaurus documentation, through the repository's supported template path; # 3. the protected merge of the two (§4 step 3 -- the package-backed `merge` command -# site/package.json runs, the same command docs-ci.yml's verify job proves on every PR); +# site/package.json runs, the same command docs.yml's Build job proves on every PR); # 4. the ASP.NET product host, composed with the one pinned, released # SubZeroDev.Platform.Hosting package (§3 -- no sibling checkout, no floating range); # 5. a runtime stage carrying only the published host and the verified combined # artifact -- no Node, no npm cache, no source tree, no build tooling, no # registry credential (§5, §6). # -# CHANGELOG.md is not regenerated inside this build (docs-deploy.yml's own job does that +# CHANGELOG.md is not regenerated inside this build (docs.yml's changelog step does that # from full git history, which this build intentionally does not clone for). The # committed docs/docs/engine/CHANGELOG.md -- already documented as a static fallback for # anyone reading the repository directly -- ships instead. That is the one place this # image's docs subtree can differ from a fresh GitHub Pages deploy; every route the site -# actually serves is otherwise the same combined artifact docs-deploy.yml produces. +# actually serves is otherwise the same combined artifact docs.yml produces. ######################################################################################## # site -- the engine package build the landing page needs, then the landing page itself. @@ -43,9 +43,10 @@ COPY docs/docs/engine/TODO.md docs/docs/engine/TODO.md RUN npm --prefix site run build ######################################################################################## -# docs -- the Docusaurus build, through the same supported template path docs-ci.yml and -# docs-deploy.yml both use. The base image already carries /template with node_modules -# and pwsh installed; docs-build.ps1 overlays ./docs over it and runs the real build. +# docs -- the Docusaurus build, through the docs-template image's supported template path +# (docs.yml builds the same overlay on the build-agent image instead). The base image +# already carries /template with node_modules and pwsh installed; docs-build.ps1 overlays +# ./docs over it and runs the real build. ######################################################################################## FROM ghcr.io/the-running-dev/docs-template:latest AS docs WORKDIR /workspace diff --git a/RELEASE.md b/RELEASE.md index 69e19273..d7e8cca3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -129,9 +129,9 @@ client, MCP server, site, docs, host) identify the same candidate SHA. ## 5. Release-note source -The changelog is generated from `main`'s merge history by `build/ConvertTo-Changelog.ps1` into -`docs/docs/engine/CHANGELOG.md`. There is no hand-maintained changelog and this checklist does -not start one. +The changelog is generated from `main`'s merge history into `docs/docs/engine/CHANGELOG.md` by the +docs workflow's `changelog` input (GitHub-ActionTemplates `actions/changelog`). There is no +hand-maintained changelog and this checklist does not start one. The immutable release-note source for `0.11.0` is the commit range: diff --git a/agent.md b/agent.md index f162537a..6dbf568b 100644 --- a/agent.md +++ b/agent.md @@ -136,9 +136,9 @@ README, whose status is deliberately coarse. open; add new gaps there as full entries. - **Engine suite runs under vitest** — `npm test` from `src/engine/` is the count; a number written here goes stale, as "15 tests" did for a suite that long outgrew it. CI runs it: - `.github/workflows/ci.yml` (`engine`), plus the installed `docs-ci.yml` and `docs-deploy.yml`. - The three pull-request checks are required on `main`; deploy is not, since it runs only on - push to `main` (`TODO.md` W0, closed). + `.github/workflows/ci.yml` (`engine`), plus `docs.yml`, the caller of the shared reusable + docs workflow. The pull-request checks (`engine` and `docs / Build`) are required on `main`; + `docs / Deploy` is not, since it runs only on push to `main` (`TODO.md` W0, closed). - **The docs-site base image is verified for the current W0 baseline.** `docs.ps1` builds on the public `ghcr.io/the-running-dev/docs-template`; the installed image uses Docusaurus 3, port 3000, and the local `sidebar.ts`. A production build passed with no @@ -146,8 +146,9 @@ README, whose status is deliberately coarse. now serves the generated homepage (`docs/docs/index.md`); `/docs/engine/vision` is the first spec page beneath it. **Once `feature/landing-page` merges, the bare domain `/` stops being `docs/src/pages/index.md` (generated from README) and becomes the standalone landing - page (`site/`) instead** — `build/Merge-LandingPage.ps1`, wired into both `docs-ci.yml` and - `docs-deploy.yml`, overlays it on every build. `/docs/` and everything beneath it is + page (`site/`) instead** — the package-backed `merge` (`npm --prefix site run merge`), which + `docs.yml` runs through `node-project: site` and `node-scripts: check merge`, overlays it on + every build. `/docs/` and everything beneath it is unaffected; only the site root changes owner. - **Two link checks, and between them everything is now gated.** `build/Test-Documentation.ps1` hard-fails on relative links and heading anchors — that is the one that catches a doc diff --git a/build/ConvertTo-Changelog.ps1 b/build/ConvertTo-Changelog.ps1 deleted file mode 100644 index 9353bd37..00000000 --- a/build/ConvertTo-Changelog.ps1 +++ /dev/null @@ -1,95 +0,0 @@ -<# -.SYNOPSIS - Builds the changelog body from main's merge history. - -.DESCRIPTION - Every commit on `main` in this repository is a squash-merged pull request, so - `git log` already IS the changelog -- one entry per shipped PR, newest first, - linking back to the PR itself. No manual maintenance, no second source of - truth that can drift from what actually merged. - - Returns the changelog BODY only, as an array of lines -- no front matter. The - two callers that write it (the Docusaurus page and the repository-root copy - GitHub's own UI recognizes) wrap the same body differently, so front matter - doesn't belong in the generator itself. - - A commit whose subject contains "update changelog" (case-insensitive, anywhere - in the subject rather than an exact prefix) is skipped -- that is this - generator's own regeneration commit, once `.github/workflows/changelog.yml` - merges one, and it would otherwise show up inside the changelog it produced. - Matched loosely on purpose: a merge commit's exact subject depends on how it - was merged and whether a reviewer edited the pre-filled title, so an exact - prefix match is one retitle away from missing it. - -.PARAMETER RepoRoot - Path to the git repository root. Needs full history (not a shallow clone). - -.PARAMETER RepositorySlug - "/", used to build pull-request links. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string] $RepoRoot, - - [Parameter(Mandatory)] - [string] $RepositorySlug -) - -$ErrorActionPreference = 'Stop' - -# A control character no commit subject will contain, so splitting on it can't -# be confused by punctuation a real subject might use. -$separator = [char]0x1f -$format = "%ad$separator%s" - -$rawLog = & git -C $RepoRoot log --date=short --pretty=format:$format HEAD -if ($LASTEXITCODE -ne 0) { - throw 'git log failed -- is this running inside a git checkout with full history (fetch-depth: 0)?' -} - -# Markdown-escapes a commit subject before it's embedded as link text or a -# plain list item. '<' and '>' are HTML-entity-encoded first -- otherwise a -# literal ""-style placeholder in a commit subject (a PR title showing -# a path template, for instance) is parsed by MDX/Docusaurus as an unclosed -# JSX/HTML tag and fails the documentation build. Then an unescaped '[' or -# ']' in a PR title would otherwise corrupt the surrounding link syntax -# (backslash escaped first, so escaping the brackets afterwards can't double -# up on a subject that already contains a literal backslash). The entity -# encoding runs before the backslash/bracket escaping and introduces no -# backslash, bracket, or ampersand-based sequence itself, so it cannot be -# double-escaped by the steps that follow it. -function ConvertTo-EscapedMarkdown { - param([Parameter(Mandatory)][string] $Text) - $Text.Replace('<', '<').Replace('>', '>').Replace('\', '\\').Replace('[', '\[').Replace(']', '\]') -} - -$entryLines = foreach ($entry in ($rawLog -split "`n")) { - if ([string]::IsNullOrWhiteSpace($entry)) { continue } - - $parts = $entry -split [regex]::Escape([string]$separator), 2 - $date = $parts[0] - $subject = $parts[1] - - if ($subject -match '(?i)update changelog') { continue } - - $escapedSubject = ConvertTo-EscapedMarkdown -Text $subject - $prMatch = [regex]::Match($subject, '\(#(?\d+)\)\s*$') - if ($prMatch.Success) { - $prUrl = "https://github.com/$RepositorySlug/pull/$($prMatch.Groups['pr'].Value)" - "- **$date** — [$escapedSubject]($prUrl)" - } - else { - "- **$date** — $escapedSubject" - } -} - -@( - '# Changelog' - '' - 'One entry per merged pull request, newest first — generated from the merge history' - 'of `main`, not maintained by hand. This project squash-merges every pull request, so' - '`git log` already is the changelog; regenerating it is strictly more reliable than a' - 'second, hand-kept copy.' - '' -) + $entryLines diff --git a/src/host/README.md b/src/host/README.md index 783fd5a7..cd88eea6 100644 --- a/src/host/README.md +++ b/src/host/README.md @@ -25,7 +25,7 @@ The host serves whatever is in its `wwwroot`. It refuses to start if `index.html `roadmap/index.html`, or `docs/index.html` is missing — that is the same guard the container image's build proves red in CI (`StaticArtifact` in `Program.cs`). To run against a real artifact, build the combined site+docs output first -(`npm --prefix site run merge`, the same package-backed merge `docs-ci.yml` and `docs-deploy.yml` use) +(`npm --prefix site run merge`, the same package-backed merge `docs.yml` uses) and copy it into `SubZeroDev.GameEngine.Host/wwwroot/` before `dotnet run`. The container image (repository-root `Dockerfile`) does this assembly for you in one From 9a24e2293d75bbe3402a6a6444c3254a1b8500c2 Mon Sep 17 00:00:00 2001 From: Ben Richards Date: Thu, 8 Oct 2026 22:20:07 +0300 Subject: [PATCH 2/2] fix(site): give header nav links a fixed line height so their hit area does not depend on the fallback font --- site/src/site.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/site.css b/site/src/site.css index 8956f55d..90c36bdc 100644 --- a/site/src/site.css +++ b/site/src/site.css @@ -50,6 +50,9 @@ footer { .site-header nav a { color: var(--landing-muted); font-size: 0.82rem; + /* A fixed line height keeps the link's box the same in every fallback font; + `normal` comes from the font's metrics and is under 16px in some. */ + line-height: 1.5; text-underline-offset: 0.3em; }