Skip to content

ci: Read closing Issues from the PR body #53

Description

@idy

Background

OpenAI PR Review decides pr-linkage from GraphQL closingIssuesReferences, the list behind GitHub's Development sidebar. Since about 2026-09-30 07:00 UTC GitHub has stopped turning closing keywords into those references: h2vivi/firmwares PRs #1477 and #1479 and GizClaw/gizos PR #617 all carry Closes #N in the body, the Issue timeline shows the cross-reference with willCloseTarget: false, and closingIssuesReferences stays empty, while PRs opened before 07:00 UTC with the same wording are linked. Re-saving the body or changing the keyword does not help, and there is no public API to create the link. Every such PR is blocked with Pull request must natively close at least one same-repository Issue although its body states the Issue it closes.

The reviewer should not depend on when or whether GitHub indexes the keyword. The PR body is already part of the readiness snapshot and states the same fact.

Goal

Take the Issues a pull request closes from the closing keywords in its body instead of from GitHub's Development links. A PR whose body says Closes #123 for a readable same-repository Issue satisfies pr-linkage, and the named Issues feed the Issue review, the sub-issue coverage rule, and plan conformance exactly as the native set did.

Non-goals

  • Do not change how Issue events find the PRs to refresh: openai-pr-review-dispatch.yml keeps using closedByPullRequestsReferences; an unlinked PR is refreshed by its next event or an @codex review comment.
  • Do not change Issue-format rules, native parent / sub-issue / dependency checks, review-thread handling, limits, model, or effort.
  • Do not add a caller input or secret; do not consult Development links as a second source.

Code Changes Tree

.github/
├── scripts/
│   ├── pr-readiness/
│   │   ├── closing-issues.mjs        # parse closing keywords from the body, read the referenced Issues in one GraphQL request, normalize them
│   │   ├── verify.mjs                # rebuild the snapshot from body-declared closing Issues instead of closingIssuesReferences
│   │   ├── common.mjs                # reword the pr-linkage blockers for body-declared linkage
│   │   └── test.mjs                  # cover the parser, the collector, the bound, API failures, and stale linkage in the verifier
│   └── pr-review/
│       ├── run.mjs                   # reword the PR-stage prompt from native to body-declared linkage
│       └── test.mjs                  # run the discussion step with a closing body, a non-Issue reference, and an API failure
└── workflows/
    ├── codex-openai-review.yml       # collect linked Issues through closing-issues.mjs, check out its review-request dependency, reword defaults
    └── openai-pr-review-dispatch.yml # reword the default readiness instructions
README.md                             # document the keyword grammar, ignored contexts, unreadable references, and the 100-reference bound

Design

Closing references

closingIssueReferences(body, repository) returns the distinct references in body order. A reference is one of GitHub's closing keywords (close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved, any case, optional colon), spaces or tabs, then #N, owner/repo#N, or https://github.com/owner/repo/issues/N. The keyword must not continue a word (hotfixes #1 declares nothing) and the number must not continue into a word. Text inside fenced code, inline code, block quotes (through the existing stripQuotedText), indented code blocks, and HTML comments declares nothing, so a PR-template placeholder or a pasted log cannot satisfy the rule. #N resolves against the PR's repository.

Reading the Issues

collectClosingIssues reads at most 100 references with one aliased GraphQL request (iK: repository(owner, name) { issue(number) { … } }) carrying the same fields as before: title, body, state, Issue Type, parent, sub-issues, blockedBy, blocking, each with the 100-node bound. A reference that is not a readable Issue — a pull request number, a missing number, an inaccessible repository, reported as NOT_FOUND — is ignored, as GitHub ignores it. Any other GraphQL error, or a response without data, fails context collection closed. Issues are de-duplicated by their canonical nameWithOwner#number. linked_issue_count is the number of Issues read, or the number of references when the body names more than 100, so the existing too-many-closing-issues blocker reports the truncation.

Call sites

The reusable workflow's context step and verify.mjs both call the module, which removes their duplicated Issue normalization. The context step imports it from the reviewer checkout (REVIEWER_SOURCE_DIR) and sends the request with github.request('POST /graphql') to receive partial data with NOT_FOUND errors. Both reviewer checkouts add .github/scripts/review-request to their sparse set because the module imports stripQuotedText. analyzePullRequest and the snapshot shape are unchanged; the body was already hashed, so a changed closing reference still invalidates the snapshot and the publication-time verifier still rejects it.

Test And Acceptance Criteria

Acceptance Criteria

  • A PR whose body contains Closes #N for an open same-repository Issue passes pr-linkage without any Development link, and that Issue is reviewed.
  • A body with only a plain mention, a quoted or commented keyword, or a reference to a pull request or missing number still reports missing-closing-issue.
  • More than 100 references report too-many-closing-issues; a GraphQL failure other than NOT_FOUND fails closed.
  • Removing or changing the closing reference, or editing the named Issue, while a review runs is rejected by the verifier.
  • Existing caller YAML needs no new input, secret, or permission.

Validation

  • node .github/scripts/pr-review/test.mjs
  • node .github/scripts/review-request/test.mjs
  • node .github/scripts/pr-readiness/test.mjs
  • node .github/scripts/issue-review/test.mjs
  • actionlint -ignore 'property "workflow_(repository|sha)" is not defined' .github/workflows/*.yml
  • git diff --check
  • Live read: run the collector against h2vivi/firmwares PR #1479's body and confirm it returns Issue #1478 while a pull-request number, a missing number, and an inaccessible repository are ignored.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions