Repository navigation
Conversation
Add .github/scripts/check_pr_description.py with issue_number_from_description(), which returns N when the first line of a pull request description is exactly "Fixes #N" and otherwise raises an error whose message says how to fix the description. - A line ends at CRLF, a lone carriage return or a line feed, as in Markdown, so descriptions edited in the web interface pass. - Trailing spaces and tabs are ignored; leading whitespace fails. - An empty or null description fails. - The number is ASCII digits without a leading zero. - A "Fixes #N" line further down is reported with its line number. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Give check_pr_description.py an entry point. It reads the pull request description from the event payload at GITHUB_EVENT_PATH, reads the issue number from the first line, and asks GitHub for that issue with "gh api repos/<GITHUB_REPOSITORY>/issues/<N>". It fails, printing one error annotation, when: - GitHub has no issue or pull request with that number (HTTP 404 or 410); - the number is a pull request (the answer has a pull_request field); - the answer belongs to another repository or has another number, which is what gh prints for an issue transferred out of this repository, because it follows GitHub's redirect; - the issue is closed; - gh fails for any other reason, or prints something other than an issue. The repository comparison ignores case, as GitHub does. Messages are escaped so a multi-line gh error stays one annotation. The tests run the script with a fake gh first on PATH, so they need no network access. The moved-issue case uses #154, which GitHub moved to PolicyEngine/policyengine.py#115. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nge it Add .github/workflows/pr-description.yml with one job, "PR description", which runs check_pr_description.py on pull_request events of type opened, edited, reopened and synchronize. - It is a separate workflow from pr.yml, so editing a description reruns only this job rather than the whole of CI, and pr.yml's rule that every job is needed by "CI complete" and carries the stack condition does not apply to it. - It has no branches or paths filter and no stack condition, so it runs on every pull request, including each one in a stack. - The workflow token can only read contents, issues and pull requests; the check step gets it as GH_TOKEN for gh api. - A newer event for the same pull request cancels a run still in progress. Tests pin the triggers, the absence of a condition, the permissions, and the step that runs the script with the job token. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily dependency-update PR's description started with "Automated daily dependency updates", so the PR description check would fail on every one. dependency-update-create-pr.sh now runs whenever the update branch was pushed, with or without an open update PR: - It uses the oldest open issue titled "Update dependencies", or opens one. "Fixes #N" closes it when the PR merges, so the next PR gets a new issue. The issues API lists pull requests too, so it skips entries that have a pull_request field. - Without an open update PR, it opens one whose description starts with "Fixes #N". - With one, it makes "Fixes #N" the first line: it replaces an earlier "Fixes #" line, as when that issue was closed, or adds the line above a description that has none, as on #583, opened before the rule. - A failed issue lookup stops the script before it opens an issue. The GitHub App token in update-dependencies.yml requests no specific permissions, so it has every permission of the App's installation. That installation can write issues: check-policyengine-updates.yml requests issues: write from the same organization secrets (APP_ID, APP_PRIVATE_KEY), and policyengine[bot] opened #831 and #856 with it. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move the rule from doc-only to enforced. Its enforcer is the "PR description" job in pr-description.yml, and the registry now names the step that runs check_pr_description.py. - Proof: pull_request event fixtures under rules/fixtures/sim.process.pr-tracking-issue. The bad one has the first line of PR #616, "Fixes #615."; the good one has the same description with the period removed. test_rule_proof_pr_tracking_issue runs the check on both, with a fake gh answering that #615 is open. pr.yml's test job runs the proof. - The statement now says the issue must be open and not a pull request, and that automated PRs open or reuse one. - pr-description.yml joins [discovery].workflows, so a new job there must be registered too. - github-prs.md gains a "The Fixes Line" section: what passes and fails, how to fix each failure, and how the automated PRs comply. - Regenerate docs/generated/rules.md and the Rules block in AGENTS.md. Making the job a required check is a repository-ruleset change and is not part of this commit. Fixes #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub also sends the pull_request "edited" event for closed pull requests. Merging a pull request closes the issue its first line names, so editing a merged pull request's description would run the check and report a failure on it. Run the job only when the pull request in the event is open. Every open pull request still runs it, including each one in a stack, so the required check is never skipped where it matters. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README: every PR needs a tracking issue, not only non-trivial work, now that the PR description check fails without one. - AGENTS.md: list the PR description job alongside the rest of PR CI. - Test that gh api's HTTP 410 (a deleted issue) reports a missing issue, like HTTP 404. - Test that the dependency-update script replaces a "Fixes #" line that is the open update PR's whole description. The new tests cover existing behaviour, so they passed when added. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
github-prs.md said the job checks every pull request; since the previous commit it skips closed ones. Also correct a test fixture's docstring: it loads the check script as a module, which is not how the workflow runs it. Part of #861. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
pr-description-check
branch
from
October 21, 2026 21:10
8a63b73 to
322d4b8
Compare
anth-volk
added this pull request to stack #891
October 21, 2026 21:26
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #861
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
Step 2.8 of the code-quality plan: enforce rule
sim.process.pr-tracking-issue, which says the first line of every pull request description isFixes #ISSUE_NUMBER.PR descriptionjob (.github/workflows/pr-description.yml). It runs.github/scripts/check_pr_description.py, a standard-library script, onpull_requestevents of typeopened,edited,reopenedandsynchronize.Fixes #N. It then runsgh api repos/<repo>/issues/<N>and fails unless #N is an open issue, not a pull request, in this repository.Fixes #Nfirst.doc-onlytoenforced, with enforcer kindworkflow-joband a registered proof.github-prs.mdgains a section on what passes and how to fix a failure, and the rendered views are regenerated.Of the last 300 PRs here, about half would fail this check, for example
Fixes #615.(#616),Refs #434(#543) and a## Summaryheading.Not in this PR: making the job a required check, which is a ruleset change; see "After merge".
Commits
issue_number_from_description().gh apilookup and the script's entry point.sim.process.pr-tracking-issuewith the PR description job. Adds the registry entry, proof fixtures, docs and rendered views.Design decisions
A separate workflow, not a
pr.ymljob.pr.ymlruns full CI and doesn't trigger onedited. Addingeditedthere would rerun all of CI on every description edit.test_ci_complete_gate.pyrequires everypr.ymljob to be needed byCI completeand to carry the "lowest PR in the stack" condition. Neither fits a check that must run on every PR.[discovery].workflows, so the rules guard requires every job in it to be registered, as it does forpr.yml.Runs on every open PR, including stacked ones.
branchesorpathsfilter, and no stack condition.github.event.pull_request.state == 'open'. GitHub also sendseditedfor closed PRs, and merging a PR closes the issue it names, so editing a merged PR's description would otherwise show a failing check on it.concurrency, keyed on the PR number).Permissions.
contents: read,issues: readandpull-requests: read, and nothing else.GH_TOKEN: ${{ github.token }}.How the first line is read (each choice has a test):
Fixes #404 \r\n…, as in #405)\ralso ends a line, as in Markdown.Fixes #N \r.Fixes #Nfurther downFixes #Nwas found.nulldescriptionFixes #0,Fixes #0861, non-ASCII digitsFixes #615.,fixes #1,Closes #1,Fixes owner/repo#1, an issue URLFixes #N.Which numbers count.
pull_requestfield.repository_url(ignoring case, as GitHub does) andnumberwith the request.gh apifollows GitHub's redirect for a transferred issue. For example, #154 here answers asPolicyEngine/policyengine.py#115, which is the case the test uses.ghfailure fails with gh's own message.The description comes from the event payload, as the plan specifies. Re-running a run checks the description as it was when that run started.
github-prs.mdsays so: after editing, rely on the run the edit starts; after reopening an issue, re-run the job.Where the tests live.
projects/policyengine-simulation-entry/tests/, beside the other.github/scriptstests (test_workflow_scripts.py).FakeCommandshelper, which puts a fakeghfirst onPATH, so no test makes a network call.pr.yml'stestjob runs them, and the proof is registered against that job.unittest: that is the convention for this directory, and ruff'sPTrules apply there.Dependency-update automation
update-policyengine-package.sh) already opens an issue per version and starts its PR withFixes #N, as chore(deps): update policyengine to 6.9.0 #857 and chore(deps): update policyengine to 6.8.0 #832 do. Unchanged.dependency-update-create-pr.sh) now runs whenever the update branch was pushed, with or without an open update PR:Fixes #Ncloses it when the PR merges, so the next PR gets a new issue.Fixes #N.Fixes #Nthe first line: it replaces an earlierFixes #line, or adds the line above a description that has none.editedevent from the description update then passes it.set -einside$(...), so the lookup sets a variable rather than printing.update-dependencies.ymlcreates its GitHub App token with nopermission-*inputs, so the token has every permission of the App's installation.check-policyengine-updates.ymlrequestspermission-issues: writefrom the same organization secrets (APP_ID,APP_PRIVATE_KEY, visible to all repositories). Its runs succeed, andpolicyengine[bot]opened Update policyengine to 6.8.0 #831 and Update policyengine to 6.9.0 #856.permission-*inputs. Naming any permission limits the token to the ones named, and this job also pushes merges frommain, which can carry workflow-file changes that need further permissions.After merge
Make
PR descriptiona required status check in themainruleset, next toCI complete, with GitHub Actions as its source.The check is reported only after an event on a PR, so open PRs need a push, a description edit, or a close and reopen before it appears. #583 gets one on the daily job's next successful push.
Environment variables
GITHUB_EVENT_PATHcheck_pr_description.pyGITHUB_REPOSITORYcheck_pr_description.py,dependency-update-create-pr.shGH_TOKEN(check)ghincheck_pr_description.pygithub.tokenenv:inpr-description.yml, limited by the workflowpermissions:blockGH_TOKEN(dependency update)ghindependency-update-create-pr.shactions/create-github-app-token, using organization secretsAPP_IDandAPP_PRIVATE_KEYenv:fromsteps.app-token.outputs.token, unchangedSuppressions
None added.
Red and green record
FileNotFoundError, the script didn't existFileNotFoundErrorforpr-description.yml--body, and the step still requiredhas_open_pr == 'false'read_textturned the CRLF under test into LFCI job 'pr-description' … enforces no registered rule.KeyError: 'if'Self-review
This is a self-review, so it assigns no review disposition.
Fixes #Nline that is an open update PR's whole descriptiongithub-prs.mdsaid "every pull request" after commit 6; a fixture docstring was inaccurateA further pass found nothing more to act on.
Known limitation, not changed.
pull_requestworkflows run the PR's own copy of the workflow and script, so a PR could weaken the check that runs on itself. The plan's §9.1.4 accepts this for workflow files, and reviewers read workflow diffs.pull_request_targetwould runmain's copy instead, but it wouldn't run on the PR that adds it.Testing
uv run pytest tests/inprojects/policyengine-simulation-entry: all pass. That includestest_pr_description_check.py(52) andtest_workflow_scripts.py(36).python -m unittest discover -s scripts/rules/tests -t .: OK.python scripts/run_quality_guards.py:rules-registryandci-scriptspass.python -m scripts.rules guard --base-ref origin/main: passes.ruff check .andruff format --check .are clean from the repository root.editedevent with this description, passed. The job token had Contents, Issues, Metadata and PullRequests read only. The step printed "The first line is Fixes Check that every pull request description starts with Fixes #N for an open issue in this repository #861, and Check that every pull request description starts with Fixes #N for an open issue in this repository #861 is an open issue in PolicyEngine/policyengine-sim-api."Refs #861, run 39619516824 failed. Its annotation, titled "PR description", read:The first line is "Refs #861". Edit the description so its first line is exactly Fixes #ISSUE_NUMBER, …. The description was then restored, which starts another run.ghcredentials (not the job token):Refs #861: failed, wrong first line.🤖 Generated with Claude Code