Skip to content

Research 013 — Docs previews ​

Status: complete Every [NEEDS VERIFICATION] marker in spec.md has a verdict below: six Confirmed, one Refuted, none open. Four were settled by reading and doing during step 1.5; the remaining three (V2, V3, V4) were approved as Open on 2026-08-13 on spec 003's precedent, and answered the same day by the first two real CI runs. V4 came back refuted — the pull request accumulates a deployment entry per push rather than updating one — and P1 #2 and SC2 were amended to match, by the human's decision.

Step 1.5 output, written between the spec draft and the approval gate. open until every [NEEDS VERIFICATION] marker in spec.md has a verdict here; complete once they do. One entry per [NEEDS VERIFICATION] marker in spec.md, plus anything that turned up alongside.

Evidence is cited, not recalled. A claim about this repo cites a file and line; a claim about the outside world cites a response, a measurement, or a document. "I believe" is not evidence.

Verified 2026-08-13 against Wrangler 4.122.0, a Cloudflare free-plan account, and the GitHub Actions documentation as published on that date. Action versions were read from each repository's releases/latest via the GitHub API on 2026-08-13, not from memory. The live evidence below comes from a real Cloudflare Pages project, gw2-priory-docs, created during this step in direct-upload mode with --production-branch main, and a real deployment of this branch's built docs.

Four of the seven questions were answered by doing the thing rather than reading about it. Three remain open by nature: they are properties of GitHub's pull-request UI that only a real pull request with the workflow in place can show, and they are carried into step 5 as dated observations rather than guessed at here.

V1 — Does environment.name accept an expression, so one job can be Docs preview on a PR and Docs on main? ​

Question. R5 wants a single deploy job whose environment is named by an expression over github.event_name. If GitHub evaluates no expressions there, R5's fallback is one environment name for both cases — explicitly not two jobs.

Verdict. Confirmed — with a constraint that turns out to matter, and that the design already happens to satisfy.

Evidence. GitHub's context-availability table (https://docs.github.com/en/actions/reference/workflows-and-actions/contexts) lists the two keys separately, with different allowances:

KeyContexts allowed
jobs.<job_id>.environmentgithub, needs, strategy, matrix, vars, inputs
jobs.<job_id>.environment.urlgithub, needs, strategy, matrix, job, runner, env, vars, steps, inputs

environment.name inherits the parent row, which includes github — so ${{ github.event_name == 'pull_request' && 'Docs preview' || 'Docs' }} is evaluable. Critically, the parent row does not include steps, while the environment.url row does. The name may therefore not depend on a step output, and the URL must — which is exactly the split R5 describes. What looked like an arbitrary formatting choice is the only legal arrangement.

Caveat. Confirmed from the documented context table, not from a run. The failure mode if the table is wrong is loud and immediate — a workflow that will not parse — so it costs nothing to discover on the first push.

V2 — Does the deployment appear on the pull request page, or only in the workflow run summary? ​

Question. P1 #1 promises a control on the pull request. If environments only surface in the run summary, the story is not met and the rejected bot comment comes back into scope.

Verdict. Confirmed — answered on the first real run, 2026-08-13.

Evidence (2026-08-13). Run 31727381528 published, and PR #13's timeline carries a deployed event:

gh api repos/Wallaka/gw2-priory/issues/13/timeline --jq '.[] | select(.event=="deployed")'

The deployment record reads env=Docs preview, ref=013-docs-previews, with a success status whose environment_url is https://d96c0bff.gw2-priory-docs.pages.dev — the immutable deployment URL, as F2 requires. The environment name expression evaluated correctly, confirming V1 in practice as well as on paper. P1 #1 is met and the rejected bot-comment alternative stays rejected.

Superseded reasoning (kept for the record). Before the run: the mechanism was confirmed, its placement was not.

Evidence. GitHub's environments documentation states: "When a workflow job that references an environment runs, it creates a deployment object with the environment property set to the name of your environment" (https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments). So a deployment record is definitely created. Neither that page nor the workflow-syntax reference states where it is rendered, and no amount of further reading substitutes for opening a pull request that has one.

