Skip to content

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.ts end to end. Every assertion in describe('T1: …') currently resolves through the positional steps binding; 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.
ts
const jobs = workflow.jobs ?? {};
const checkJob = jobs.check;
const steps = checkJob?.steps ?? [];
  • [ ] Confirm the binding has teeth: temporarily rename the job in ci.yml from check to checks, run pnpm 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.
ts
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.docs is typed rather than cast.
ts
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; expect pnpm test to 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. Run pnpm test and confirm each new assertion fails because jobs.docs is undefined — not because of a typo in the test.
ts
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 check job, immediately after pnpm docs:build.
yaml
      - uses: actions/upload-artifact@v7
        with:
          name: docs-site
          path: .vitepress/dist
          retention-days: 1
  • [ ] GREEN, part two: add the docs job. Pin the majors exactly as written — upload is v7 and download is v8; they are not a matched pair (research.md V5), and v7 for both pins a download major that does not exist.
yaml
  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-level contents: read assertion (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-site in the download step to name: docs, run pnpm test, watch P3 #2 fail, restore.
  • [ ] Run pnpm lint and pnpm 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 documented block so the new prose is asserted, not merely hoped for.
ts
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 because ci.md says none of this yet.
  • [ ] GREEN: add a "Docs previews" section to docs/architecture/ci.md covering, in prose: the project gw2-priory-docs in direct-upload mode with production branch main and 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 with wrangler whoami and 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.md F1) and why environment.url carries deployment-url rather than the branch alias (F2).
  • [ ] Create docs/gaps/docs-build-render-failures.md recording research.md F4 — a Vue compile error fails docs: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 test and pnpm docs:build. Expect green, and expect zero TypeError in 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 at specs/013-docs-previews/spec.md, asserting every row is filled and every backticked .test.ts path 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, name docs/architecture/ci.md as 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.ts for 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 docs job needed deployments: 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.md has 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 docs job does not run and nothing publishes. Restore. SC3.
  • [ ] Confirm the pull request stayed mergeable throughout — the required status check reports on check alone. 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 implemented into spec.md inside this branch, so the transition is part of the reviewed diff — never a separate edit to main afterward.

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 deploy used 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.