From 6cb2b3084d3b44241e11044e7fdf0479ddf8576e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 09:20:50 -0700 Subject: [PATCH 1/4] docs: design workflow pagination identity fix --- ...onsolidation-workflow-pagination-design.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md diff --git a/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md b/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md new file mode 100644 index 000000000..e7e1ea424 --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md @@ -0,0 +1,143 @@ +# Consolidation Workflow Pagination Identity Design + +## Status + +Approved design; implementation pending. + +## Problem + +The duplicate-draft consolidation inspection reads every run of the release +workflow and rejects mutation while any run is nonterminal. The reader sends +an exact request through GitHub's owner/name endpoint: + +`/repos/cacheplane/dawnai/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs` + +GitHub's authenticated response can express pagination links through its +canonical numeric repository endpoint instead: + +`/repositories/1210070282/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs` + +Both paths identify the same repository and workflow. The current Link +verifier accepts only the owner/name form, so the live v0.8.22 inspection +failed closed before it could confirm that there were zero nonterminal runs. +It wrote no proposal or journal and performed no mutation. + +## Goals + +- Accept GitHub's two exact path representations of the already-authorized + Dawn repository and release workflow. +- Preserve strict validation of origin, protocol, credentials, fragment, + workflow path encoding, query fields, page values, Link relations, page + counts, record counts, cumulative response bytes, and deadline. +- Continue rejecting pagination links for every other repository, including a + different numeric repository ID. +- Resume the live consolidation inspection without an override, compatibility + shim, synthesized pagination, or release-specific exception. + +## Non-goals + +- Following arbitrary GitHub API paths or redirects. +- Relaxing the workflow-run query, status set, page size, or maximum page + count. +- Ignoring the Link header or calculating later-page URLs without validating + GitHub's pagination graph. +- Changing consolidation authority, deletion authorization, or the approved + v0.8.22 release identities. + +## Approaches Considered + +### 1. Trust an exact union of repository paths + +Accept the existing owner/name path and the canonical numeric path containing +the fixed Dawn GitHub repository ID. Continue applying every current URL and +pagination invariant. This is the recommended approach because it models the +two equivalent identities GitHub actually returns without widening trust to +another repository or endpoint. + +### 2. Send workflow-run requests through the numeric path + +Changing the outbound request to use only the numeric path may make today's +response links textually identical to the request. It does not establish that +GitHub will always preserve that representation, and it unnecessarily changes +the request boundary. It therefore does not solve the underlying identity +model as directly. + +### 3. Ignore or synthesize pagination links + +The reader could disregard GitHub's Link header and derive page URLs locally. +That would remove validation of the server's pagination graph and weaken a +release-safety boundary. This approach is rejected. + +## Design + +The reader has two Link-validation layers: a transport-level graph validator +that protects all GitHub API responses and a workflow-specific verifier that +checks the release-workflow page shape. Both layers must apply the same narrow +identity rule for this endpoint. + +The workflow pagination rule will recognize exactly two pathnames: + +1. the current path derived from the trusted owner/name workflow-run URL; and +2. `/repositories/1210070282/actions/workflows/` followed by the same exactly + encoded release workflow path and `/runs` suffix. + +The numeric path is bound to the same fixed repository ID already required by +the consolidation authority capture. It is not parsed as an arbitrary numeric +identifier and is not learned from an untrusted response. + +At the transport layer, the alternate numeric pathname is valid only when the +current request is the fixed owner/name release-workflow endpoint. The +transport validator will canonicalize either accepted representation to the +owner/name page URL before it records relation targets and checks compatible +`next`/`last` or `prev`/`first` aliases. Generic pagination for every other API +endpoint will retain its existing exact-request-path rule. + +The workflow-specific verifier will apply the same pathname union and return +the canonical owner/name page URL. This preserves the enumerator's exact +comparison with the expected next owner/name page. The enumerator will +continue to construct and request that expected URL rather than following a +response URL directly. + +At both layers, all validation after pathname recognition remains unchanged. A +trusted URL must use the exact GitHub API HTTPS origin, contain no user info or +fragment, and have exactly one `per_page=100` field and one positive decimal +`page` field with no other query parameters. The Link graph must retain its +current unique and compatible relation rules. + +This is a representation fix at the authenticated GitHub reader boundary. It +does not change the authority captured by the consolidation operation or grant +the response control over a request target. + +## Error Handling + +- A numeric path containing any repository ID other than `1210070282` is + untrusted. +- A different repository name, workflow path, origin, protocol, query shape, + page size, or malformed page remains untrusted. +- A numeric repository path attached to any request other than the fixed + owner/name release-workflow endpoint remains untrusted. +- Missing, repeated, aliased, or contradictory Link relations continue to + fail closed. +- Unstable totals, duplicate run IDs, inconsistent page lengths, excessive + pages, byte-budget exhaustion, and deadline exhaustion remain unchanged. + +## Testing and Verification + +Tests will first demonstrate the current failure, then cover: + +- successful two-page enumeration when GitHub uses the numeric repository path + for compatible `next`/`last` and `prev`/`first` links; +- rejection of the same path with a foreign numeric repository ID; +- rejection of numeric repository aliases by generic pagination endpoints; +- continued success for the existing owner/name path; and +- continued rejection of all existing malformed, cross-origin, extra-query, + and contradictory Link cases. + +Verification will run the focused adapter tests, the complete duplicate-draft +consolidation test suite, scoped formatting and documentation checks, and the +repository Definition of Done with Docker required. The exact PR head will be +reviewed by GitHub Copilot before merge. After merge, the dedicated release +worktree will be reset to the exact verified `main` commit and the live inspect +command will be rerun. Any proposal produced by that inspection must be +independently checked for private file mode, canonical envelope, survivor and +duplicate IDs, candidate commit, and digest before a separate perform decision. From a25980d8634cd207f234d8d9132a8dd2c7f3d671 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 09:41:51 -0700 Subject: [PATCH 2/4] docs: plan workflow pagination identity fix --- ...09-02-consolidation-workflow-pagination.md | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-02-consolidation-workflow-pagination.md diff --git a/docs/superpowers/plans/2026-09-02-consolidation-workflow-pagination.md b/docs/superpowers/plans/2026-09-02-consolidation-workflow-pagination.md new file mode 100644 index 000000000..fa9897b39 --- /dev/null +++ b/docs/superpowers/plans/2026-09-02-consolidation-workflow-pagination.md @@ -0,0 +1,325 @@ +# Consolidation Workflow Pagination Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow duplicate-draft consolidation inspection to validate GitHub's canonical numeric-repository workflow pagination links without weakening any other pagination or release-safety boundary. + +**Architecture:** Add one workflow-specific canonicalization rule shared by the transport Link-graph validator and the workflow-run enumerator. It accepts only the existing owner/name path or the fixed Dawn repository-ID path, returns the owner/name page URL for both, and leaves generic GitHub pagination behavior unchanged. + +**Tech Stack:** Node.js 24, ESM, built-in `node:test`, pnpm, Biome, GitHub CLI + +--- + +## File structure + +- Modify `scripts/release/duplicate-draft-consolidation-adapters.mjs` — bind + the fixed repository ID, canonicalize the two exact workflow page + representations, and apply that canonical form at both Link-validation + layers. +- Modify `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs` + — prove the live numeric Link graph succeeds and that foreign IDs and + unrelated endpoints remain rejected. +- Create this plan only; no release workflow, dependency, controller hash, or + Vercel lane changes are in scope. + +### Task 1: Lock the numeric workflow Link behavior with regressions + +**Files:** +- Test: `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs:1099-1190` + +- [ ] **Step 1: Add fixed numeric workflow test URLs** + +Near the existing GitHub test constants, add the public repository identity +and exact numeric workflow base: + +```js +const REPOSITORY_ID = "1210070282" +const NUMERIC_WORKFLOW_BASE = + `${API_ORIGIN}/repositories/${REPOSITORY_ID}/actions/workflows/` + + ".github%2Fworkflows%2Frelease.yml/runs" +``` + +- [ ] **Step 2: Write the failing live-shape regression** + +Add a two-page test beside +`workflow-run pagination accepts compatible next-last and prev-first aliases`. +Return numeric repository URLs in all four Link relations but keep the recorded +outbound requests on the existing owner/name URLs: + +```js +test("workflow-run pagination canonicalizes GitHub numeric repository links", async () => { + const namedFirst = + `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=1` + const namedSecond = + `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + const numericFirst = `${NUMERIC_WORKFLOW_BASE}?per_page=100&page=1` + const numericSecond = `${NUMERIC_WORKFLOW_BASE}?per_page=100&page=2` + const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const recording = recordingFetch([ + jsonResponse({ total_count: 101, workflow_runs: page }, 200, { + Link: `<${numericSecond}>; rel="next", <${numericSecond}>; rel="last"`, + }), + jsonResponse({ total_count: 101, workflow_runs: [workflowRun(101)] }, 200, { + Link: `<${numericFirst}>; rel="prev", <${numericFirst}>; rel="first"`, + }), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + const result = await adapters.github.listNonterminalWorkflowRuns(workflowQuery()) + + assert.equal(result.runs.length, 101) + assert.deepEqual( + recording.calls.map(({ url }) => url), + [namedFirst, namedSecond], + ) +}) +``` + +- [ ] **Step 3: Run the regression and verify RED** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test \ + --test-name-pattern='workflow-run pagination canonicalizes GitHub numeric repository links' \ + scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +``` + +Expected: FAIL because the transport validator replaces the mixed-path Link +graph with `malformed` before the workflow reader can accept it. + +- [ ] **Step 4: Add fail-closed negative coverage** + +Extend the exact trusted-workflow-Link rejection table with a numeric URL whose +repository ID is not `1210070282`. + +Add a separate transport-sensitive generic Release regression whose response +has an otherwise-valid numeric workflow URL as `rel="last"` with no `next`: + +```js +const adapters = await createAdapters({ + fetchImpl: async () => + jsonResponse([], 200, { + Link: `<${NUMERIC_WORKFLOW_BASE}?per_page=100&page=2>; rel="last"`, + }), + run: commandRunner([]), +}) +assert.deepEqual(await adapters.github.listReleases(), { + status: "ERROR", + operation: "releases", + httpStatus: 200, + code: "MALFORMED_LINK_HEADER", +}) +``` + +The `last`-only graph is deliberate: the generic reader has no next page to +reject downstream. The assertion therefore proves the transport layer did not +activate workflow canonicalization for an unrelated Release request. These +tests must not add a new escape hatch or compatibility option. + +- [ ] **Step 5: Run the focused adapter file and confirm only the new positive case is red** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test \ + scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +``` + +Expected: the numeric positive regression fails; the foreign-ID and generic +`last`-only regressions pass, as do all existing owner/name and rejection cases. + +### Task 2: Canonicalize the fixed workflow identity at both validation layers + +**Files:** +- Modify: `scripts/release/duplicate-draft-consolidation-adapters.mjs:24-45` +- Modify: `scripts/release/duplicate-draft-consolidation-adapters.mjs:2281-2333` +- Modify: `scripts/release/duplicate-draft-consolidation-adapters.mjs:2466-2514` +- Test: `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs` + +- [ ] **Step 1: Bind the repository ID and exact path union** + +Add `const REPOSITORY_ID = "1210070282"` beside `REPOSITORY`. In the workflow +page validator, derive the owner/name path from `workflowRunsUrl(1)` and build +only this numeric alternative: + +```js +const numericPath = + `/repositories/${REPOSITORY_ID}/actions/workflows/` + + `${encodeURIComponent(RELEASE_WORKFLOW)}/runs` +``` + +Reject every pathname outside that two-member set. Keep the exact HTTPS API +origin, empty credentials and fragment, two-query-entry, single +`per_page=100`, and positive-decimal `page` checks. + +- [ ] **Step 2: Return one canonical workflow page URL** + +After successful validation, return the existing owner/name representation +instead of `url.href`: + +```js +return workflowRunsUrl(url.searchParams.get("page"), url.searchParams.get("per_page")) +``` + +This also fixes query ordering to the enumerator's constructed form. Do not +convert `page` through `Number`; retain the validated decimal string. + +- [ ] **Step 3: Add a non-throwing canonicalization wrapper for the transport layer** + +Keep `exactWorkflowPageUrl`'s current throwing contract for the workflow +reader. Add a private wrapper for the transport graph: + +```js +function canonicalWorkflowPageUrl(value) { + try { + return exactWorkflowPageUrl(value) + } catch { + return null + } +} +``` + +Determine whether the current request is the fixed owner/name workflow page by +requiring its pathname to equal the owner/name path and its full URL to equal +its canonical workflow page URL. A numeric request URL must not activate the +alias rule. + +- [ ] **Step 4: Canonicalize every workflow Link relation before graph checks** + +In `validPaginationLinkGraph`, preserve the current generic branch exactly. +Only for the fixed owner/name workflow request: + +1. require every relation target to pass `canonicalWorkflowPageUrl`; +2. use the returned owner/name URL as the key passed to + `addTargetRelation`; and +3. run `hasCompatibleSharedLinkTargets` over those canonical keys. + +This must canonicalize `next`, `last`, `prev`, and `first`, so equivalent +numeric and owner/name representations cannot evade or falsely fail shared +target checks. The inner `workflowNextUrl` will apply the same canonicalizer +and its existing exact comparison to `workflowRunsUrl(page + 1)` will remain +unchanged. + +- [ ] **Step 5: Run the new regression and verify GREEN** + +Run the exact command from Task 1 Step 3. Expected: PASS, with recorded requests +remaining the owner/name first and second pages. + +- [ ] **Step 6: Run the full adapter test file** + +Run: + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test \ + scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +``` + +Expected: all adapter tests pass, including existing owner/name pagination, +malformed graphs, cumulative byte limits, and deadline limits. + +- [ ] **Step 7: Commit the tested implementation** + +```bash +git add scripts/release/duplicate-draft-consolidation-adapters.mjs \ + scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +git commit -m "fix(release): accept canonical workflow pagination links" +``` + +### Task 3: Verify the complete release boundary and merge the focused PR + +**Files:** +- Verify: `scripts/release/duplicate-draft-consolidation-adapters.mjs` +- Verify: `scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs` +- Verify: `docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md` +- Verify: `docs/superpowers/plans/2026-09-02-consolidation-workflow-pagination.md` + +- [ ] **Step 1: Run the complete consolidation suite** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node --test \ + scripts/release/test/duplicate-draft-consolidation*.test.mjs +``` + +Expected: every consolidation test passes. + +- [ ] **Step 2: Run scoped static checks** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH pnpm exec biome check \ + --config-path packages/config-biome/biome.json \ + scripts/release/duplicate-draft-consolidation-adapters.mjs \ + scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH node scripts/check-docs.mjs +git diff --check origin/main...HEAD +``` + +Expected: all checks pass and the branch diff contains only the approved spec, +plan, adapter, and adapter test. + +- [ ] **Step 3: Run the repository Definition of Done** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ + DAWN_REQUIRE_DOCKER=1 pnpm ci:validate +``` + +Expected: every required local gate passes, including the full build, release +controller, packaging, TypeScript tooling, and harness lanes. + +- [ ] **Step 4: Commit the reviewed plan and any verification-only updates** + +Commit only remaining intentional tracked changes. Do not amend the tested +implementation after review begins. + +- [ ] **Step 5: Push a focused PR and request GitHub Copilot** + +Push `blove/fix-consolidation-workflow-pagination`, open a PR against `main`, +and request `copilot-pull-request-reviewer[bot]` on the exact head through the +GitHub CLI. Require Copilot to finish with no unresolved Critical or Important +finding. Re-run affected tests after any review change. + +- [ ] **Step 6: Require exact-head CI and merge** + +Require all mandatory checks plus the real `vercel-native` lane on the exact +reviewed head. Treat infrastructure-only retries separately and preserve their +evidence. Merge only when the PR is mergeable and required checks are green. + +### Task 4: Resume the live read-only consolidation inspection + +**Files:** +- Live private output: `.dawn/release/duplicate-draft-consolidation.proposed.json` + +- [ ] **Step 1: Return the dedicated worktree to exact merged `main`** + +Fetch `origin`, switch this worktree to symbolic `main`, fast-forward only, and +confirm local HEAD, `origin/main`, and GitHub's default-branch SHA are +identical. Require a clean status, the expected `cacheplane/dawnai` origin, the +release workflow still `disabled_manually`, and zero nonterminal release runs. + +- [ ] **Step 2: Run the exact production inspection** + +```bash +PATH=/Users/blove/.nvm/versions/node/v24.19.0/bin:$PATH \ + pnpm release:consolidate-drafts inspect \ + --version 0.8.22 \ + --commit-sha 2a80deece2ff958fe7fde8fddeb4f99bed70a1c8 \ + --survivor 379991871 \ + --duplicates 379982100,379986168 \ + --output .dawn/release/duplicate-draft-consolidation.proposed.json +``` + +Expected: inspection confirms zero nonterminal Release runs, performs no +writer call, and writes one canonical private proposal. + +- [ ] **Step 3: Independently validate the proposal before any perform decision** + +Confirm the proposal is a regular no-follow file with mode `0600`, parses as +the canonical proposed envelope, and contains the exact repository, version, +candidate commit, survivor ID, ordered duplicate IDs, and printed record +SHA-256. Confirm no journal exists and no GitHub Release or npm state changed. +Stop at this read-only checkpoint and report the evidence before authorizing a +separate live deletion. From e557757c8345278e333a14e95fa90c13b96663e9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 10:02:25 -0700 Subject: [PATCH 3/4] fix(release): accept canonical workflow pagination links --- ...duplicate-draft-consolidation-adapters.mjs | 27 ++++++++++- ...cate-draft-consolidation-adapters.test.mjs | 47 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/scripts/release/duplicate-draft-consolidation-adapters.mjs b/scripts/release/duplicate-draft-consolidation-adapters.mjs index e1fbcda5c..f9a8d587d 100644 --- a/scripts/release/duplicate-draft-consolidation-adapters.mjs +++ b/scripts/release/duplicate-draft-consolidation-adapters.mjs @@ -38,6 +38,7 @@ import { createOwnerPreflightAdapters as defaultCreateOwnerPreflightAdapters } f import { createReleasePreparationRunner as defaultCreateReleasePreparationRunner } from "./process-runner.mjs" const REPOSITORY = "cacheplane/dawnai" +const REPOSITORY_ID = "1210070282" const OWNER = "cacheplane" const REPO = "dawnai" const API_ORIGIN = "https://api.github.com" @@ -2314,6 +2315,7 @@ function exactWorkflowPageUrl(value) { throw new TypeError("GitHub workflow-run Link URL is malformed") } const expectedPath = new URL(workflowRunsUrl(1)).pathname + const numericRepositoryPath = `/repositories/${REPOSITORY_ID}/actions/workflows/${encodeURIComponent(RELEASE_WORKFLOW)}/runs` const entries = [...url.searchParams] if ( url.origin !== API_ORIGIN || @@ -2321,7 +2323,7 @@ function exactWorkflowPageUrl(value) { url.username !== "" || url.password !== "" || url.hash !== "" || - url.pathname !== expectedPath || + (url.pathname !== expectedPath && url.pathname !== numericRepositoryPath) || entries.length !== 2 || url.searchParams.getAll("per_page").length !== 1 || url.searchParams.get("per_page") !== "100" || @@ -2330,7 +2332,15 @@ function exactWorkflowPageUrl(value) { ) { throw new TypeError("GitHub workflow-run Link URL is not trusted") } - return url.href + return workflowRunsUrl(url.searchParams.get("page"), url.searchParams.get("per_page")) +} + +function canonicalWorkflowPageUrl(value) { + try { + return exactWorkflowPageUrl(value) + } catch { + return null + } } function normalizeAttestationResult(value) { @@ -2485,7 +2495,20 @@ function validPaginationLinkGraph(value, requestUrl) { if (graph === null) return false const relations = new Set() const targetRelations = new Map() + const expectedWorkflowPath = new URL(workflowRunsUrl(1)).pathname + const canonicalRequestUrl = canonicalWorkflowPageUrl(requestUrl) + const workflowAliasesEnabled = + requestUrl.pathname === expectedWorkflowPath && + canonicalRequestUrl !== null && + requestUrl.href === canonicalRequestUrl for (const entry of graph) { + if (workflowAliasesEnabled) { + const url = canonicalWorkflowPageUrl(entry.url) + if (url === null || relations.has(entry.relation)) return false + relations.add(entry.relation) + addTargetRelation(targetRelations, url, entry.relation) + continue + } const url = normalizedAbsoluteUrl(entry.url) if ( url === null || diff --git a/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs b/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs index 107fd2d17..f0362b3e7 100644 --- a/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs +++ b/scripts/release/test/duplicate-draft-consolidation-adapters.test.mjs @@ -10,6 +10,8 @@ import { createAuthorizedDeleteHarness } from "./support/duplicate-draft-consoli const REPOSITORY = "cacheplane/dawnai" const API_ORIGIN = "https://api.github.com" +const REPOSITORY_ID = "1210070282" +const NUMERIC_WORKFLOW_BASE = `${API_ORIGIN}/repositories/${REPOSITORY_ID}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs` const BASE = `${API_ORIGIN}/repos/${REPOSITORY}` const SURVIVOR = "379991871" const DUPLICATES = Object.freeze(["379982100", "379986168"]) @@ -1096,6 +1098,23 @@ test("GitHub reader preserves fail-closed pagination and transport classificatio } }) +test("release pagination rejects numeric workflow Link targets outside workflow transport", async () => { + const adapters = await createAdapters({ + fetchImpl: async () => + jsonResponse([], 200, { + Link: `<${NUMERIC_WORKFLOW_BASE}?per_page=100&page=2>; rel="last"`, + }), + run: commandRunner([]), + }) + + assert.deepEqual(await adapters.github.listReleases(), { + status: "ERROR", + operation: "releases", + httpStatus: 200, + code: "MALFORMED_LINK_HEADER", + }) +}) + test("workflow-run enumeration rejects unstable totals, duplicate IDs, and bounds", async () => { const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) const next = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` @@ -1134,6 +1153,7 @@ test("workflow-run enumeration requires one exact trusted Link next relation", a `; rel="next"`, `<${BASE}/issues?per_page=100&page=2>; rel="next"`, `<${endpoint}&extra=true>; rel="next"`, + `<${API_ORIGIN}/repositories/1210070283/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2>; rel="next"`, `<${endpoint}>; rel="next prev"`, `<${endpoint}>; rel="next", <${endpoint}>; rel="prev"`, `<${endpoint}>; rel="next", <${endpoint}>; rel="first"`, @@ -1187,6 +1207,33 @@ test("workflow-run pagination accepts compatible next-last and prev-first aliase ) }) +test("workflow-run pagination canonicalizes GitHub numeric repository links", async () => { + const firstPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=1` + const secondPageUrl = `${BASE}/actions/workflows/.github%2Fworkflows%2Frelease.yml/runs?per_page=100&page=2` + const numericFirstPageUrl = `${NUMERIC_WORKFLOW_BASE}?per_page=100&page=1` + const numericSecondPageUrl = `${NUMERIC_WORKFLOW_BASE}?per_page=100&page=2` + const page = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) + const recording = recordingFetch([ + jsonResponse({ total_count: 101, workflow_runs: page }, 200, { + Link: `<${numericSecondPageUrl}>; rel="next", <${numericSecondPageUrl}>; rel="last"`, + }), + jsonResponse({ total_count: 101, workflow_runs: [workflowRun(101)] }, 200, { + Link: `<${numericFirstPageUrl}>; rel="prev", <${numericFirstPageUrl}>; rel="first"`, + }), + ]) + const adapters = await createAdapters({ + fetchImpl: recording.fetchImpl, + run: commandRunner([]), + }) + + const result = await adapters.github.listNonterminalWorkflowRuns(workflowQuery()) + assert.equal(result.runs.length, 101) + assert.deepEqual( + recording.calls.map(({ url }) => url), + [firstPageUrl, secondPageUrl], + ) +}) + test("workflow-run pagination enforces one cumulative raw-byte budget", async () => { const firstPage = Array.from({ length: 100 }, (_unused, index) => workflowRun(index + 1)) const secondPage = [workflowRun(101)] From 217f4c6eb97b5192c0040b30c618e8d6603e14c6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 10:45:30 -0700 Subject: [PATCH 4/4] docs: mark pagination design implemented --- .../2026-09-02-consolidation-workflow-pagination-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md b/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md index e7e1ea424..c2ef096dd 100644 --- a/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md +++ b/docs/superpowers/specs/2026-09-02-consolidation-workflow-pagination-design.md @@ -2,7 +2,7 @@ ## Status -Approved design; implementation pending. +Approved and implemented. ## Problem