Caveat. This is the one question that can send P1 back to the drawing board, and it cannot be answered before the workflow exists. It is deliberately left open rather than assumed: SC1 is already marked observational, and this is what it observes.

V3 — Does the deploy job need deployments: write in permissions? ​

Question. R5 needs to know whether to widen the workflow's permissions block, which is currently contents: read (.github/workflows/ci.yml).

Verdict. Confirmed not required — answered on the first real run, 2026-08-13.

Evidence (2026-08-13). Run 31727381528's docs job created the deployment and published successfully with the workflow's permissions still at contents: read and no job-level grant. The lean was right, and declining to add the scope pre-emptively cost nothing.

Superseded reasoning (kept for the record). Before the run: open, leaning not required.

Evidence. The deployment object is created by the Actions service as a consequence of the job referencing an environment (quoted in V2), not by a call the workflow makes with GITHUB_TOKEN. No page found on 2026-08-13 lists deployments: write as a prerequisite for the environment: key; the permission is documented for workflows that call the Deployments REST API themselves, which this one does not.

Caveat. Absence of a documented requirement is weaker evidence than a run. The cost of being wrong is one red job with an explicit permissions error, and the fix is one line — so this is carried into implementation rather than settled by adding the permission speculatively. Adding it "just in case" would grant a token scope the workflow may not need, which is the wrong default.

V4 — Does a repeated deployment update one entry on the pull request, or add one per run? ​

Question. P1 #2 promises the preview updates in place rather than accumulating entries.

Verdict. Refuted — entries accumulate. P1 #2 assumed the opposite and was amended.

Evidence (2026-08-13). Two runs, 31727381528 then 31727821558, produced two deployment records — 5893154142 and 5893233968, both env=Docs preview, ref=013-docs-previews — and PR #13's timeline carries two deployed events. The older deployment's statuses are success, in_progress: GitHub did not auto-inactivate it. So the pull request grows one entry per push.

What this costs, and what it does not. Nothing functional. The branch alias resolved to the newest build immediately after the second run (/docs/gaps/docs-build-render-failures returned 200), so the preview is never stale — which is the property the feature exists for. What failed was the spec's description of GitHub's display, and no engineering choice changes it: a deployment record per run is how Actions environments work. The alternatives are the rejected bot comment, or no link at all.

Consequence. P1 #2 and SC2 were amended on 2026-08-13 to state what happens, by the human's decision. The refutation touches one rendering sentence, not a premise the design rests on, so the spec was corrected in place rather than returned to step 1 — a judgement made explicitly rather than by default, since the constitution's usual answer is the opposite.

V5 — What are the current majors of the artifact actions and the Cloudflare deploy action, and what is the deployment-URL output called? ​

Question. R10 requires pinning to majors verified against the registry, following the posture in docs/architecture/ci.md. The draft spec guessed wrangler-action@v3.

Verdict. Confirmed, and the draft's guess was wrong — corrected below.

Evidence. gh api repos/<owner>/<repo>/releases/latest, 2026-08-13:

ActionCurrent majorReleased
actions/upload-artifactv7.0.12026-04-10
actions/download-artifactv8.0.12026-03-11
cloudflare/wrangler-actionv4.0.02026-05-12

Outputs, read from cloudflare/wrangler-action's committed action.yml (fetched via the GitHub contents API, same date):

  • deployment-url — "If the command was a Workers or Pages deployment, this will be the URL of the deployment"
  • pages-deployment-alias-url — "the URL of the deployment alias (if it exists) — needs wrangler >= 3.78.0"
  • also pages-deployment-id, pages-environment, command-output, command-stderr

So deployment-url is real and correctly named. See F2 for which of the two URLs belongs in environment.url — the answer is not the obvious one.

Caveat. Upload is on v7 while download is on v8; they are not a matched pair, and writing v7 for both — the natural assumption — would pin a version of download-artifact that does not exist. Whether these two specific majors interoperate is asserted by neither changelog and is confirmed by the first real run.

V6 — Does a direct-upload Pages project mint per-branch hostnames, or is that git-connected only? ​

