Skip to content

ci: per-commit fast-check gate (catch broken intermediate commits under rebase-merge) - #1190

Open
vringar wants to merge 1 commit into
masterfrom
ci/per-commit-checks
Open

ci: per-commit fast-check gate (catch broken intermediate commits under rebase-merge)#1190
vringar wants to merge 1 commit into
masterfrom
ci/per-commit-checks

Conversation

@vringar

@vringar vringar commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Motivation

OpenWPM uses rebase-and-merge, so every commit in a PR lands on master
individually. 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 master as an
individual 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/setup composite action (conda env + built extension) and
finishes quickly because it skips the expensive part.

Where the logic lives

The checks live in scripts/per_commit_checks.py, not inline in the
workflow 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.yaml is now
just checkout, setup and one call, and the script follows the conventions of
the other scripts/*.py helpers (module docstring, argparse, main() -> int,
covered by black/isort/mypy).

It also runs locally, which is the main reason this is worth doing:

python scripts/per_commit_checks.py --base master --head HEAD

What it catches (per commit)

  • Syntax errors / undefined names / bad importspython -c "import openwpm"
    plus pytest --collect-only exercise import of the package and the whole test
    tree, which covers the breakage that only shows up at import time.
  • Type errors & lintpre-commit run --all-files runs 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 runs
    over 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.
  • Extension TypeScript buildnpm run build runs only when the commit
    touched Extension/, preceded by npm ci when it touched the manifest or the
    lockfile.
  • Dependency changes./install.sh re-creates the conda env, but only
    when a commit actually changes environment.yaml, so the common case stays
    cheap.

What it intentionally does NOT do

  • It does not run the browser/Selenium test suite. Runtime and
    browser-behavior regressions stay covered by the existing tests job on the
    PR tip + the merge queue, plus author discipline. Running the full suite on
    every commit would be far too expensive for the marginal value.

Notes

  • This job only gates merges once it's added as a required status check
    in branch protection. Until then it runs informationally.
  • This is up for design review — feedback welcome on scope (which checks
    belong in the per-commit tier) and on the merge-queue handling.
  • The workflow self-tests by running on its own PR (pull_request trigger).
    The merge_group trigger only fires inside a real merge queue, so that path
    can't be exercised from the PR itself; the field names
    (merge_group.base_sha / merge_group.head_sha) were validated against the
    published webhook payload schema.

How the commit range is computed

The script reads the range out of the event payload at GITHUB_EVENT_PATH
rather than having the workflow interpolate ${{ github.event... }} into a
shell 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: 0 plus
    an explicit-SHA checkout per commit makes this independent of which ref
    actions/checkout materialized.
  • 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.

@vringar
vringar force-pushed the ci/per-commit-checks branch from f361334 to 1041f4f Compare June 15, 2026 22:14
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.34%. Comparing base (428f2e1) to head (c12a7be).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vringar
vringar marked this pull request as ready for review June 19, 2026 23:03
Copilot AI review requested due to automatic review settings June 19, 2026 23:03
@vringar
vringar force-pushed the ci/per-commit-checks branch from 1041f4f to 24076a0 Compare June 19, 2026 23:04

Copilot AI 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.

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 on pull_request and merge_group.
  • Computes a commit range and iterates commit-by-commit, running pre-commit (changed-files), conditional Extension build, import openwpm, and pytest --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.

Comment thread .github/workflows/per-commit-checks.yaml Outdated
Comment thread .github/workflows/per-commit-checks.yaml Outdated
@vringar
vringar force-pushed the ci/per-commit-checks branch from c4533be to 4183b69 Compare July 20, 2026 22:54
@vringar
vringar force-pushed the ci/per-commit-checks branch from 4183b69 to 16984fa Compare September 6, 2026 15:26
…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
vringar force-pushed the ci/per-commit-checks branch from 16984fa to c12a7be Compare September 6, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants