Skip to content

fix(pr-pipeline): treat an incomplete GitHub search as unknown - #9089

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/search-incomplete-results
Oct 22, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
fix/search-incomplete-results

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

No related issue: GitHub search returned incomplete empty results during the 2026-10-22 Actions incident

Summary

During today's incident the search API answered total_count: 0 with incomplete_results: true for repo:melodic-software/claude-code-plugins is:issue is:open (the real count is about 601). The in-scope lane callers of GitHub search were audited: check-wip-cap.mjs is the one REST /search/issues caller, and it already turned an incomplete answer into a failed read. A failed queued read left admissions open (admission_basis: fail-open), so work-unblock's admissionGate got unlimited room (admission_room ?? Infinity) on a false empty.

The owner approved this behavior on 2026-10-22 ("pause admissions when GitHub search reports an incomplete answer"). It partly reverses the documented fail-open admission rule (#8804) for one case, an answer that says incomplete_results: true. A read that fails without that answer still fails open.

Fix

  • readLaneWip returns searchIncomplete when any answer says incomplete_results: true. A missing field still counts as a plain failed read.
  • wipStatus closes new admissions whether or not pause-from is set or reached (admission_room 0, admission_reason and admission_basis search-incomplete). This repository sets pause-from: "", so a gate tied to pause-from would not apply here. The queued cap still pauses only past pause-from, and no run pause changes.
  • main logs ::warning title=check-wip-cap::GitHub search answered incomplete results (admission_basis search-incomplete), so its counts are unknown and no new admissions are made this run.
  • check-wip-cap's over-cap warning names the real reason the lane keeps running: the base config could not be read, pause-from is not set, or pause-from is not reached.
  • label-unblocked.mjs: room 0 already defers. The deferral log now names its reason (GitHub search answered incomplete results) instead of always citing the queued cap.
  • docs/conventions/pr-pipeline/README.md (WIP cap section, status table) and a pr-pipeline changelog fragment.
  • No broker-fetched file changed (BROKER_PATHS in pr-require-checks.yml lists only resolve-config files), so no azure-iac pin PR is needed. createGitHub and request-budget.mjs are untouched, so this does not conflict with fix(pr-pipeline): retry a GitHub 5xx on lane lookups with backoff #9079.

Verification

  • Red first, on origin/main's check-wip-cap.mjs: 4 failing in check-wip-cap.test.mjs and 1 in label-unblocked.test.mjs.
  • Red for the follow-up, on ccb4e19 merged with main:
    • 3 failing in check-wip-cap.test.mjs: pause-from unset, future or past; main under this repository's own pause-from: ""; the warning naming the config.
    • 1 failing in label-unblocked.test.mjs: deferral with or without pause-from, and its reason in the log.
  • Green at fa38ac9: check-wip-cap.test.mjs passes 43 of 43, label-unblocked.test.mjs 119 of 119, and apply-dependency-verdict.test.mjs 36 of 36.
  • scripts/check-changelog-fragments.sh --check and markdownlint pass.

Related

Search callers with no incomplete signal, left for a follow-up:

  • .github/workflows/pr-merge.yml:547: gh pr list --author runs through GraphQL search. A false empty lists no candidates, so it fails safe.
  • babysit_gh.py: gh pr list --author (GraphQL search) and gh search prs (REST, but --json drops the flag). Discovery only.
  • work-item-tracker/adapters/github/list-items.sh --label: GraphQL search.
  • Outside the audited paths: plugins/code-tidying/skills/tidy/scripts/open-pr-count.sh (a PR cap read through gh pr list --search) and plugins/playbooks/skills/repo-sweep/scripts/state.sh (dedupe through gh pr list --search).

🤖 Generated with Claude Code

kyle-sexton and others added 2 commits October 22, 2026 11:16
During the 2026-10-22 GitHub incident the search API answered
total_count 0 with incomplete_results true for a repository with about
600 open issues. check-wip-cap already turned that answer into a failed
read, but a failed queued read leaves admissions open, so work-unblock's
admission gate had unlimited room on a false empty.

readLaneWip now reports searchIncomplete when any answer says
incomplete_results true, and wipStatus closes new admissions on or after
pause-from with admission_basis and admission_reason search-incomplete;
main logs a named warning. A read that fails without that answer still
fails open, and no run pause changes.

The deepEqual in the existing incomplete-read test gains the new
searchIncomplete field: a shape addition, not a weakened assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kyle-sexton and others added 4 commits October 22, 2026 11:32
…ause-from says

This repository's pr-pipeline.yaml sets pause-from "" (the queued cap
alerts only), and wipStatus returned room null whenever pausing was
false, so an incomplete search still admitted without limit. A
search-incomplete read now sets room 0 regardless of pause-from (owner
decision 2026-10-22); the queued cap itself still pauses only past
pause-from.

check-wip-cap's over-cap warning now names why the lane keeps running
(unreadable base config, pause-from unset, or not reached), and
work-unblock's deferral log names its reason instead of always citing
the queued cap.

The earlier pause-from-unset assertion (admissions open) is replaced by
the owner's rule; the same fields are asserted for unset, future and
past pause-from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… action description and header comments

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 22, 2026 15:48
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T15:50:25.774985Z a5cb225 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed a5cb225e60345d5048f592083612b77409d8c127: 0 finding(s), 0 as inline comments.

No security findings in PR 9089 at head a5cb225.

