From c1b504d9403330c7e02e0c7d52236daff7621f35 Mon Sep 17 00:00:00 2001 From: stxkxs <139715017+stxkxs@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:41:11 -0700 Subject: [PATCH 1/2] feat: one required status check that fails closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch protection is about to go on across the org, and it needs a check name that is stable and that actually gates. Naming the real jobs gives neither. Names are not stable. Matrix legs are named after their inputs, so a required context can stop reporting because a component was deleted — and a required context that never reports leaves every pull request Pending forever, from a change that looks unrelated to CI. Naming jobs also cannot keep up: a job added to a workflow is not automatically required, so the branch-protection list is a hand-kept copy of the CI graph that drifts in the direction of less coverage. This adds one gate job per PR-triggered workflow, depending on every other job in it, delegating to the shared action in nanohype/.github. That single name becomes the only required check. Two details carry the whole thing: `if: always()` — without it a failed dependency SKIPS the gate, and GitHub counts a skipped check as passing for branch protection, so the gate would report green in exactly the case it exists to catch. Restricted to pull_request — the gate treats a skipped dependency as a failure, and a job carrying `if: github.event_name == 'pull_request'` is legitimately skipped on a push to main. The gate exists to gate merges, and merges come from pull requests. The shared action refuses to pass on a dependency that is not success (skipped and cancelled included), on an empty needs list, and on any job in the workflow that the gate does not watch. That last check is what keeps the hand-written needs list honest as jobs are added. No repository settings change here. The gate blocks nothing until it is named as a required check. --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ .github/workflows/security.yml | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abf0dc6..37f9218 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,3 +72,26 @@ jobs: # v2 binary requires action v7. v2.12.x is built with Go 1.26, # so it type-checks this module's go 1.26 dependencies cleanly. version: v2.12.2 + + merge-gate: + name: merge gate + runs-on: ubuntu-latest + needs: + [ + build-and-test, + lint, + ] + # always() is load-bearing: without it a failed dependency SKIPS this job, + # and GitHub counts a skipped check as passing for branch protection, so the + # gate would report green exactly when something broke. + # + # Restricted to pull_request because the gate treats a skipped dependency as + # a failure, and a job carrying `if: github.event_name == 'pull_request'` is + # legitimately skipped on a push to main. The gate exists to gate merges, and + # merges come from pull requests; on push it is skipped and gates nothing. + if: always() && github.event_name == 'pull_request' + steps: + - uses: nanohype/.github/actions/merge-gate@6ec6c5b3e6c4a8b15e12da4afd7ac4870a630092 # main + with: + needs: ${{ toJSON(needs) }} + gate-job-id: merge-gate diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 02489ca..c5017d5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -71,3 +71,26 @@ jobs: with: sarif_file: gosec.sarif category: gosec + + merge-gate-security: + name: merge gate (security) + runs-on: ubuntu-latest + needs: + [ + govulncheck, + gosec, + ] + # always() is load-bearing: without it a failed dependency SKIPS this job, + # and GitHub counts a skipped check as passing for branch protection, so the + # gate would report green exactly when something broke. + # + # Restricted to pull_request because the gate treats a skipped dependency as + # a failure, and a job carrying `if: github.event_name == 'pull_request'` is + # legitimately skipped on a push to main. The gate exists to gate merges, and + # merges come from pull requests; on push it is skipped and gates nothing. + if: always() && github.event_name == 'pull_request' + steps: + - uses: nanohype/.github/actions/merge-gate@6ec6c5b3e6c4a8b15e12da4afd7ac4870a630092 # main + with: + needs: ${{ toJSON(needs) }} + gate-job-id: merge-gate-security From a8a77a8fc201bc70b0107edcf9d4f9f1e84efabc Mon Sep 17 00:00:00 2001 From: stxkxs <139715017+stxkxs@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:45:05 -0700 Subject: [PATCH 2/2] fix: run on pushes to main, not to every branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both workflows triggered on `push: branches: ['**']`, so a feature branch push and its pull request each ran them — two check runs of the same name reporting to one commit. Every other repo in the org restricts push to main. That matters now rather than cosmetically. The merge gate carries `github.event_name == 'pull_request'`, because it treats a skipped dependency as a failure and some jobs are legitimately PR-only. On the duplicate push run the gate is therefore skipped, and GitHub scores a skipped check as Success. Two entries named "merge gate" land on the same commit with opposite meanings, and branch protection reads whichever reported last. A required check whose value depends on run ordering is not a gate. Also halves the CI this repository spends per branch. --- .github/workflows/ci.yml | 8 ++++++-- .github/workflows/security.yml | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37f9218..289ba4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,13 @@ name: ci on: + # Restricted to main, as every other repo in the org is. With '**' a feature + # branch push and its pull request both run this workflow, so two check runs + # of the same name report to one commit — and the merge gate is skipped on the + # push one, which GitHub scores as Success. A required check whose value + # depends on which run reported last is not a gate. push: - branches: - - '**' + branches: [main] pull_request: permissions: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c5017d5..97d9db5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,9 +1,13 @@ name: security on: + # Restricted to main, as every other repo in the org is. With '**' a feature + # branch push and its pull request both run this workflow, so two check runs + # of the same name report to one commit — and the merge gate is skipped on the + # push one, which GitHub scores as Success. A required check whose value + # depends on which run reported last is not a gate. push: - branches: - - '**' + branches: [main] pull_request: schedule: # Weekly Tuesday 06:00 UTC — catches newly published CVEs even when no PRs land.