ci(harness): decide relevance in a gate job, not with paths: filters (#116 item 5) - #124
Merged
Conversation
Prerequisite for making `Action harness: all checks` a required check on main (#116 item 5). A `paths:` filter suppresses creation of the workflow RUN, not just its jobs, so no check run is ever published for that commit and a required check sits in "Expected — waiting for status to be reported" forever. GitHub documents this directly ("Troubleshooting required status checks", Handling skipped but required checks): a workflow skipped by path filtering blocks merging, whereas a job skipped by a conditional reports Success. We saw it live — release PR #119 touched only CHANGELOG.md and GitHub reported "no checks reported on the branch"; docs PR #121 likewise ran nothing. Note #116 item 5's own suggestion — a companion workflow with negated paths — does not work: `paths-ignore` is not the complement of `paths`, so a PR touching both covered and uncovered paths fires BOTH workflows and produces two same-named check runs on one SHA. That case is the norm here, not an edge case. So: both `paths:` filters go, the workflow always runs, and a `gate` job decides relevance. Only the four root jobs take `needs: gate`; everything else already chains off them, and a job whose needs were skipped is itself skipped. The decision rule is an IGNORE list, not a cover list, and that direction is the point. For a required check the expensive mistake is a green earned by running nothing, so anything unrecognised — a new action directory, a new fixture — runs the whole harness. A cover list would invert this: forgetting to extend it would silently rubber-stamp untested action changes, where today the same omission blocks the merge. The gate also self-tests. It derives the must-always-run set from the `uses: ./<action>` lines in the workflow itself, so a typo that widened IGNORED to swallow a real action path fails the gate closed rather than skipping the suite — which harness-summary would otherwise accept as a legitimate shape. Deriving it from the workflow means future coverage (e.g. publish-gh-pages) is protected with no second edit. Permissions are job-scoped, not workflow-scoped: on a fork pull_request the other jobs run PR-authored composite-action code via `uses: ./`, and none of them needs a pull-request scope. harness-summary is rewritten to certify both shapes — all-ran and all-skipped — to reject a vacuous empty job set, and to fail when the gate and the fan-out disagree. Verified locally before pushing, by extracting both scripts from the YAML and running them against mocked inputs: the gate over the real file lists of #119 and #121 (both correctly irrelevant), an action change, a fixture change, a workflow_dispatch, push with no before-SHA, a compare 404 from a force-push (fails open), a gh API error (fails closed), and a deliberately widened IGNORED (fails closed with the self-test error). The summary was exercised across all ten reachable shapes. Refs #116 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the GitHub Actions “action harness” workflow so it always creates a workflow run (avoiding paths:-suppressed required checks), and moves “does this harness apply?” logic into a new gate job that conditionally skips the expensive jobs while still publishing a successful required check when appropriate.
Changes:
- Remove
paths:filters from.github/workflows/test-actions.ymland add agatejob that determines whether the harness is relevant for the current event’s changed files. - Make the four root harness jobs depend on the gate output (skip-on-irrelevant), and update
harness-summaryto validate both “all-ran” and “all-skipped” shapes and reject vacuous success. - Update docs/metadata (TESTING/CONTRIBUTING/tests README) and CHANGELOG to reflect the new gate-based behavior and the required-check rationale.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/README.md |
Updates the test-asset map and documents the new gate-based harness triggering model. |
TESTING.md |
Updates harness description to explain always-run workflow + gate-controlled job fan-out. |
CONTRIBUTING.md |
Adds contributor guidance to avoid reintroducing paths: filters and to extend the gate ignore list instead. |
CHANGELOG.md |
Records the CI behavior change and rationale under Unreleased. |
.github/workflows/test-actions.yml |
Removes paths: filters, adds relevance gate, wires root jobs to gate output, and hardens harness-summary certification logic. |
The first CI run of this PR reported: gate self-test passed; harness invokes: build-jupyter-cache build-lectures publish-gh-pages restore-jupyter-cache setup-environment publish-gh-pages is not invoked by the harness. The unanchored grep was matching the action name out of the gate's own COMMENT, which mentioned `uses: ./publish-gh-pages` as an example of future coverage. Harmless in effect — the extra entry only enlarges the must-run set, so it fails safe — but it made the self-test assert something other than what it appears to, which is precisely the failure mode this job exists to prevent. Anchoring to the start of a line counts only real step invocations. Caught by reading the job's own output rather than by a test, which is worth noting: the self-test cannot detect this class itself. Refs #116 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot review. The line claimed "a relevance gate plus 15 jobs exercising ... via uses: ./ local paths", which implies harness-summary exercises actions. It does not — it invokes nothing and only certifies the others. The real shape is the gate, 14 action-invoking jobs, and the summary. Verified against the workflow rather than counted by hand. Refs #116 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 6, 2026
Merged
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.
Prerequisite for making
Action harness: all checksa required check onmain. Closes #116 item 5 — though not the way that item proposed.Why the filters have to go
A
paths:filter suppresses creation of the workflow run, not just its jobs. No run means no check run is ever published for that commit, so a required check sits in "Expected — waiting for status to be reported" forever. GitHub documents this directly, in Troubleshooting required status checks → Handling skipped but required checks: a workflow skipped by path filtering blocks merging, whereas a job skipped by a conditional reports Success.Two live demonstrations from this week: release PR #119 touched only
CHANGELOG.mdand GitHub reported "no checks reported on the 'release/v0.10.0' branch"; docs PR #121 ran nothing either. Under branch protection both would have been unmergeable.Why not the companion workflow #116 suggested
#116 item 5 proposed "a companion workflow with the negated paths that emits a passing job of the same name". That does not work.
paths-ignoreis not the complement ofpaths— both triggers are existential over the changed-file set, so a PR touching both a covered and an uncovered path satisfies both filters, fires both workflows, and produces two check runs with the same name on one SHA. That case is the norm in this repo, not an edge case.The design
Both filters go; the workflow always runs; a
gatejob decides relevance. Only the four root jobs takeneeds: gate— everything else already chains off them, and a job whoseneedswere skipped is itself skipped.The decision rule is an ignore list, not a cover list, and that direction is the whole point. For a required check the expensive mistake is a green earned by running nothing. So anything unrecognised — a new action directory, a new fixture, a new helper — runs the whole harness. A cover list inverts the failure direction: forgetting to extend it would silently rubber-stamp untested action changes, whereas today the same omission blocks the merge. Forgetting to extend the ignore list only costs CI minutes, which are free on a public repo.
The gate self-tests.
IGNOREDis a hand-written regex evaluated only against live input, so a dropped|or a mis-escaped.could answer "irrelevant" for a realsetup-environment/action.ymlchange — andharness-summarycannot catch that, because all-skipped is a legitimate shape by construction. So the gate derives its must-always-run set from theuses: ./<action>lines in the workflow itself and fails closed ifIGNOREDever matches one. Deriving it from the workflow means future coverage (e.g.publish-gh-pages) is protected with no second edit.Permissions are job-scoped. On a fork
pull_requestthe other 15 jobs execute PR-authored composite-action code viauses: ./, and none of them needs a pull-request scope.harness-summaryis rewritten to certify both shapes, reject a vacuous empty job set, and fail when the gate and the fan-out disagree.Verification
Both scripts were extracted from the YAML and run against mocked inputs before pushing.
Gate — decisions:
CHANGELOG.md)relevant=false.gitignore,PLAN.md,TESTING.md,tests/README.md)relevant=falsesetup-environment/action.ymlrelevant=true.github/fixtures/mini-lectures/lectures/lecture1.mdrelevant=truepublish-gh-pages/action.yml(not yet covered)relevant=true— fails openworkflow_dispatchrelevant=trueGate — failure directions, which are what make the check trustworthy:
IGNOREDwidened to swallowsetup-environment/gate self-test: IGNORED matches [setup-environment/action.yml], no decision emittedgh apierror on a PRrelevant=true— fails openbeforeSHArelevant=true— fails openSummary — all ten reachable shapes: relevant+all-success → green; relevant+any failure or skip → red; not-relevant+all-skipped → green; not-relevant+any job ran → red; gate failed / skipped / missing → red; no jobs besides the gate → red; gate succeeded with no decision → red.
What this does not do
It does not enable branch protection — that is a separate, deliberate step once this is on
mainand observed working.And a green required check gates the hosted-runner unit surface only.
bjc-smokestill passescreate-issue-on-failure: 'false', there is still no container job, andbuild-jupyter-cache's internal calls still run@v0. The chain that actually broke in lecture-dp is covered by the canary, not by this check.Refs #116
🤖 Generated with Claude Code