Spec 013 — Docs previews
Status: implemented Branch: 013-docs-previews
Approved by the human on 2026-08-13, with three [NEEDS VERIFICATION] items carrying an Open verdict — V2, V3 and V4 in research.md, all properties of GitHub's pull-request UI that only a real run with the workflow in place can show. This follows spec 003, which approved SC1–SC3/SC6 the same way and settled them in a dated verification log at step 5.
Moved to implemented by the human on 2026-08-13, on a knowingly imperfect record rather than a clean one:
- P1 #2 and SC2 were amended after observation. The spec claimed one deployment entry updating in place; GitHub accumulates one per push (
research.mdV4, refuted). Freshness — the property the feature exists for — holds. - SC4 is inferred, not observed. No two pull requests have been open at once.
- SC5 is pending merge. Production publishes on the first push to
main. - SC6 passes degenerately. Branch protection is unavailable on this free private plan, so every pull request is mergeable regardless of what the publish job does. Untested against an enforcing rule.
Each is recorded in the verification log in docs/architecture/ci.md rather than smoothed over. The agent transcribed this status on the human's explicit instruction; the decision is the human's.
Status is set by the human, never by the agent. It moves draft → approved → implemented.
Problem
Every artifact this project produces is Markdown — specs, plans, tasks, docs/architecture/*, the constitution — and VitePress renders all of it into a browsable site with a generated sidebar, local search, and Mermaid diagrams. CI proves that site builds (pnpm docs:build, spec 003) but hosts nothing. To actually read a spec as rendered — diagrams drawn, cross-links resolved, sidebar in place — the reviewer must clone the branch, install, and run pnpm docs:dev locally.
That cost falls exactly where it hurts most. The workflow's whole shape is artifact, then approval gate: spec.md is committed and pushed before discovery, and each later artifact is pushed as it lands. The human is asked to approve a document at each gate, but the only convenient way to read it is as raw Markdown in a diff. A rendered spec — with its Mermaid workflow diagrams as pictures rather than fenced source — is the artifact the gate is actually about.
Spec 003 named this and deferred it deliberately: "A live PR preview environment for the docs … a browsable preview environment is a deliberately separate, later spec, because it introduces hosting, secrets, and PR commenting into what is otherwise pure verification." This is that spec. It publishes the built site per branch: a per-pull-request preview reachable from the PR page, and a stable URL for main.
User stories
Ordered by priority. Each story must be independently testable and shippable — if only P1 ships, there is still something usable.
P1 — Every pull request carries a browsable, rendered copy of its docs
As the reviewer, I want each pull request to publish its built docs site to its own URL, reachable from a View deployment control on the PR page, so that I can read a spec as rendered — Mermaid diagrams drawn, sidebar generated, search working — without cloning the branch or running a local server.
Independent test: open a pull request that adds a Markdown file containing a Mermaid fence; the PR page shows a deployment entry whose link serves that PR's site, and the new page renders there with the diagram drawn.
Acceptance scenarios
- Given a pull request whose checks pass, when CI finishes, then the PR page shows a deployment entry labelled
Docs previewwith a link, and that link serves the site built from the PR's head commit. - Given that published preview, when the pull request's branch is pushed again, then a new deployment entry is published for that commit and the branch's URL resolves to the newer build, so the preview is never stale. Amended 2026-08-13 after observation: this scenario originally claimed one entry updating in place. It does not — GitHub records a deployment per run and the pull request accumulates one entry each, with no automatic inactivation (research.md V4). The promise that survives is freshness, which holds; the claim about GitHub's display was wrong and is corrected here rather than left as a passing-looking falsehood. Amendment decided by the human.
- Given a pull request whose
checkjob fails, when CI finishes, then no deployment happens and no preview is published for that commit — a red branch never publishes. - Given two pull requests open at once, when both publish, then each has its own URL and neither overwrites the other's content.
- Given a pull request, when the deploy fails (bad token, provider outage), then the pull request remains mergeable — the required status check is unaffected by the deploy job's result.
P2 — main publishes to one stable URL
As the developer, I want every merge to main to publish the same built site to a single fixed URL, so that the project has a canonical docs address and a preview is meaningfully "the diff against what's live".
Independent test: merge a pull request that changes a docs page; the fixed production URL serves the changed page shortly after, without any manual step.
Acceptance scenarios
- Given a commit pushed to
main, when CI finishes green, then the site built from that commit is published to the project's production URL, and the deployment entry is labelledDocsrather thanDocs preview. - Given a merged pull request, when its branch is deleted, then nothing must be torn down or shut off — the published site is static files on a CDN, with no process running per preview and no per-preview cost.
P3 — The deploy wiring cannot silently rot
As the developer, I want the workflow's new shape asserted by the existing structural test — two named jobs in the right dependency order, the artifact the deploy publishes being the one the checks built — so that a rename or a reorder fails pnpm test loudly instead of silently publishing the wrong bytes, or nothing at all.
Independent test: run pnpm test; the CI structural suite asserts the two-job shape and the artifact match. Rename the artifact in one of the two jobs and the suite fails.
Acceptance scenarios
- Given the committed workflow, when the CI structural test runs, then it confirms two jobs exist — the checks job and the deploy job — and that the deploy job declares
needs:on the checks job. - Given the committed workflow, when the CI structural test runs, then it confirms the artifact name uploaded by the checks job is the one the deploy job downloads, and that the upload step comes after
pnpm docs:build. - Given the deploy job is removed or its
needs:dropped, whenpnpm testruns, then the structural test fails and names what is missing.
Requirements
- R1 — The site is published from CI, not by the hosting provider's own git integration. The provider is Cloudflare Pages, and its project is created in direct-upload mode with no repository connected, so the only thing that ever builds this site is the CI job that already runs the checks. This keeps one toolchain rather than two: the bytes that publish are the bytes that passed
lint → typecheck → test → build → verify:contract → docs:build. - R2 — The existing
checkjob keeps its checks unchanged, in order, and gains one step afterpnpm docs:build: uploading VitePress's build output as a workflow artifact. Adding a publish step must not reorder or weaken a verification step. - R3 — Publishing happens in a second job that declares
needs:on the checks job, so it runs only after every check is green. It downloads the artifact instead of reinstalling and rebuilding, so the published bytes are provably the checked bytes and the job costs seconds rather than a second install. - R4 — One deploy step serves both cases. The target branch is the pull request's head branch on a
pull_requestrun and the pushed branch on apushrun; the Cloudflare project's production branch is set tomain, somainpublishes to the production URL and every other branch publishes as a preview. Noif:gate, no duplicated step. - R5 — The preview is reachable from the pull request through GitHub's native deployment control, not a bot comment. The deploy job declares an
environmentwith aurltaken from the deploy step's output, so GitHub renders its View deployment control on the PR. The environment is namedDocs previewon pull requests andDocsonmain. Confirmed — research.md V1:environment.namemay use thegithubcontext, so one job carries both names. The same table showsnamemay not usestepswhileurlmay — so the split of this requirement across the two keys is the only legal arrangement, not a style choice.Open — research.md V2, V3, V4: whether the deployment renders on the pull request page rather than only in the run summary; whether the job needsdeployments: write; and whether repeat deploys update one entry or add one per run. All three are GitHub UI behaviours that no document settles and the first real run does. Accepted as open by the human at approval, on spec 003's precedent, and carried to step 5 as dated observations (SC1, SC2). - R6 — The button's label is the environment name; GitHub's own View deployment wording is not configurable. The spec claims control over the label only to that extent.
- R7 — Provider credentials are two GitHub repository secrets — an API token scoped to Cloudflare Pages · Edit on the account, and the account id — set by hand by the human and referenced from the workflow. Like the branch-protection rule of spec 003, they are a platform-side setting that cannot be a committed artifact; the workflow references them by name only, and no token value appears in the repository or in run logs.
- R8 — The checks job remains the sole required status check on
main. A failed deploy — expired token, provider outage — must leave the pull request mergeable. Publishing is a convenience; it is not a gate. - R9 — Nothing is torn down when a pull request merges or closes. The deployed artefact is static files served from a CDN: no server runs per preview, so there is nothing to kill and no idle cost. Deleting superseded preview deployments is out of scope (see Out of scope).
- R10 — Every action the new job uses is pinned to the major current at implementation time, verified against its registry rather than assumed — the posture
docs/architecture/ci.mdalready records foractions/checkout@v7,pnpm/action-setup@v6,actions/setup-node@v7. Confirmed — research.md V5:actions/upload-artifact@v7,actions/download-artifact@v8,cloudflare/wrangler-action@v4, read from each registry on 2026-08-13. The two artifact actions are not a matched pair. The deployment-URL output isdeployment-url; the siblingpages-deployment-alias-urlexists but is empty on production deploys (F2). - R11 —
tests/ci/workflow.test.tsis extended to cover the new shape (P3), and its existing single-job assertion is updated rather than deleted: it currently assertsjobshas exactly one key, which this spec deliberately breaks. The suite must continue to assert everything it asserts today — triggers, the six ordered checks, cache, concurrency, and the Node/pnpm pins — against the checks job specifically, not "the first job", so a future job reorder cannot blind it. - R12 — The uncommittable half — the Cloudflare project's existence, its direct-upload mode, its production-branch setting, and the two secrets' names — is recorded in
docs/architecture/ci.md, with a dated verification log for the observational criteria. This is the same treatment spec 003 gave branch protection, and for the same reason: a setting no repo test can reach is captured in prose or it is lost. - R13 — The published site is world-readable by anyone holding its URL, while the repository stays private. This is an accepted, deliberate consequence, decided during brainstorming: the docs read like an open-source project's docs, the URLs are not indexed, and gating them behind an identity provider costs an email round-trip on every view. Recorded here so the decision is visible rather than discovered.
- R14 — No application code changes. This spec adds no HTTP endpoint, alters no OpenAPI document, and touches neither
apps/apinorapps/web;pnpm verify:contractmust therefore still pass unchanged. Stated explicitly because endpoints and OpenAPI are first-class spec content in this project — here the correct content is "none". - R15 — No artifact is written under any
docs/superpowers/path, and the conformance suites from earlier specs continue to pass unchanged.
Mark anything unresolved inline rather than assuming an answer. Two markers, split by who can answer:
[NEEDS CLARIFICATION: specific question]— only the human can answer. A product decision, a scope boundary, a preference. Blocks step 1.5.[NEEDS VERIFICATION: specific question]— only reality can answer. Whether the codebase works that way, whether an endpoint returns that field, whether that number is achievable. Answered inresearch.mdwith cited evidence, never by assumption. Blocks the approval gate.
Any success criterion stating a number carries a [NEEDS VERIFICATION] until a measurement in research.md backs it. An unbacked number is a guess wearing a criterion's clothes.
Success criteria
Measurable and technology-agnostic — outcomes, not implementation. Publishing is inherently a hosted concern (see Assumptions), so the criteria name what a reviewer can observe — a link on a PR, a page that renders, a merge that stays possible — rather than the YAML that produces it.
- SC1 — A pull request that adds a Markdown page publishes a site at which that page is browsable, reachable from a control on the pull request page, with any Mermaid fence rendered as a diagram. (observational — verified on the first real pull request, recorded with a date)
- SC2 — Pushing again to that pull request publishes the newer build, and the branch's URL resolves to it. (observational — recorded with a date. Amended 2026-08-13: originally required "exactly one deployment entry, not a growing list", which observation refuted — see P1 #2.)
- SC3 — A pull request whose checks fail publishes nothing. (observational — recorded with a date)
- SC4 — Two pull requests open simultaneously resolve to two distinct URLs, each serving its own branch's content. (observational — recorded with a date)
- SC5 — A merge to
mainupdates one fixed URL, and the merged branch's preview requires no teardown action of any kind. (observational — recorded with a date) - SC6 — A deploy failure leaves the pull request mergeable: the required status check reports on the checks job alone. (observational — recorded with a date)
- SC7 — The deployment entry reads
Docs previewon a pull request andDocsonmain. (observational — recorded with a date; depends on theenvironment.nameverification in R5) - SC8 — The committed workflow has two jobs in a
needs:relationship, the deploy job publishes the same artifact name the checks job uploads, and that upload happens after the docs build — asserted by an automated test over the workflow file. - SC9 — Every assertion the CI structural suite makes today still holds after the change, bound to the checks job by name rather than by position — asserted by that suite.
- SC10 — No credential value appears anywhere in the repository; the workflow references secrets by name only — asserted by an automated test over the workflow file.
- SC11 — The count of files under any
docs/superpowers/path stays zero,pnpm verify:contractpasses unchanged, and the earlier specs' suites still pass — asserted by the existing invariants. - SC12 — Every acceptance scenario and success criterion above maps either to a named automated test or to a dated manual-verification record, with no gap; the automated portion passes.
Out of scope
- Deleting superseded preview deployments. Merged and closed pull requests leave their published previews in place. They are static files with no running cost, and a cleanup workflow is machinery guarding against a problem that does not exist yet. If the provider's retained-deployment count ever becomes a real limit, cleanup earns its own change then.
- Gating the preview behind authentication. Decided during brainstorming: the URLs stay open (R13). Adding an identity policy later is a provider-side settings change that no repo code depends on, so deferring costs nothing.
- A custom domain. The provider's generated hostname is the address. A domain is a purchase and a DNS concern, unrelated to whether previews work.
- A bot comment carrying the link. Explicitly rejected in favour of the native deployment control.
- Previewing anything but the docs.
apps/webis not deployed by this spec. Publishing the running application per pull request is a much larger concern — a server, an API, secrets, data — and belongs to its own spec. - Deploying the application, or any release automation. Unchanged from spec 003's exclusion; this spec publishes a static documentation site and nothing else.
- Managing the provider's project as code. Like branch protection before it, the project's settings are created by hand and documented (R12), not provisioned by a committed script. The tension with "all artifacts are committed" is acknowledged and captured rather than engineered around now.
- Making the repository public, or GitHub Pages. Considered during brainstorming and set aside: Pages on a private repository requires a paid plan, and previews there would mean subfolder deploys with per-deploy base-path juggling. Whether to go public remains an open question of its own — it would also close spec 003's SC6 gap — but it is not decided by this spec.
Assumptions
Like specs 001–003, this is a workbench spec: it names its tooling because choosing that tooling is the point, not an implementation leak. The following are fixed by prior decisions or by brainstorming, and taken as given.
- The repository is private, on a free plan.
docs/architecture/ci.mdrecords this as the reason branch protection cannot be applied; it is also why GitHub Pages is unavailable and why an external host is in play at all. - GitHub Actions is the CI platform and
.github/workflows/ci.ymlis the single workflow (spec 003, R1). This spec extends that file rather than adding a second workflow. - The docs build already works and is already verified.
pnpm docs:buildis a green check on every run today. This spec publishes its output; it does not change VitePress's configuration, and in particular needs nobasepath, because each branch is served from its own hostname rather than a subfolder. Confirmed — research.md V6: a direct-upload project does mint per-branch hostnames. Proven live on 2026-08-13, deploying this branch to013-docs-previews.gw2-priory-docs.pages.devwith nobaseconfigured; the generated sidebar, cross-links andcleanUrlsrouting all resolved. - The Cloudflare project is named
gw2-priory-docs. The name is visible in every URL this spec produces, so it is written down rather than left to implementation. It is a preference, not a constraint — changing it before the project is created costs nothing. - A Cloudflare account exists and the human can create the project and the API token. The two platform-side actions of R7 and R12 are the human's, taken once, before the first run can publish.
- Free-tier capacity is sufficient. Uploads are static assets and the build happens in GitHub Actions, not on the provider. Confirmed — research.md V7: unlimited concurrent preview deployments, 20,000 files per deployment, 25 MiB per asset. The documented 500-builds/month ceiling applies only to builds Cloudflare runs from a connected repository, which a direct-upload project never does. This site measures 296 files — 1.5% of the file ceiling.
- The in-repo structural-test pattern is established.
tests/ci/workflow.test.tsalready parses the committed workflow and asserts its shape; R11 extends that suite rather than introducing a new kind of test. As with spec 004's change to the ordered-checks assertion, this spec's workflow change is deliberately coupled to a test change. - No
[NEEDS CLARIFICATION]remains. Every product decision — provider, wiring, open access, native control over a comment, both preview and production — was settled in brainstorming. Seven verification items went to step 1.5; four came back Confirmed and are recorded inline above, and three remain Open because only a real run can answer them (R5).
Traceability
Each acceptance scenario and success criterion maps to a named test or to a dated manual-verification record — the latter only for the inherently observational criteria (a link on a pull request page, a rendered diagram, a merge that stays possible) that no in-repo test can prove. SC12 asserts this table has no empty cell. Filled in during implementation.
| Criterion | Test / verification |
|---|---|
| P1 #1 | manual — docs/architecture/ci.md log: preview link on the PR serves the head commit |
| P1 #2 | manual — docs/architecture/ci.md log: second push publishes a newer build; entries accumulate (amended, research.md V4) |
| P1 #3 | manual — docs/architecture/ci.md log: red checks publish nothing |
| P1 #4 | manual — docs/architecture/ci.md log: two PRs, two URLs |
| P1 #5 | manual — docs/architecture/ci.md log: deploy failure leaves the PR mergeable |
| P2 #1 | manual — docs/architecture/ci.md log: merge updates the production URL |
| P2 #2 | manual — docs/architecture/ci.md log: no teardown action needed |
| P3 #1 | tests/ci/workflow.test.ts — two jobs, deploy needs: checks |
| P3 #2 | tests/ci/workflow.test.ts — artifact name match, upload after docs:build |
| P3 #3 | tests/ci/workflow.test.ts — a removed deploy job fails the shape assertion |
| SC1–SC7 | manual — docs/architecture/ci.md verification log |
| SC8 | tests/ci/workflow.test.ts — two-job shape, artifact match, step order |
| SC9 | tests/ci/workflow.test.ts — existing assertions bound to the checks job by name |
| SC10 | tests/ci/workflow.test.ts — credentials referenced as secrets.* only |
| SC11 | tests/workflow/repo-invariants.test.ts + pnpm verify:contract |
| SC12 | tests/ci/workflow.test.ts — this table complete + named tests exist |