From 98edbe7215d237e8f878f196a2da78b84708a385 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 26 Oct 2026 19:03:14 +1100 Subject: [PATCH 1/2] ci(mutants): diff against the pull request's real base, read from the API The gate scopes cargo-mutants to the PR's diff, which it took as `git diff origin/$GITHUB_BASE_REF`. For a pull request whose base branch is itself the head of another open pull request, GitHub has been seen to build the test merge commit against that other PR's merge commit and to report the other PR's base in `github.base_ref`: a PR into `next` while `next` has a PR into `main` arrived here with `main` as its base, and the stacked PRs above it with their grandparents' bases. The diff was then everything between `main` and `next`, 670 to 800 files, and the gate hit its two-hour timeout on changes that touched no Rust at all. The step now asks `gh pr view` for the base the PR page shows, fetches that commit and the PR head, and diffs the two with three dots, so the diff holds the PR's own lines whatever GitHub checked out. The log names the base it used beside what the payload said, and lists the files under mutation instead of printing the whole diff. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu --- .github/workflows/mutants.yml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 65cabc4c0..02d3f1304 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -91,15 +91,34 @@ jobs: # PR only: a manual dispatch has no base to diff against, so it sweeps # both crates instead (see the next step). + # + # The base comes from the API at run time, not from the event payload. + # For a pull request whose base branch is itself the head of another + # open pull request (a stacked PR, or any PR into `next` while `next` + # has a PR into `main`), GitHub has been seen to build the test merge + # commit against the other PR's merge commit and to report the other + # PR's base in `github.base_ref`. The diff was then everything between + # `main` and `next`, and the gate ran for two hours on a Go-only + # change. `gh pr view` returns the base the PR page shows. The diff is + # taken between that base and the PR's head, not the checked-out merge + # commit, so it holds the PR's own lines whatever GitHub merged them + # into. - name: Compute the PR diff if: github.event_name == 'pull_request' env: - BASE_REF: ${{ github.base_ref }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PAYLOAD_BASE_REF: ${{ github.base_ref }} run: | - git fetch --no-tags origin "$BASE_REF" - git diff "origin/$BASE_REF" > pr.diff - echo "Changed lines under mutation:" - cat pr.diff + base=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json baseRefName,baseRefOid --jq '"\(.baseRefName) \(.baseRefOid)"') + base_ref=${base% *} + base_sha=${base#* } + echo "Base: $base_ref at $base_sha (the event payload said '$PAYLOAD_BASE_REF'); head: $HEAD_SHA" + git fetch --no-tags origin "$base_sha" "$HEAD_SHA" + git diff "$base_sha...$HEAD_SHA" > pr.diff + echo "Files under mutation:" + grep '^diff --git' pr.diff | sed 's|^diff --git a/.* b/| |' || echo " (none)" # Never fails the job: the enforce step below is the gate, and the # comment must be posted first. From a849cacd1b129ba099603b6cdaa5387bbe724b76 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 26 Oct 2026 19:18:56 +1100 Subject: [PATCH 2/2] ci(mutants): check out the pull request's head so the diff matches the tree The previous commit diffed the base against the PR head while actions/checkout left GitHub's test merge commit in the tree. If the base had moved and touched earlier lines of a file the PR changes, the hunks' line numbers no longer matched the tree, and cargo-mutants 27.1.0 refuses such a diff ("Diff content doesn't match source file"), failing the gate on a valid PR. The checkout now takes the PR head on a pull request, the diff step asserts that is what it has, and diffs the base against it. A manual dispatch checks out the default ref as before. Claude-Session: https://claude.ai/code/session_01N7bCqa3ZVJMcEE99RzVqEu --- .github/workflows/mutants.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 02d3f1304..f94eb1310 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -73,6 +73,12 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: + # On a pull request, the PR's head rather than GitHub's test merge + # commit: cargo-mutants applies `pr.diff` to the checked-out tree + # and refuses a hunk whose lines do not match it, and the diff + # below is taken between the base and this head. Empty on a + # manual dispatch, which checks out the default ref. + ref: ${{ github.event.pull_request.head.sha || '' }} # Full history, to diff the PR against its base for `--in-diff`. fetch-depth: 0 persist-credentials: false @@ -100,9 +106,8 @@ jobs: # PR's base in `github.base_ref`. The diff was then everything between # `main` and `next`, and the gate ran for two hours on a Go-only # change. `gh pr view` returns the base the PR page shows. The diff is - # taken between that base and the PR's head, not the checked-out merge - # commit, so it holds the PR's own lines whatever GitHub merged them - # into. + # taken between that base and the PR's head, which is what the checkout + # step put in the tree, so the hunks' line numbers are the tree's. - name: Compute the PR diff if: github.event_name == 'pull_request' env: @@ -115,8 +120,9 @@ jobs: base_ref=${base% *} base_sha=${base#* } echo "Base: $base_ref at $base_sha (the event payload said '$PAYLOAD_BASE_REF'); head: $HEAD_SHA" - git fetch --no-tags origin "$base_sha" "$HEAD_SHA" - git diff "$base_sha...$HEAD_SHA" > pr.diff + git fetch --no-tags origin "$base_sha" + test "$(git rev-parse HEAD)" = "$HEAD_SHA" || { echo "the checkout is not the PR head"; exit 1; } + git diff "$base_sha...HEAD" > pr.diff echo "Files under mutation:" grep '^diff --git' pr.diff | sed 's|^diff --git a/.* b/| |' || echo " (none)"