I read the diff for all 7 changed files, and the configError variable that the new warning text uses, in check-wip-cap.mjs.

  • Fail-closed change: a search that answers incomplete_results: true now closes new admissions. searchIncomplete is set only when that field is exactly true, so a missing or malformed field still fails open as before. Because it only sets admission_room to 0 and admission_basis and admission_reason to search-incomplete, it narrows what the lane does and widens nothing.
  • Run pause unchanged: the queued cap still pauses only on or after pause-from. A run pause (capped) is not touched, so an attacker cannot stall running work through this path.
  • Abuse limit: forcing incomplete_results: true would only defer new agent-ready labels, and the deferral is reversible by a later sweep. The attacker would need to degrade GitHub's own search, which is outside the trust boundary.
  • Log output: the new warning and the gate.why() text are static strings with no interpolated issue or PR content, so there is no workflow-command injection. The existing alert text keeps passing through escape().
  • Manifests, instruction surfaces, Actions: the PR changes no plugin manifest, no standing instruction (CLAUDE.md, AGENTS.md, rules, skill or agent bodies, hook allowlists), and no workflow trigger, permission or pin. The action.yml change is limited to description text.

Manifest fields: none touched.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed a5cb225e60345d5048f592083612b77409d8c127: 0 finding(s), 0 as inline comments.

Code review: PR #9089 (head a5cb225)

No findings. I read the full diff and the surrounding check-wip-cap.mjs code (readLaneWip, wipStatus, keepLastState, main) and label-unblocked.mjs's admissionGate.

Not run. I could not run the two changed test files (check-wip-cap.test.mjs, label-unblocked.test.mjs) because the sandbox needed approval for node --test. The "43 of 43" and "119 of 119" results in the PR body are author-claimed and I did not confirm them.

What I checked by reading:

  • Flag capture: searchIncomplete is set before the throw, only when the answer is exactly incomplete_results === true. A missing field still counts as a plain failed read, and a test covers that.
  • Room and reason: wipStatus gives room 0 whatever pause-from says, with a matching admission_reason and admission_basis. paused and pause_reason stay on the existing run-pause logic, so no run pause changes. That matches the PR's stated intent.
  • Error and config paths: keepLastState spreads the status unchanged, so the closed admission survives a failed open-PR read or a base config error. The no-repo fallback wipStatus call defaults searchIncomplete to false, so it is unaffected.
  • work-unblock: admissionGate reads admission_reason once, together with room. The deferral log picks its message from that reason, and the existing room-0 deferral path is unchanged.
  • Over-cap warning: the reworded lane keeps running text has a reachable branch for each reason it names. This follows from main's pausing being separate from the pausing inside wipStatus, which comes from the default pause-from.
  • Docs, fragment and action text: the README, the action description and outputs, and the header comments match the code. The changelog fragment is bump: patch, which fits a bug fix.
  • Manifests and closing issue: the diff touches no plugin.json or marketplace.json, so no manifest lines apply. The PR states "No related issue", so there are no acceptance criteria to check.
  • Embedded instructions: none in the PR context.

The PR body lists other search callers it leaves for a follow-up. I treated those as out of scope.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit a50c773 Oct 22, 2026
56 of 57 checks passed
@kyle-sexton
kyle-sexton deleted the fix/search-incomplete-results branch October 22, 2026 16:02
github-merge-queue Bot pushed a commit that referenced this pull request Oct 22, 2026
…ts tests (#9120)

No related issue: test-node fails on check-wip-cap tests when
resolve-config is not selected (run 39809302124)

## Summary

`test-node` failed in [run
39809302124](https://github.com/melodic-software/claude-code-plugins/actions/runs/39809302124/job/119931199035)
with `Cannot find module 'yaml'` from
`.github/actions/check-wip-cap/check-wip-cap.test.mjs:547`. That line
loads `yaml` from resolve-config's `node_modules`. Only the "Test
resolve-config" step installs them, and that step exits early unless the
plan names `.github/actions/resolve-config`. A change that selected
check-wip-cap without resolve-config therefore ran its suite with
nothing installed.

## Fix

- `.github/actions/check-wip-cap/package.json` declares
`"resolve-config": "file:../resolve-config"`. `check-wip-cap.mjs`
already imports `../resolve-config/resolve-config.mjs` at runtime.
- `scripts/run-outside-node-suites.sh` runs `npm ci --ignore-scripts`
once per run for each `file:` dependency that has a lockfile, before the
package's `npm test`. On a warm cache it takes under a second, and it
adds no resolve-config test run.
- The planner already turns `file:` dependencies into triggers
(`scripts/plan-test-lanes.sh:304-308`), so a resolve-config change now
also runs check-wip-cap's suite.
- One workflow comment is updated. Raising the job timeout was not
needed.

## Verification

- `scripts/run-outside-node-suites.test.sh`: a new fixture case (an npm
stub; one locked and one unlocked `file:` dependency) went red first
with `FAIL: expected one npm ci of pkgs/locked, then npm test: test`,
then green with `PASS=8 FAIL=0`.
- Reproduced locally with resolve-config's `node_modules` removed. `node
--test` in check-wip-cap failed at `check-wip-cap.test.mjs:1:1`. Then
`run-outside-node-suites.sh --paths` over the run's three packages
installed resolve-config first, and all three passed: check-wip-cap
39/39, maintenance-audit-lane-runs 43/43, work-unblock 117/117.
- These scripts pass: `plan-test-lanes.test.sh` (41/41),
`check-docs-only-gate.test.sh` (88/88),
`ci-test-node-stop-commands.test.sh` (2/2), and shellcheck.
- No file in the broker's `BROKER_PATHS` is touched.

## Related

- #9041 added the `createRequire` load of `yaml` from resolve-config.
- #9097 and #9089 are blocked by this failure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant