ci: per-commit fast-check gate (catch broken intermediate commits under rebase-merge) - #1190
Open
vringar wants to merge 1 commit into
Open
ci: per-commit fast-check gate (catch broken intermediate commits under rebase-merge)#1190vringar wants to merge 1 commit into
vringar wants to merge 1 commit into
Conversation
vringar
force-pushed
the
ci/per-commit-checks
branch
from
June 15, 2026 22:14
f361334 to
1041f4f
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1190 +/- ##
==========================================
+ Coverage 62.23% 62.34% +0.10%
==========================================
Files 40 40
Lines 3930 3930
==========================================
+ Hits 2446 2450 +4
+ Misses 1484 1480 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
vringar
marked this pull request as ready for review
June 19, 2026 23:03
vringar
force-pushed
the
ci/per-commit-checks
branch
from
June 19, 2026 23:04
1041f4f to
24076a0
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new GitHub Actions workflow to gate rebase-and-merge PRs by running a fast static-check suite on every individual commit in the PR (and in merge-queue merge_group candidates), preventing “broken intermediate commits” from landing on master.
Changes:
- Introduces
per-commit-checks.yaml, triggered onpull_requestandmerge_group. - Computes a commit range and iterates commit-by-commit, running
pre-commit(changed-files), conditional Extension build,import openwpm, andpytest --collect-only. - Aggregates failures across commits (doesn’t stop at first failure) and fails the job at the end if any commit failed.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
vringar
force-pushed
the
ci/per-commit-checks
branch
from
July 20, 2026 22:54
c4533be to
4183b69
Compare
vringar
force-pushed
the
ci/per-commit-checks
branch
from
September 6, 2026 15:26
4183b69 to
16984fa
Compare
…er rebase-merge) Add a lightweight CI job that runs the fast static checks on every commit in a PR / merge-queue candidate, not just the tip. Because the project uses rebase-and-merge, every commit lands on master individually, but the existing tests job only validates the tip — a commit that is broken in isolation can reach master. This gate closes that gap cheaply: per commit it runs the pre-commit hooks (black/isort/mypy/actionlint) over the whole tree, rebuilds the extension when its sources changed, imports the package, and collects the test suite. It intentionally does not run the browser test suite — runtime and browser-behavior regressions stay covered by the tests job on the tip plus the merge queue. The logic lives in scripts/per_commit_checks.py rather than inline in the workflow, so it is readable, reviewable and runnable locally (`python scripts/per_commit_checks.py --base master --head HEAD`) instead of being a shell loop embedded in YAML. The workflow is reduced to checkout, setup and one call. The script reads the commit range from the GitHub event payload, so no untrusted event data is interpolated into a shell command.
vringar
force-pushed
the
ci/per-commit-checks
branch
from
September 6, 2026 15:34
16984fa to
c12a7be
Compare
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.
Motivation
OpenWPM uses rebase-and-merge, so every commit in a PR lands on
masterindividually. But CI only tests the PR tip (and the merge-queue candidate
tip). That means a commit which is broken in isolation — e.g. it introduces a
syntax error, an undefined name, a bad import, or a type/lint regression that a
later commit in the same PR happens to fix — can still ship to
masteras anindividual broken commit. Anyone who later bisects, checks out, or builds from
that commit hits the breakage.
This adds a lightweight gate that runs the fast static checks on every
commit in the PR / merge-queue range, so an intermediate commit that's broken
on its own can't slip through. It's the cheap 80/20: it reuses the existing
./.github/actions/setupcomposite action (conda env + built extension) andfinishes quickly because it skips the expensive part.
Where the logic lives
The checks live in
scripts/per_commit_checks.py, not inline in theworkflow YAML. The loop is too involved to read as an embedded shell script
(walking commits, checking out each one, deciding per commit whether the conda
env or the extension needs rebuilding, and collecting failures instead of
aborting on the first one), and once it's in YAML you can't run it, you can't
lint it, and you can't review it as code. So
per-commit-checks.yamlis nowjust checkout, setup and one call, and the script follows the conventions of
the other
scripts/*.pyhelpers (module docstring, argparse,main() -> int,covered by black/isort/mypy).
It also runs locally, which is the main reason this is worth doing:
What it catches (per commit)
python -c "import openwpm"plus
pytest --collect-onlyexercise import of the package and the whole testtree, which covers the breakage that only shows up at import time.
pre-commit run --all-filesruns black / isort /mypy / actionlint over the whole tree. mypy is the load-bearing check
here: it flags
[name-defined](undefined name) and type errors. This runsover everything rather than over the files the commit changed, both because
the stronger property is what we actually want (every commit leaves the tree
clean, not just its own diff) and because it doesn't depend on pre-commit's
change selection picking the right set of files.
npm run buildruns only when the committouched
Extension/, preceded bynpm ciwhen it touched the manifest or thelockfile.
./install.shre-creates the conda env, but onlywhen a commit actually changes
environment.yaml, so the common case stayscheap.
What it intentionally does NOT do
browser-behavior regressions stay covered by the existing
testsjob on thePR tip + the merge queue, plus author discipline. Running the full suite on
every commit would be far too expensive for the marginal value.
Notes
in branch protection. Until then it runs informationally.
belong in the per-commit tier) and on the merge-queue handling.
pull_requesttrigger).The
merge_grouptrigger only fires inside a real merge queue, so that pathcan't be exercised from the PR itself; the field names
(
merge_group.base_sha/merge_group.head_sha) were validated against thepublished webhook payload schema.
How the commit range is computed
The script reads the range out of the event payload at
GITHUB_EVENT_PATHrather than having the workflow interpolate
${{ github.event... }}into ashell command, so no event-controlled data ends up in a command line:
pull_request:base = pull_request.base.sha,head = pull_request.head.sha— the PR's own commits (not the synthetic merge commit).
fetch-depth: 0plusan explicit-SHA checkout per commit makes this independent of which ref
actions/checkoutmaterialized.merge_group:base = merge_group.base_sha(the group's parent commit),head = merge_group.head_sha(the group tip) — exactly the candidate commits.Every check is run for its side effect on a failure counter, never by raising,
so the script visits every commit and reports all failing ones before it
exits non-zero. The per-commit checkout uses
--force, as the pre-commit hooks(black, isort) rewrite the files they're unhappy with and would otherwise leave
the tree dirty enough to block the next checkout.