From 06deaeb35067576c680f4f8f3ac1c7c6163f97f7 Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Thu, 27 Aug 2026 16:22:47 +0000 Subject: [PATCH 1/2] ci: add a branch-protection fan-in gate per workflow GitHub evaluates a job's `if:` before expanding its matrix, so a matrix job skipped at job level reports one check run under the unexpanded name -- `Build CLI for ${{ matrix.platform.target }}` rather than one context per platform. Any per-leg context required by branch protection then never reports and the PR hangs on "Expected -- Waiting for status to be reported". Each PR workflow now ends in a `gate` job that always runs and passes when every job it needs finished success-or-skipped. That gives branch protection one stable context per workflow instead of 12 hand-typed matrix-leg names that also break on any matrix edit (runner labels and Xcode versions are currently baked into them). Each gate's `needs` lists the jobs that are required checks today, so the required set is preserved rather than widened. It also removes a latent ambiguity: the required context `build` currently matches the `build` job in both ruby.yml and wasm.yml. No behavior change on its own -- the gate only aggregates results. --- .github/workflows/pull_request.yml | 29 ++++++++++++++++++++++++++++ .github/workflows/pyo3.yml | 31 ++++++++++++++++++++++++++++++ .github/workflows/ruby.yml | 30 +++++++++++++++++++++++++++++ .github/workflows/wasm.yml | 29 ++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9be9e8a3..29614a10 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -301,3 +301,32 @@ jobs: - name: Setup and build wasm uses: ./.github/actions/setup_build_wasm + + gate: + name: PR Gate + runs-on: ubuntu-latest + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + if: always() + needs: [build_release, test, trunk_check_runner, build_pyo3, build_wasm] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/pyo3.yml b/.github/workflows/pyo3.yml index 4eb7cd43..baf7c6d0 100644 --- a/.github/workflows/pyo3.yml +++ b/.github/workflows/pyo3.yml @@ -300,3 +300,34 @@ jobs: - name: Annotate workflow with S3 reference run: | echo "::notice title=Published bindings::context-py bindings published to S3 at ${{ needs.get-date-sha.outputs.reference }}" + + gate: + name: context-py Gate + runs-on: ubuntu-latest + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + # linux-pytest is omitted: it is not a required check today, and this job + # preserves that set rather than widening it. + if: always() + needs: [linux, macos, sdist] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 66360420..309042a8 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -204,3 +204,33 @@ jobs: platform: x86_64-linux artifact-pattern: "" knapsack-pro-test-suite-token-rspec: ${{ secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC }} + + gate: + name: rspec Gate + runs-on: ubuntu-latest + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + # build-ruby-gem / test-ruby-gem-uploads are omitted: not required checks today. + if: always() + needs: [build] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 53dd62b0..b79d55f6 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -64,3 +64,32 @@ jobs: if: "!cancelled() && github.event_name != 'pull_request'" run: | echo "::notice title=Published bindings::context-js bindings published to S3 at ${{ steps.get-date-sha.outputs.reference }}" + + gate: + name: context-js Gate + runs-on: ubuntu-latest + # Branch-protection fan-in: one stable required context for this whole workflow. + # A matrix job skipped by its job-level `if:` reports a single check run under the + # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is + # never evaluated -- so per-leg contexts never report and required checks hang on + # "Expected". This job always runs, so its name always reports. + if: always() + needs: [build] + steps: + - name: Check fan-in results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + shell: bash + run: | + read -ra results <<<"${RESULTS}" + for result in "${results[@]}"; do + case "${result}" in + # A skipped job is a pass: it was deliberately not run for this change. + success | skipped) ;; + *) + echo "::error::a needed job reported '${result}'" + exit 1 + ;; + esac + done + echo "needed jobs all passed or were skipped: ${RESULTS}" From 33b531b1082014d3c6e238fab5d81c49e3a5b934 Mon Sep 17 00:00:00 2001 From: Tyler Jang Date: Thu, 27 Aug 2026 16:26:55 +0000 Subject: [PATCH 2/2] ci: cap the fan-in gate jobs at 5 minutes The gate only reads `needs.*.result`, so anything beyond a few seconds means it is wedged rather than working -- and a wedged required check blocks merges until the default 6h job timeout expires. --- .github/workflows/pull_request.yml | 1 + .github/workflows/pyo3.yml | 1 + .github/workflows/ruby.yml | 1 + .github/workflows/wasm.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 29614a10..a11ffd92 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -305,6 +305,7 @@ jobs: gate: name: PR Gate runs-on: ubuntu-latest + timeout-minutes: 5 # Branch-protection fan-in: one stable required context for this whole workflow. # A matrix job skipped by its job-level `if:` reports a single check run under the # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is diff --git a/.github/workflows/pyo3.yml b/.github/workflows/pyo3.yml index baf7c6d0..e6f34ba3 100644 --- a/.github/workflows/pyo3.yml +++ b/.github/workflows/pyo3.yml @@ -304,6 +304,7 @@ jobs: gate: name: context-py Gate runs-on: ubuntu-latest + timeout-minutes: 5 # Branch-protection fan-in: one stable required context for this whole workflow. # A matrix job skipped by its job-level `if:` reports a single check run under the # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 309042a8..05601e99 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -208,6 +208,7 @@ jobs: gate: name: rspec Gate runs-on: ubuntu-latest + timeout-minutes: 5 # Branch-protection fan-in: one stable required context for this whole workflow. # A matrix job skipped by its job-level `if:` reports a single check run under the # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index b79d55f6..258ce70a 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -68,6 +68,7 @@ jobs: gate: name: context-js Gate runs-on: ubuntu-latest + timeout-minutes: 5 # Branch-protection fan-in: one stable required context for this whole workflow. # A matrix job skipped by its job-level `if:` reports a single check run under the # *unexpanded* name (`Test for ${{ matrix.platform.target }}`) because the matrix is