From 6772ad00fe44feb505d748d69a4ec2949120a486 Mon Sep 17 00:00:00 2001 From: Mark Brannan Date: Wed, 2 Sep 2026 18:11:28 -0700 Subject: [PATCH 1/3] ci: add a ci-gate job as the single required status check The branch ruleset needs one context to require. Requiring the matrix legs directly means a renamed or dropped leg silently changes what is enforced, and a required context that never reports blocks every pull request. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb95860..2334363 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,3 +22,20 @@ jobs: # The one dependency is `ampacity`, which is data and ships no code. - run: npm ci - run: npm test + + # The single required status check. Everything else fans in here, so the + # ruleset requires one context and adding or renaming a matrix leg does not + # silently change what is required. `always()` is what lets it evaluate when + # a dependency was skipped rather than being skipped itself -- a required + # context that inherits a skip never reports, and blocks the pull request + # forever. No `name:` here, deliberately: the reported context is the job id. + ci-gate: + needs: [test] + if: always() + runs-on: ubuntu-latest + steps: + - run: | + echo "test: ${{ needs.test.result }}" + if [ "${{ needs.test.result }}" != "success" ]; then + exit 1 + fi From 08ea64a6fe5c0e2dbebda313b53833948f08852e Mon Sep 17 00:00:00 2001 From: Mark Brannan Date: Wed, 2 Sep 2026 18:30:48 -0700 Subject: [PATCH 2/3] ci: give ci-gate an empty permissions block CodeQL flags a job that does not limit GITHUB_TOKEN. This one reads nothing and writes nothing -- its verdict comes from `needs` -- so the right scope is no permissions at all. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2334363..91f273d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,8 @@ jobs: # context that inherits a skip never reports, and blocks the pull request # forever. No `name:` here, deliberately: the reported context is the job id. ci-gate: + # It reads nothing and writes nothing; the results come from `needs`. + permissions: {} needs: [test] if: always() runs-on: ubuntu-latest From e8633bc116331261c271ad92dc2e572a8dbbae81 Mon Sep 17 00:00:00 2001 From: Mark Brannan Date: Wed, 2 Sep 2026 18:47:45 -0700 Subject: [PATCH 3/3] ci: say what always() actually protects against The comment conflated two different skips. A job skipped by `if` reports `skipped`; a workflow skipped by path or branch filters leaves its check pending. Neither is the explicit verdict a ruleset needs, which is the reason for `always()` -- not the "never reports" story the comment told. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91f273d..25d280e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,9 +26,12 @@ jobs: # The single required status check. Everything else fans in here, so the # ruleset requires one context and adding or renaming a matrix leg does not # silently change what is required. `always()` is what lets it evaluate when - # a dependency was skipped rather than being skipped itself -- a required - # context that inherits a skip never reports, and blocks the pull request - # forever. No `name:` here, deliberately: the reported context is the job id. + # a dependency failed or was skipped, so the gate reports an explicit pass or + # fail of its own. Without it the gate would inherit the skip, and a skipped + # required check is not a dependable block: a job skipped by `if` reports + # `skipped`, while a whole workflow skipped by path or branch filters leaves + # the check pending forever. Neither is the verdict a ruleset needs. + # No `name:` here, deliberately: the reported context is the job id. ci-gate: # It reads nothing and writes nothing; the results come from `needs`. permissions: {}