Skip to content

ci(harness): decide relevance in a gate job, not with paths: filters (#116 item 5) - #124

Merged
mmcky merged 3 commits into
mainfrom
ci/harness-relevance-gate
Aug 5, 2026
Merged

ci(harness): decide relevance in a gate job, not with paths: filters (#116 item 5)#124
mmcky merged 3 commits into
mainfrom
ci/harness-relevance-gate

Conversation

@mmcky

@mmcky mmcky commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Prerequisite for making Action harness: all checks a required check on main. 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 checksHandling 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.md and 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-ignore is not the complement of paths — 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 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 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. IGNORED is a hand-written regex evaluated only against live input, so a dropped | or a mis-escaped . could answer "irrelevant" for a real setup-environment/action.yml change — and harness-summary cannot catch that, because all-skipped is a legitimate shape by construction. So the gate derives its must-always-run set from the uses: ./<action> lines in the workflow itself and fails closed if IGNORED ever 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_request the other 15 jobs execute PR-authored composite-action code via uses: ./, and none of them needs a pull-request scope.

harness-summary is 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:

input result
PR #119's real file list (CHANGELOG.md) relevant=false
PR #121's real file list (.gitignore, PLAN.md, TESTING.md, tests/README.md) relevant=false
setup-environment/action.yml relevant=true
.github/fixtures/mini-lectures/lectures/lecture1.md relevant=true
publish-gh-pages/action.yml (not yet covered) relevant=true — fails open
workflow_dispatch relevant=true

Gate — failure directions, which are what make the check trustworthy:

input result
IGNORED widened to swallow setup-environment/ rc=1, gate self-test: IGNORED matches [setup-environment/action.yml], no decision emitted
gh api error on a PR rc=1, fails closed, no decision emitted
compare 404 (force-push) rc=0, relevant=true — fails open
push with no before SHA rc=0, relevant=true — fails open

Summary — 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 main and observed working.

And a green required check gates the hosted-runner unit surface only. bjc-smoke still passes create-issue-on-failure: 'false', there is still no container job, and build-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

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>
Copilot AI lite review requested due to automatic review settings August 5, 2026 05:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.yml and add a gate job 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-summary to 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.

Comment thread .github/workflows/test-actions.yml Outdated
Comment thread tests/README.md Outdated
mmcky and others added 2 commits August 5, 2026 15:42
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>
@mmcky
mmcky merged commit 340da9a into main Aug 5, 2026
16 checks passed
@mmcky
mmcky deleted the ci/harness-relevance-gate branch August 5, 2026 06:04
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.

Follow-ups from the stage-1 action harness (PR #114)

2 participants