Question. The spec's Assumptions rest on each branch being served from its own hostname, which is what lets VitePress deploy with no base path. If direct-upload projects only produce per-deployment hashes, preview URLs stop being predictable from a branch name.

Verdict. Confirmed — direct-upload projects do mint branch aliases.

Evidence. Live, 2026-08-13. Project created with wrangler pages project create gw2-priory-docs --production-branch main, no repository connected. Then:

$ npx wrangler pages deploy .vitepress/dist --project-name gw2-priory-docs --branch 013-docs-previews
✨ Success! Uploaded 296 files (1.48 sec)
✨ Deployment complete! Take a peek over at https://2ab17adc.gw2-priory-docs.pages.dev
✨ Deployment alias URL: https://013-docs-previews.gw2-priory-docs.pages.dev

Both URLs serve the site. Sampled routes on the alias, all 200: /, /CLAUDE, /docs/architecture/ci, /specs/001-workflow-tooling/spec, /specs/013-docs-previews/spec — the last returning <title>Spec 013 — Docs previews | GW2 Priory</title>. The generated sidebar, cross-links and cleanUrls routing all work with no base configured, confirming that half of the assumption too.

V7 — Are there free-plan ceilings a per-push publish would realistically hit? ​

Question. Whether publishing on every push is sustainable on the free plan.

Verdict. Confirmed — comfortable, with one limit worth remembering.

