Skip to content

CI: skip PR re-test after a clean main merge (#1373) - #1383

Merged
Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
agent/fix-ci-pr-merge-main-reuse
Oct 11, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
agent/fix-ci-pr-merge-main-reuse

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1373

Problem

With a merge queue, a PR never needs to be up to date with main: the queue re-tests every entry on the latest main before it lands. But when someone merges main into a PR branch, the push re-runs the whole PR tier on a diff that already passed. #1373 measured about 23% of full PR runs as these re-syncs during post-landing bursts.

Fix

clippy gets a new step, run only on pull_request: scripts/ci-reuse-pr-head.py. It sets pr_unchanged=true only when all of these hold:

  1. The PR targets main, the only branch with a merge queue, and it isn't the release/v5-prerelease integration PR, whose full tier the queue doesn't run. The step doesn't even run otherwise.
  2. The PR head is a two-parent merge commit (an ordinary push costs one REST read and stops here).
  3. The PR's own diff (<base>...head, from the merge-base) matches the diff of the first parent, the previous head (<base>...HEAD^1), hunk for hunk. Hunk line numbers are ignored, like git patch-id --stable.
  4. ci.yml completed with success on the previous head for the same head repository, and its clippy job actually ran. A draft run also ends in success with every job skipped, so it doesn't count.

When it's true, the build and test jobs skip: node-addon, coverage, e2e-build (and so every e2e / cargo-vex-matrix consumer), yarn-classic-matrix and yarn-berry-e2e. So do the full-scope-only test, docker-base, e2e-build-windows and cargo-old-toolchains. These still run: clippy (including cargo check --all-targets on the merged tree, so a compile break from main still shows on the PR), lint-ecosystems, release-readiness, dispatch-tests, hosted-e2e (unscoped as before) and the full-scope test-release. The merge queue never runs test-release, so skipping it would mean it never ran on the merged result.

These cases run everything: a conflict resolution (it changes a hunk), a merge of a branch other than the base (it adds files or hunks), a pure rename or a binary or oversized patch (no patch in the compare API), 300 or more files, a failed, cancelled, missing or draft previous run, any API error or timeout, and every push, merge_group, schedule or workflow_dispatch event. The script prints no response bodies.

Coverage stays the same as the issue's analysis: every test ran on this exact PR diff at the previous head, and the merge queue runs it again on the latest main before it merges. A semantic conflict with main now surfaces in the queue instead of on the PR, which is the guarantee the queue already gives.

The issue's optional process part (an AGENTS.md note telling agents not to merge main unless they need to) is left out. That's a fleet-wide instruction change for a maintainer to decide. With this change, those merges cost about 4 job-min instead of ~30.

Runner usage (lean PR run)

jobs job-min
Before, any non-draft push (run 38132754998) 29 ~30.5
After, ordinary push 29 ~30.5 (+1 REST read, <1 s in clippy)
After, clean merge-main push 6 (clippy, lint-ecosystems, release-readiness, dispatch-tests, hosted-e2e, ci-ok) ~3.9

merge_group and push runs are unchanged: the step only runs on pull_request, so the output is empty and every gate passes. Required check names (ci-ok, clippy) and the ci-ok aggregation are unchanged. Skipped jobs already count as passing there. The Depot mapping and kill switch are untouched. No new workflows or schedules.

Validation

  • python3 -m pytest scripts/tests -q: 330 passed, 4 skipped (on 941e1c4).
    • New scripts/tests/test_ci_reuse_pr_head.py (7 tests, 33 subtests): a clean merge is reused, and each fall-back case above runs everything.
    • New test_unchanged_pr_merge_skips_only_what_the_queue_re_tests in test_ci_scheduling.py pins which jobs carry the gate, and checks that every ungated job in clippy's tree either waits on a gated build or never runs on a lean PR.
  • zizmor --offline .github/workflows/ci.yml: 49 findings before and after (0 medium/high). The new step passes event fields through env:, not inline templates.
  • actionlint v1.7.7: same 140 lines of output before and after. All of them are its pre-existing YAML-anchor limitation.
  • Every fromJSON matrix literal parses, and the workflow loads as YAML.
  • Live dry run of the script against real history:
  • No Rust or platform code changed, so no full-scope dispatch is needed.

/code-review high findings

Fixed in 941e1c4:

  • Reuse applied to PRs into non-main branches, which have no queue. It now runs only for base_ref == 'main', checked in both the step if and the script.
  • The release/v5-prerelease PR's full tier (e2e-full, e2e-docker, cargo-vex-matrix-full, yarn-berry-full) isn't in the queue. That PR never reuses now.
  • test-release is excluded from merge_group. It is no longer gated.
  • The scheduling test's "never on a PR" check was a substring match. It now requires the exact github.event_name != 'pull_request' prefix, with the v5-PR exception only.
  • The two compare payloads were fetched before the cheap run check. The order is swapped.

Not changed, with reasons:

  • A reuse run can count as evidence for the next merge (a chain). The chain only extends over byte-identical PR diffs, and the queue still re-tests on the latest main. Every queue-excluded job (test-release, the v5 full tier) now always runs, so nothing is lost. The run's base branch isn't in the runs API for fork PRs, so a same-SHA run from another PR can't be ruled out cheaply. With bases now restricted to main, both runs tested the same diff against main.
  • There is no way to force a full PR run. "Re-run all jobs" re-evaluates the same evidence. Pushing any commit that changes the diff runs everything, and the queue tests it on the latest main regardless. A label bypass wouldn't help a re-run either: re-runs reuse the original event payload, and labeled isn't a trigger.
  • api() / run predicate duplicated from ci-reuse-merge-group.py. The predicates differ (merge_group + queue branch vs pull_request + clippy job). Sharing a module between two standalone scripts/*.py files would add import plumbing for ~10 lines.

🤖 Generated with Claude Code

https://claude.ai/code/session_015QPfJx7SNJv8THsH3qoLKi


Generated by Claude Code

Merging main into a PR branch re-ran the whole PR tier on a diff that
had already passed, though the merge queue re-tests every entry on the
latest main before it lands. During post-landing bursts about a quarter
of full PR runs were these re-syncs (#1373).

clippy now runs scripts/ci-reuse-pr-head.py on pull_request. When the
head is a two-parent merge, the PR's own diff matches its previous head
hunk for hunk (line numbers ignored, like git patch-id), and CI passed
on that head with clippy actually running, it sets pr_unchanged=true.
The build and test jobs then skip; clippy, the cheap preflight jobs and
hosted-e2e still run. Conflict resolutions, merges of other branches,
binary or oversized diffs and any API error run everything.

Refs #1373

Assisted-by: Claude Code:claude-opus-5-5
Review of the first cut found gaps where a skipped PR job would never
run again before landing:

- PRs into branches other than main have no merge queue, so they now
  always run everything.
- The release/v5-prerelease PR runs a full tier the queue skips, so it
  never reuses either.
- test-release is excluded from merge_group runs, so it now runs on
  an unchanged merge-main push too.

The script also checks for a green previous run before fetching the
two compare payloads, since most merge pushes follow a cancelled run.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 11, 2026 12:07
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 941e1c4. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 11, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 941e1c4.

  • CI: 37/37 non-skipped checks green on the head (48 skipped by path filters). No failures that also hit main.
  • Bugbot: reviewed 941e1c4 and found no issues.
  • Mergeable: yes, no conflicts. Doesn't touch CHANGELOG.md.
  • For reviewers: the change is in .github/workflows/ci.yml and scripts/ci-reuse-pr-head.py. It decides when a push that only merges main can reuse the PR head's earlier CI result, so check that the reuse is limited to PRs the merge queue re-tests anyway.

Slack announcement not sent yet (the Slack send tool wasn't available this run).


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. When a PR push only merges main into the branch and the PR's own diff is unchanged, CI skips the build/test jobs and runs just clippy (still compiling the merged tree), the cheap preflights, test-release and hosted-e2e. A new script, scripts/ci-reuse-pr-head.py, decides this: the head must be a two-parent merge, the diff from the merge-base must match the previous head's diff hunk for hunk, and ci.yml must have passed on the previous head with clippy actually run. The merge queue still re-tests every PR on the latest main, so coverage before landing stays the same.

Risk: low-medium. CI-only; no Rust or product code. If the script gets it wrong, a PR shows green without its full tests, but the queue's merge_group run still gates the merge. Every error path falls back to the full run. It only applies to PRs into main, never release/v5-prerelease.

Look here

Verified

  • Read the full diff (4 files, +329/−10). CHANGELOG.md is untouched.
  • python3 -m pytest scripts/tests -q: 330 passed, 4 skipped on 941e1c4.
  • No interaction with ci-reuse-merge-group.py: that one reuses only on push to main from a passed merge_group run, so a lean PR run can never stand in for queue CI.
  • ci-ok counts skipped jobs as passing, and the jobs this gates are all needs: clippy without always(). The required check names don't change.
  • The new step passes event data only through env:; a fork's head_ref appears only in an if: expression. No response bodies are printed.
  • CI: ci-ok and clippy green on the head (37/37 non-skipped). Bugbot reviewed 941e1c4 and found no issues.

Changes I made: none.

Open questions: none that block merging. A small mismatch: the description names the scheduling test test_unchanged_pr_merge_skips_every_test_job_but_hosted_e2e, but in the code it's test_unchanged_pr_merge_skips_only_what_the_queue_re_tests (test_ci_scheduling.py:117). As the PR says, the optional AGENTS.md process note from #1373 is left for a maintainer to decide.

Auto-merge is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit c537035 Oct 11, 2026
85 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-ci-pr-merge-main-reuse branch October 11, 2026 18:01
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 11, 2026
e2e-build-windows keeps this branch's full-scope-only condition and
gains main's pr_unchanged gate. The scheduling test pins the combined
condition.

Assisted-by: Claude Code:claude-opus-5-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-perf CI / merge-queue performance finding (profiler routine) Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI perf: PR CI — 23% of full PR runs re-test unchanged diffs after a merge-main push (~20–30k Linux job-min/day, feeds the ubuntu-latest backlog)

3 participants