Skip to content

ci: add a single required CI gate job - #52

Merged
YASoftwareDev merged 9 commits into
masterfrom
ci/required-gate
Sep 17, 2026
Merged

YASoftwareDev merged 9 commits into
masterfrom
ci/required-gate

Conversation

@YASoftwareDev

Copy link
Copy Markdown
Owner

What

A ci-gate job ("CI gate") that needs every other job and fails unless all of them succeeded. Intended as the single required status check for branch protection on master. Folds in v1.11.7.

Why

master has no branch protection at all (verified: GET /branches/master/protection → 404, no rulesets). Nothing mechanically prevents merging a red PR — it has rested entirely on me checking CI by hand, and earlier today a trailing echo masked a watcher's exit code and nearly had me report a red run as green. That is a rule that has already been broken once, so it wants a control rather than more care.

Requiring the 29 matrix contexts individually would mean editing the protection rule in lockstep with the matrix, and a renamed cell would quietly stop being required. One aggregate check is stable.

Verified

Both properties red-proofed before wiring, since a gate nobody has watched fail is decoration:

Case Result
all success pass
one failure fail
one skipped fail
one cancelled fail
empty results fail

The empty case was a real bug found by the proof: the first draft passed vacuously when no dependency results arrived. "Cannot tell" must not read as "fine".

Completeness assertion, run against a mutated workflow: dropping tmux-status-rows from needs → FAIL - not required: tmux-status-rows; unmodified → PASS - gate requires all 6. So a job added later cannot sit outside the gate while CI stays green.

if: always() is deliberate — without it the gate would be skipped when a dependency fails, and a required check that never reports leaves the PR hanging rather than failing.

Follow-up (not in this PR)

Once merged, branch protection on master gets: required status check CI gate, PRs required (0 approvals, single maintainer), no force pushes, no deletions, enforced for admins — so the gate binds the owner too, which is the point.

master has no branch protection, so nothing mechanically stops a red PR being
merged - it has rested on checking CI by hand, and a masked exit code nearly
got a red run reported as green today.

The gate aggregates every job so protection can require ONE stable check:
requiring 29 matrix contexts directly means editing the rule whenever the
matrix changes, and a renamed cell would silently stop being required.

Two properties red-proofed before wiring it:
- results logic (bash): all-success passes; failure, skipped, cancelled and
  EMPTY all fail - no results must not read as fine
- completeness: dropping tmux-status-rows from `needs` makes the gate fail,
  so a job added later cannot sit outside it while CI stays green
GitHub expressions reject double-quoted strings, so `join(needs.*.result, " ")`
made the whole workflow invalid: the run concluded in 0s with no jobs and no
pull_request run was created. actionlint did not flag it.
Bisected the invalid-workflow failure: a minimal ci-gate job was accepted, and
`join(needs.*.result, ' ')` was accepted too, so the fault lay in the parts
stripped in between - the trailing two-line comment after `if: always()` being
the remaining suspect. Comments now sit on their own lines above each key.

The completeness assertion moves to tests/ci-gate-covers-all.py: tracked,
runnable locally, and red-proofed (dropping install-nosudo from needs makes it
exit 1 naming that job) rather than buried in a YAML heredoc.
GitHub scans the whole workflow for expressions, comments included, so the
literal `${{ }}` in a comment was an empty expression and made the file
invalid - the second of two faults, introduced while documenting the first.
An invalid workflow file runs nothing, so CI cannot report the fault - and once
branch protection requires `CI gate`, an invalid workflow means that check never
reports and the PR blocks with no visible cause. This class has to be caught
locally, which is why it is a script rather than a job.

Catches both faults that cost runs on PR #52: a double-quoted string inside an
expression, and the expression delimiters written in a comment (GitHub scans
comments, and an empty pair is a syntax error). Red-proofed by reintroducing
each into a copy of the workflow - both exit 1; the clean tree passes 3/3.

Reports a skip by name when actionlint is absent, so a pass never claims more
coverage than it checked.
@YASoftwareDev

Copy link
Copy Markdown
Owner Author

Added after the initial description: tests/lint-workflows.sh, a pre-push linter for this class of fault.

Two genuine faults were found in this PR's own workflow, both invalid-workflow errors:

  1. join(needs.*.result, " ") — GitHub expressions reject double-quoted strings.
  2. The literal expression delimiters written in a comment while documenting fault 1 — GitHub scans comments too, and an empty pair is a syntax error.

Diagnosis was slow because the signal is poor: 0s run, zero jobs, no pull_request run, no annotation via the API. YAML parsing and GitHub's published JSON schema both called the file valid; actionlint caught the second fault only. Hence the linter, which must be a script rather than a job — an invalid workflow runs nothing, so CI structurally cannot report it, and with CI gate required the PR would block with no visible cause.

@YASoftwareDev
YASoftwareDev merged commit 7602b42 into master Sep 17, 2026
30 checks passed
@YASoftwareDev
YASoftwareDev deleted the ci/required-gate branch September 18, 2026 04:47
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