Skip to content

Commit f728ecd

Browse files
chrisleekr-bot[bot]chrisleekr-bot[bot]claude
authored
fix(fetcher): paginate GraphQL connections + MAX_FETCHED_* caps (closes #66) (#95)
Co-authored-by: chrisleekr-bot[bot] <2890262+chrisleekr-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com>
1 parent 95856bc commit f728ecd

11 files changed

Lines changed: 1084 additions & 215 deletions

File tree

IMPLEMENT.md

Lines changed: 41 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,56 @@
1-
# IMPLEMENT — Issue #58 (supply-chain attestations)
1+
# Implement: issue #66 — paginate GraphQL fetcher + MAX_FETCHED\_\* caps
22

33
## Summary
44

5-
Wires SLSA v1 build provenance + CycloneDX/SPDX SBOMs into every Docker image
6-
the release pipeline publishes, and adds a hard regression gate that fails the
7-
workflow if either attestation is silently dropped by a future refactor. All
8-
changes are additive YAML (`.github/workflows/docker-build.yml`) plus matching
9-
docs — no `Dockerfile.*`, `package.json`, or `src/` change. Closes #58.
10-
11-
The fix has two layers:
12-
13-
1. **BuildKit-native** (`provenance: mode=max` + `sbom: true` on the build
14-
step): each per-arch leaf push gets an in-toto SLSA v1 provenance manifest
15-
and an SPDX 2.3 SBOM stored as OCI subject descriptors, and the merge
16-
step's `imagetools create` walks each index digest so the descriptors
17-
survive the manifest-list assembly without any extra CLI plumbing.
18-
2. **GitHub-Sigstore** (`actions/attest-build-provenance@v4` +
19-
`actions/attest-sbom@v4` after the merge): bind Sigstore-signed
20-
attestations to the merged manifest digest, surfaced via the GitHub
21-
Attestations API and Docker Hub's "Build attestations" badge. The
22-
CycloneDX SBOM that flows into `attest-sbom` is generated from the
23-
merged image by `anchore/sbom-action@v0`.
24-
25-
The `scan` job runs `gh attestation verify` for both predicate types
26-
(`https://slsa.dev/provenance/v1` + `https://cyclonedx.org/bom`) before Trivy
27-
on every release tag — two separate calls so each predicate must exist.
5+
Closes #66 (`fix(pipeline): GraphQL fetcher silently truncates PR/issue context past 100 items`).
6+
7+
`src/core/fetcher.ts` previously issued single-page GraphQL requests with `first: 100` on every connection (issue/PR comments, reviews, the inline comments nested under each review, and changed files). Anything past the first 100 items was silently dropped before the data ever reached the agent — the prompt looked complete but was missing context, which is the failure mode the issue calls out.
8+
9+
The fix:
10+
11+
1. Every paginated query exposes a single cursor variable named exactly `$cursor` and selects `pageInfo { hasNextPage endCursor }` on its one paginated connection. Both names are part of `@octokit/plugin-paginate-graphql`'s contract: the plugin hard-codes the cursor parameter name (`iterator.js`) and runs a depth-first search for the first `pageInfo` it finds (`object-helpers.js`), so multi-connection / aliased-cursor queries silently truncate or throw. The PR fetch is therefore split into three parallel `paginate(...)` calls (`PR_FIRST_QUERY` for top-level scalars + files, `PR_COMMENTS_QUERY`, `PR_REVIEWS_QUERY`); the issue fetch uses one. The plugin is bundled with `octokit ^5.0.5` and exposed on every `octokit.graphql` instance.
12+
2. A separate `REVIEW_COMMENTS_QUERY` walks each review's overflow inline comments keyed on the review node ID — the nested per-review pagination cannot ride along on `PR_REVIEWS_QUERY` because of contract-rule #1.
13+
3. Four new env vars (`MAX_FETCHED_COMMENTS` / `_REVIEWS` / `_REVIEW_COMMENTS` / `_FILES`, default `500` each) cap the merged result. When a cap fires the fetcher emits `log.warn({ connection, fetched, cap })` and sets `FetchedData.truncated.<connection> = true`.
14+
4. `buildPrompt` in `src/core/prompt-builder.ts` reads `data.truncated` and prepends a `WARNING: pre-fetched context is incomplete…` banner naming the affected connections, so the agent knows to reach for the GitHub CLI when it needs the missing items.
15+
5. The TOCTOU filter (`filterByTriggerTime`) runs after the paginate merge, so its semantics are unchanged.
16+
17+
## Plan deviations
18+
19+
- **T1 / T2 (install + thread plugin) skipped as no-ops.** `octokit ^5.0.5` already bundles `@octokit/plugin-paginate-graphql` and exposes `octokit.graphql.paginate` on every existing instance. Verified via lockfile inspection and a runtime probe. Threading a shared factory through 11 instantiation sites would have been pure churn for zero behaviour change.
2820

2921
## Files changed
3022

31-
- `.github/workflows/docker-build.yml` · primary subject of the issue —
32-
enables BuildKit attestations on the build step (T1), scopes
33-
`id-token: write` + `attestations: write` to the merge job only (T3),
34-
captures the merged manifest digest, generates a CycloneDX SBOM, and
35-
publishes Sigstore-signed provenance + SBOM attestations after the
36-
manifest-list push (T4); adds a `gh attestation verify` regression gate
37-
to the scan job before Trivy (T5). Top-level perms gain
38-
`attestations: read` so build/scan stay read-only; merge overrides locally.
39-
- `docs/operate/deployment.md` · new "Verifying image attestations"
40-
subsection under "Build" with consumer-side `gh attestation verify` and
41-
`docker buildx imagetools inspect` recipes covering both attestation
42-
flavours and both image variants (T7).
43-
- `docs/operate/observability.md` · new "Supply-chain attestations" section
44-
documenting the registry / Sigstore / GitHub-API storage matrix and
45-
pointing operators at the consumer commands in `deployment.md` (T8).
46-
- `CLAUDE.md` · "Owns" cell for `docker-build.yml` updated to mention SLSA + SBOM attestations and the `gh attestation verify` regression gate; one-line `20260502-supply-chain-attestations` entry in "Recent Changes" (T9).
23+
- `src/core/fetcher.ts` · rewrote both queries to select `pageInfo`, switched to `graphql.paginate`, added `applyCap()` + nested review-comment follow-up; emits structured warn logs and sets `FetchedData.truncated` flags.
24+
- `src/types.ts` · extended `FetchedData` with `truncated?: { comments?, reviews?, reviewComments?, changedFiles? }`.
25+
- `src/config.ts` · added 4 zod fields + env wiring (`MAX_FETCHED_COMMENTS` / `_REVIEWS` / `_REVIEW_COMMENTS` / `_FILES`, default `500`).
26+
- `src/core/prompt-builder.ts` · added `buildTruncationBanner` and injected it into the prompt when any flag is set.
27+
- `test/core/fetcher.test.ts` · new tests covering pagination merge (length > 100), TOCTOU after merge, cap fire (log + flag), nested review-comment pagination, and banner injection.
28+
- `test/factories.ts` · extended `makeOctokit` with `graphqlPaginateResponses` (substring-keyed routing on the paginate fn).
29+
- `docs/operate/configuration.md` · 4 new rows documenting the env vars.
30+
- `docs/operate/observability.md` · new "Data fetching safety caps" section documenting the warn log shape and prompt banner.
4731

4832
## Commits
4933

50-
See the PR commit list — short SHAs and conventional-commit subjects are
51-
visible there.
34+
- `fix(fetcher): paginate GraphQL connections + MAX_FETCHED_* caps (closes #66)` — single commit, branch `fix/issue-66-paginate-graphql`.
5235

5336
## Tests run
5437

55-
- `bun run typecheck` · pass (no TypeScript output, exit 0)
56-
- `bun run lint` · pass (0 errors, 289 pre-existing warnings — none new from this change)
57-
- `bun run format` · pass after `bun run format:fix` re-flowed two doc tables
58-
- `actionlint .github/workflows/*.yml` · pass (no output, all workflows lint-clean)
59-
- `bun run scripts/check-docs-citations.ts` · pass (every `src/<file>:<line>` citation in-range)
60-
- `bun run scripts/check-docs-versions.ts` · pass (Bun version pins consistent with `.tool-versions`)
61-
- `mkdocs build --strict` · pass (`Documentation built in 0.58 seconds`, no warnings)
62-
- `bun test` · 519 pass / 153 skip / **194 pre-existing fail** — verified
63-
baseline by `git stash && bun test` before reapplying my diff: same
64-
pass/fail counts. The failing suites need Postgres + Valkey
65-
(`bun run dev:deps`); they are unrelated to YAML / Markdown changes here.
38+
- `bun run typecheck` · clean.
39+
- `NODE_OPTIONS='--max-old-space-size=4096' bunx eslint .` · 0 errors / 291 warnings (all pre-existing return-type warnings in unrelated files).
40+
- `bun run format` · clean.
41+
- `bun test test/core/fetcher.test.ts` · 30 pass / 0 fail / 64 expect calls.
42+
- `bun run scripts/check-docs-citations.ts` · clean.
43+
- `bun run scripts/check-docs-versions.ts` · clean.
44+
- `bun run docs:build` · skipped locally (`mkdocs` not installed in workspace); runs in CI via `.github/workflows/docs.yml`.
45+
- `bun run test` (isolated runner): 78 files passed, 25 files skipped because Postgres / Valkey are not running in this workspace (pre-existing infrastructure dependency, none of the skipped files were touched by this PR).
6646

6747
## Verification
6848

69-
Each task in the plan is satisfied as follows:
70-
71-
- **T1**`.github/workflows/docker-build.yml:160-169` adds
72-
`provenance: mode=max` and `sbom: true`. Inline comment cites the
73-
`push-by-digest` default-off behaviour the issue called out and links the
74-
Docker multi-platform guide. BuildKit will emit per-arch attestation
75-
manifests alongside each leaf image push.
76-
77-
- **T2** — Merge step preserved as-is (`imagetools create` already walks
78-
the per-arch index digests, which now reference both image AND
79-
attestation manifests via the BuildKit emission from T1). Inline comment
80-
at `.github/workflows/docker-build.yml:248-256` explains why no CLI
81-
plumbing is needed; this matches the documented Docker multi-platform
82-
pattern.
83-
84-
- **T3** — Top-level adds `attestations: read` (line 61) so the scan job
85-
inherits read-only verification scope. Merge job overrides locally
86-
(lines 199-202) with `id-token: write` + `attestations: write`
87-
least-privilege; build/scan retain only the top-level grants.
88-
89-
- **T4** — After `Create manifest list and push`, the workflow now:
90-
1. `Inspect merged image and capture digest` (lines 261-274) — captures
91-
the index digest with regex validation so a malformed parse fails
92-
fast rather than silently passing a bad subject to attest-\*.
93-
2. `Generate CycloneDX SBOM for merged image` (lines 276-283) using
94-
`anchore/sbom-action@v0` — syft-backed, produces CycloneDX JSON.
95-
3. `Attest build provenance` (lines 285-290) — Sigstore-signed in-toto
96-
SLSA v1 attestation pushed to the registry as a sibling descriptor
97-
on the merged manifest digest.
98-
4. `Attest SBOM` (lines 292-298) — Sigstore-signed CycloneDX SBOM
99-
attestation, same subject digest.
100-
101-
- **T5** — Scan job's new `Verify image attestations` step (lines 329-343)
102-
calls `gh attestation verify` twice with explicit `--predicate-type`
103-
filters for SLSA provenance and CycloneDX SBOM. Each call fails the job
104-
if its predicate type is absent — so dropping either attestation in a
105-
future refactor will break the release before Trivy runs.
106-
107-
- **T6 (deviation noted)** — Plan said to SHA-pin in addition to the major
108-
tag. Repo-wide `Grep` for `uses: .+@[0-9a-f]{40}` returned zero matches:
109-
every workflow uses tag-only pinning at the major version, with Renovate
110-
handling bumps via the `github-actions` group rule in `renovate.json`. I
111-
followed the **existing repo posture** (tag-only major-version pins for
112-
`actions/attest-build-provenance@v4`, `actions/attest-sbom@v4`,
113-
`anchore/sbom-action@v0`) over the plan's SHA-pin recommendation, since
114-
adding SHAs only here would be immediately undone by the next Renovate
115-
run and breaks consistency with the other 11 actions in the file.
116-
Renovate's `github-actions` group will pick up bumps weekly. Also bumped
117-
the action major versions from the plan's `@v3` to `@v4` because v4.x
118-
has been GA since 2026-02-26 (today: 2026-05-02) and the repo otherwise
119-
tracks current major versions for actions (`checkout@v6`,
120-
`build-push-action@v7`, `download-artifact@v8`).
121-
122-
- **T7-T8**`docs/operate/deployment.md` and `docs/operate/observability.md`
123-
updated with consumer verification commands and storage-surface matrix;
124-
cross-linked. `mkdocs build --strict` passes; the project's bespoke
125-
citation / version checks pass.
126-
127-
- **T9**`CLAUDE.md` CI/CD row updated and a `20260502-…` "Recent Changes"
128-
entry added. Format auto-fix re-flowed the table column widths, expected.
129-
130-
- **T10 (deferred — out-of-band verification)** — End-to-end smoke test via
131-
`gh workflow run docker-build.yml` against a dev tag is the maintainer's
132-
call to schedule (it pushes a real image to Docker Hub and consumes an
133-
attestations-API quota). All YAML / docs gates that _can_ run locally
134-
pass; the actual attestation-emit / verify behaviour is the maintainer's
135-
smoke test on first dev release after merge.
136-
137-
### Security posture preserved
138-
139-
- Top-level `permissions:` only grew by `attestations: read` (least
140-
required to verify); the existing `contents: read` and
141-
`security-events: write` are unchanged.
142-
- The merge job's elevated scopes (`id-token: write`,
143-
`attestations: write`) are confined to that one job — build and scan
144-
cannot mint Sigstore tokens or write attestations.
145-
- `gh attestation verify` runs with `secrets.GITHUB_TOKEN` (the default
146-
job token), no PAT.
147-
- All dynamic inputs flowing into `run:` blocks remain passed via `env:`
148-
first (defense-in-depth posture from CLAUDE.md preserved — the new
149-
steps follow the same pattern, e.g. `IMAGE_REF` / `REPO` / `TAG` /
150-
`IMAGE` / `DIGEST` env mappings).
49+
- **Acceptance criterion: PRs/issues with > 100 comments / reviews / changed files no longer truncate silently.** New test "merges paginated issue comments into FetchedData (length > 100)" proves the merge for 250 comments; "merges paginated review comments across nested pageInfo" proves the nested review-comment merge (100 + 50 = 150).
50+
- **Acceptance criterion: a hard cap protects the prompt window.** `applyCap` clamps to `config.maxFetchedComments` (etc.), emits a structured warn, and sets `truncated.<connection> = true`. Test "logs warn and sets truncated flag when MAX_FETCHED cap fires" asserts all three (length, log fields, flag).
51+
- **Acceptance criterion: the agent must know when context is incomplete.** `buildPrompt` injects a `WARNING: pre-fetched context is incomplete…` banner naming the affected connections. Test "buildPrompt includes truncation banner when truncated flag is set" asserts the banner.
52+
- **TOCTOU semantics preserved.** Test "applies filterByTriggerTime AFTER pagination merge" sets `triggerTimestamp` to comment-240 of 600 and asserts comment-239 (newest pre-trigger) survives while comments 240+ are dropped.
53+
54+
### Intentionally NOT done
55+
56+
- T1 / T2 (paginate-graphql install + threaded factory). Plugin is already bundled with `octokit ^5.0.5` and `octokit.graphql.paginate` is present on every existing instance. Documented above under "Plan deviations".

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)