Evidence. Cloudflare Pages limits (https://developers.cloudflare.com/pages/platform/limits/), free plan: unlimited preview deployments active at once; 20,000 files per deployment; 25 MiB per individual asset. The documented 500 builds/month ceiling applies to builds Cloudflare runs from a connected git repository — which a direct-upload project never does, since the build happens in GitHub Actions.

Measured against this repository's current site: 296 files, 14 MB total — 1.5% of the file ceiling.

Caveat. The docs do not state a retention limit on how many past deployments a project keeps. Since this spec deletes nothing (R9), that is the number to watch if anything ever bites; nothing observed suggests it will.

F1 — A brand-new project's first alias URL fails TLS for about a minute ​

*.pages.dev is a single-label wildcard, so it covers gw2-priory-docs.pages.dev but not013-docs-previews.gw2-priory-docs.pages.dev. Cloudflare issues a separate *.gw2-priory-docs.pages.dev certificate on the project's first deployment. Measured on 2026-08-13, polling the alias every 30 s immediately after a successful upload:

ElapsedResult
0 scurl: (35) sslv3 alert handshake failure
30 ssame
60 s200

Why it matters. The very first CI run on a fresh project can hand a reviewer a link that briefly fails with a TLS error rather than a 404 — an error that reads like a misconfiguration and would send someone debugging the workflow. One-time per project, not per preview. It belongs in docs/architecture/ci.md so the next person does not rediscover it under pressure.

F2 — Two URLs per deployment, and the obvious one is the wrong choice for environment.url ​

Every Pages deploy yields an immutable per-deployment hash URL (2ab17adc.…) and, for non-production branches, a mutable branch alias (013-docs-previews.…). wrangler-action exposes them as deployment-url and pages-deployment-alias-url respectively (V5).

The alias is the nicer link to share, but environment.url should carry deployment-url, for two reasons:

  1. A deployment entry is tied to a specific run. Clicking an older entry should show what that commit built, which only the immutable URL guarantees; the alias always shows the branch tip.
  2. pages-deployment-alias-url is documented as present "if it exists". Production deploys — the main case of R4 — have no alias, so a workflow wiring the alias into environment.url would produce an empty URL on exactly the runs P2 cares about.

Why it matters. Directly determines one line of the workflow R5 specifies, and prevents a bug that would only appear after merging rather than on the pull request that introduced it.

F3 — The production URL 404s until something deploys with --branch main ​

https://gw2-priory-docs.pages.dev/ returned 404 immediately after the project was created and the preview deployed. This is correct behaviour, not a fault: the production branch has had no deployment yet.

Why it matters. P2 #1 is unobservable until the first merge to main after the workflow lands. Anyone checking the production URL before then will find a 404 and reasonably suspect the setup is broken.

F4 — A Markdown file can break its own page while docs:build stays green ​

Found by breaking it: this very file's first draft contained a GitHub Actions expression in inline code. VitePress passes Markdown through Vue, and {{ … }} is interpolated inside inline code spans — fenced blocks are safe, inline spans are not. Vue evaluated it against an undefined github and the page's server-side render threw:

TypeError: Cannot read properties of undefined (reading 'event_name')

The fix is <span v-pre> around the inline code. The finding is what happened next. Probed on 2026-08-13 with a throwaway docs/tmp-ssr-probe.md containing one interpolated expression, built, and deleted:

ObservationResult
pnpm docs:build exit code0
TypeError present in its outputyes
Page emittedyes, with a failed server render

So docs:build reports success while shipping a page whose render threw. Spec 003's docs:build check proves the site builds; it does not prove the pages render.

The two failure modes are not the same, and the difference decides what CI can see. Writing this entry produced the other one: the sentence above originally contained the interpolation braces in inline code, which Vue compiled to _ctx.… and Rollup rejected as Unexpected character '…' — exit code 1, build genuinely red. So:

Failure modeExampledocs:build
Vue compile erroran expression that is not valid JavaScriptexit 1 — caught
Vue runtime SSR errorvalid JavaScript over an undefined objectexit 0 — missed

Only the second is invisible, and the second is the likelier one when quoting Actions expressions, since github.event_name is perfectly valid JavaScript that simply has nothing to evaluate against.

Why it matters. Two ways, pulling in opposite directions.

Against this spec: nothing here can rely on a green pipeline meaning "the docs are fine". SC1 asks that a published page be browsable with its diagrams drawn, and no existing automated check establishes that.

For this spec: the preview is the mitigation. This bug was caught because the page was published and looked at — which is precisely the gap the feature exists to close. It also recurs immediately, since plan.md will quote the workflow YAML: safe inside fences, hazardous the moment an expression is mentioned mid-sentence.

The deeper problem — a check that passes over a broken render — belongs to spec 003's docs:build, not to this one. Parked as a docs/gaps/ record rather than smuggled into this spec's scope (see Graduation).

Refuted claims ​

  • cloudflare/wrangler-action@v3. The spec's brainstorming sketch named v3; the current major is v4.0.0 (V5). This is a version correction inside a requirement that already said "verified against its registry rather than assumed" (R10) — the requirement anticipated exactly this, so no premise of the spec falls with it and nothing returns to step 1.
  • P1 #2's "one entry, updated in place". Refuted by observation on 2026-08-13 (V4): GitHub records a deployment per run and the pull request accumulates one entry each. Amended in spec.md rather than returned to step 1, because the false claim was about GitHub's rendering, not about anything the design depends on — and no implementation choice could have made it true.
  • No load-bearing premise was refuted. Direct-upload branch aliases, no base path needed, free-tier headroom, an expression-capable environment name — all held.

Graduation ​

Candidates for docs/architecture/ci.md at step 6, per R12:

  • The Cloudflare project's identity and mode: gw2-priory-docs, direct upload, production branch main, no repository connected — and why direct upload rather than git integration (one toolchain; the published bytes are the checked bytes).
  • The two GitHub secrets by name, the token's exact scope (Account · Cloudflare Pages · Edit), and the note that the account id is retrievable at any time with wrangler whoami rather than being stored anywhere in the repository.
  • F1, the one-time TLS provisioning delay — the finding most likely to waste someone's afternoon.
  • F2, why environment.url carries deployment-url and not the alias.
  • The dated verification log for SC1–SC7, which this file's V2/V3/V4 residuals feed into.
  • The correction that upload-artifact and download-artifact majors are not paired (V5).
  • F4 splits in two: the v-pre authoring rule belongs in docs/architecture/ alongside the other docs conventions, while "docs:build exits 0 on a failed render" is a gap in spec 003's check and is recorded in docs/gaps/ as a finding, not a proposal — it is not this spec's to fix.