Tasks 013 — Docs previews
Execution skill: superpowers:subagent-driven-development — one implementer per task, then a two-stage review (spec compliance, then code quality). superpowers:test-driven-development applies inside every task: no production code before a failing test that demands it. Reach for superpowers:systematic-debugging on any surprise rather than guessing.
Derived from plan.md (approved). Each task is small, independently verifiable, and reviewed as its own diff. Split where a reviewer could reject one task while approving its neighbour — not where the work merely changes subject. A task is done only when it satisfies the definition of done in CLAUDE.md.
Global Constraints in plan.md apply to every task and are not repeated per task. Two are load-bearing here and easy to trip: no any and no non-null assertions (!) — the test code below is written without either, using findIndex rather than find(...)!.
Authoring hazard, all tasks. Any Markdown written by these tasks that mentions a GitHub Actions expression must keep it inside a fenced code block, or wrap it in <span v-pre> when inline. Inline is interpolated by Vue and can fail the page's server render while docs:build still exits 0 (research.md F4).
T1 — The structural guard reads jobs by name, not by position
Satisfies: R11, SC9
Pure refactor: no change to .github/workflows/ci.yml, no change to what is asserted. Isolated from T2 so a reviewer can see that the existing guarantees survived the move before judging the new job. Today the suite reads Object.values(jobs)[0] (tests/ci/workflow.test.ts:40) — once a second job exists, that keeps passing while asserting nothing about the job it means.
- [ ] Read
tests/ci/workflow.test.tsend to end. Every assertion indescribe('T1: …')currently resolves through the positionalstepsbinding; all of them move together. - [ ] RED: change the module-level binding to select the job by name, and watch the suite still pass — this refactor's "failing test" is the next step, because a rename that silently matches nothing is the failure mode being guarded against.
const jobs = workflow.jobs ?? {};
const checkJob = jobs.check;
const steps = checkJob?.steps ?? [];- [ ] Confirm the binding has teeth: temporarily rename the job in
ci.ymlfromchecktochecks, runpnpm test, and watch the suite fail loudly rather than pass over an empty step list. Restore the name. - [ ] Replace the single-job assertion with one that names both jobs the plan commits to. It stays red until T2 adds
docs— that is intentional and is T2's entry condition.
it('R3/SC9: names its jobs, and the checks job runs on Linux', () => {
expect(Object.keys(jobs).sort()).toEqual(['check', 'docs']);
expect(String(checkJob?.['runs-on'])).toContain('ubuntu');
});- [ ] Add the interface fields the later tasks need, so
jobs.docsis typed rather than cast.
interface Job {
'runs-on'?: unknown;
needs?: unknown;
environment?: { name?: unknown; url?: unknown };
steps?: WorkflowStep[];
}
interface Workflow {
on?: { pull_request?: unknown; push?: { branches?: unknown } };
permissions?: { contents?: unknown };
concurrency?: { 'cancel-in-progress'?: unknown };
jobs?: Record<string, Job>;
}- [ ] Run
pnpm typecheck. Expect clean; expectpnpm testto fail on exactly one assertion — the job-names one. - [ ] Commit. The commit message says the suite is deliberately red pending T2.
Verified by: tests/ci/workflow.test.ts — T1: the CI workflow block, all assertions bound to jobs.check, passing except the job-names assertion.
T2 — CI publishes the built docs to Cloudflare Pages
Satisfies: R1, R2, R3, R4, R5, R8, R10, P3 #1, P3 #2, P3 #3, SC8, SC10
The only task that changes behaviour. Tests first: every assertion below is red before ci.yml is touched.
- [ ] RED: add the deploy-job block to
tests/ci/workflow.test.ts. Runpnpm testand confirm each new assertion fails becausejobs.docsis undefined — not because of a typo in the test.
describe('T4: the docs publish job', () => {
const docsJob = jobs.docs;
const docsSteps = docsJob?.steps ?? [];
it('P3 #1/SC8: `docs` runs only after `check`', () => {
expect(docsJob).toBeDefined();
expect(docsJob?.needs).toBe('check');
});
it('P3 #2/SC8: publishes the artifact `check` uploaded, taken after the docs build', () => {
const buildIndex = steps.findIndex((step) => step.run === 'pnpm docs:build');
const uploadIndex = steps.findIndex((step) =>
step.uses?.startsWith('actions/upload-artifact'),
);
expect(buildIndex).toBeGreaterThanOrEqual(0);
expect(uploadIndex).toBeGreaterThan(buildIndex);
const uploaded = withOf(steps[uploadIndex]).name;
const downloaded = withOf(
docsSteps.find((step) => step.uses?.startsWith('actions/download-artifact')),
).name;
expect(uploaded).toBe(downloaded);
});
it('P1 #1/SC7: the environment url comes from the deploy step output', () => {
// research.md F2: `deployment-url` is immutable and always present; the branch alias is empty
// on production runs, so wiring it here would break on merge and look fine on the PR.
expect(String(docsJob?.environment?.name)).toContain('Docs');
expect(String(docsJob?.environment?.url)).toContain(
'steps.deploy.outputs.deployment-url',
);
});
it('SC10: credentials are referenced as secrets, never inlined', () => {
const raw = readFileSync(join(repoRoot, '.github/workflows/ci.yml'), 'utf8');
const tokenLine = raw.split('\n').find((line) => line.includes('apiToken:'));
const accountLine = raw.split('\n').find((line) => line.includes('accountId:'));
expect(tokenLine).toContain('secrets.CLOUDFLARE_API_TOKEN');
expect(accountLine).toContain('secrets.CLOUDFLARE_ACCOUNT_ID');
});
});- [ ] GREEN, part one: add the upload step to the
checkjob, immediately afterpnpm docs:build.
- uses: actions/upload-artifact@v7
with:
name: docs-site
path: .vitepress/dist
retention-days: 1- [ ] GREEN, part two: add the
docsjob. Pin the majors exactly as written — upload isv7and download isv8; they are not a matched pair (research.mdV5), andv7for both pins a download major that does not exist.
docs:
needs: check
runs-on: ubuntu-latest
environment:
name: ${{ github.event_name == 'pull_request' && 'Docs preview' || 'Docs' }}
url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- uses: actions/download-artifact@v8
with:
name: docs-site
path: dist
- id: deploy
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=gw2-priory-docs --branch=${{ github.head_ref || github.ref_name }}- [ ] Do not add
deployments: write. V3 is open; if the first run fails on permissions, add it at job level only, so the workflow-levelcontents: readassertion (tests/ci/workflow.test.ts:113) keeps holding. - [ ] Run
pnpm test. Expect green, including T1's job-names assertion, which this task unblocks. - [ ] Confirm the artifact assertion has teeth: change
name: docs-sitein the download step toname: docs, runpnpm test, watch P3 #2 fail, restore. - [ ] Run
pnpm lintandpnpm typecheck. Expect clean. - [ ] Commit.
Verified by: tests/ci/workflow.test.ts — T4: the docs publish job, all four assertions green; plus the first real run in T5.
T3 — The uncommittable half is written down
Satisfies: R7, R9, R12, R13
Nothing here is enforceable by the platform, which is exactly why it is written. Mirrors how spec 003 documented branch protection.
- [ ] RED: extend the existing
T2: the CI enforcement is documentedblock so the new prose is asserted, not merely hoped for.
it('R12: docs/architecture/ci.md documents the Pages project and its secrets', () => {
const doc = readFileSync(ciDoc, 'utf8');
expect(doc).toContain('gw2-priory-docs');
expect(doc).toContain('CLOUDFLARE_API_TOKEN');
expect(doc).toContain('CLOUDFLARE_ACCOUNT_ID');
expect(doc.toLowerCase()).toContain('direct upload');
});- [ ] Run
pnpm test, watch it fail becauseci.mdsays none of this yet. - [ ] GREEN: add a "Docs previews" section to
docs/architecture/ci.mdcovering, in prose: the projectgw2-priory-docsin direct-upload mode with production branchmainand no repository connected; why direct upload rather than git integration (one toolchain — the published bytes are the checked bytes); the two secret names and the token's exact scope (Account · Cloudflare Pages · Edit); that the account id is retrievable withwrangler whoamiand is an identifier rather than a credential; that merged previews need no teardown because the site is static files on a CDN (R9); and that preview URLs are readable by anyone holding them while the repository stays private, as a decision rather than an oversight (R13). - [ ] Add the two findings that will otherwise cost someone an afternoon: the one-time TLS provisioning delay on a project's first deployment (
research.mdF1) and whyenvironment.urlcarriesdeployment-urlrather than the branch alias (F2). - [ ] Create
docs/gaps/docs-build-render-failures.mdrecordingresearch.mdF4 — a Vue compile error failsdocs:build, a runtime SSR error does not, so the check can pass over a page whose render threw. Write it as a record of what is true, not as a proposal to fix it: it is spec 003's check, not this spec's, and inventing a fix here is scope creep. - [ ] Follow the existing file's conventions — dated observations, a table where the shape is tabular. Match the surrounding prose rather than introducing a new voice.
- [ ] Run
pnpm testandpnpm docs:build. Expect green, and expect zeroTypeErrorin the build output — the new prose quotes Actions expressions, which is exactly the F4 hazard. - [ ] Commit.
Verified by: tests/ci/workflow.test.ts — T2: the CI enforcement is documented, new R12 assertion.
T4 — Spec 013's traceability is complete and enforced
Satisfies: SC12, R11
The existing T3 block asserts this for spec 003 by parsing its table. Spec 013 needs the same, or its own SC12 is an unchecked promise.
- [ ] RED: add a block mirroring
T3: spec 003 traceability is complete (SC8), pointed atspecs/013-docs-previews/spec.md, asserting every row is filled and every backticked.test.tspath it names exists on disk. - [ ] Run
pnpm test, watch it fail on the rows the table has not filled yet. - [ ] GREEN: fill the traceability table in
spec.md, transcribing the actual test names written in T1–T3 rather than the intended ones drafted at spec time. Where a row resolves to a dated observation, namedocs/architecture/ci.mdas the record — the same convention spec 003 used. - [ ] Rows SC1–SC7 stay pointed at the verification log until T5 fills it. Confirm the test accepts a manual-record cell and does not demand a
.test.tsfor every row — if it does, the assertion is wrong, not the table. - [ ] Run
pnpm test. Expect green. - [ ] Commit.
Verified by: tests/ci/workflow.test.ts — spec 013 traceability block, both assertions green.
T5 — The first real run settles what no test can
Satisfies: P1 #1–P1 #5, P2 #1, P2 #2, SC1–SC7, and research.md V2, V3, V4
This is a verification task, not a coding task. It is the planned event where the three open verdicts get answered. Use superpowers:verification-before-completion: observe, then claim — never the reverse.
- [ ] Push the branch and watch the run. Record what the pull request page shows: whether a deployment entry appears there at all, and what its label reads. This settles V2. If nothing renders on the pull request, stop — P1 #1 is unmet and the bot-comment alternative returns to the table.
- [ ] Record whether the
docsjob neededdeployments: write. This settles V3. If it failed on permissions, add the grant at job level, commit separately, and note it. - [ ] Open the published preview and confirm a page renders with its Mermaid diagram drawn —
plan.mdhas one. SC1. - [ ] Push a second commit and record whether the pull request shows one updated entry or two. This settles V4 and SC2.
- [ ] Break a check deliberately (a lint violation), push, confirm the
docsjob does not run and nothing publishes. Restore. SC3. - [ ] Confirm the pull request stayed mergeable throughout — the required status check reports on
checkalone. SC6. If a deploy failure ever blocks the merge button, R8 is violated. - [ ] Record every observation in the verification log in
docs/architecture/ci.md, each with its date and the run or pull request it came from. An undated observation is a memory, not a record. - [ ] Update
research.md: move V2, V3 and V4 from Open to their answers, citing the run. - [ ] SC4 (two pull requests, two URLs) and SC5 (merge updates production, nothing to tear down): SC5 is observable only at merge. Record it immediately after merging rather than promising to.
- [ ] Ask the human to decide the spec's status. On their instruction, transcribe
implementedintospec.mdinside this branch, so the transition is part of the reviewed diff — never a separate edit tomainafterward.
Verified by: the dated verification log in docs/architecture/ci.md, and research.md's V2/V3/V4 entries carrying verdicts rather than Open.
Notes
Staging area for decisions and surprises found during implementation — including anything that turned out differently from what plan.md assumed. Move each one into spec.md, research.md, or docs/ before closing the feature; this section is not a home.
- The manual
wrangler pages deployused throughout discovery stops being necessary once T2 lands. If anyone still needs to run it by hand after that, something in the workflow is broken — say so rather than papering over it with another manual deploy. - Spec 003's SC6 gap (branch protection unavailable on a free private plan) is untouched by this work and stays open. Do not quietly fix it here.