From 7dba36fb03d56da03e0abff152d7a582eb4f10fa Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 2 Jun 2026 06:38:54 -0400 Subject: [PATCH] fix(ci): add always-reporting CI Gate so fork PRs stop deadlocking The validate/build/test/compose-parity jobs are gated to trusted author associations so untrusted fork code never runs on the self-hosted runners. But a job skipped via a job-level if reports no status, and branch protection requires the job names typecheck/lint/format/build/test directly, so fork PRs hang on 'Expected - waiting for status to be reported' and can never merge. Add a single CI Gate job (if: always(), needs the gated jobs, no checkout so it runs no third-party code) that fails only when a gated job actually failed/cancelled and passes when they succeed or skip-for-fork. Branch protection should require CI Gate instead of the individual job names. Closes #1461 --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ba79cce..e4315973 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,3 +160,37 @@ jobs: - name: Assert compose parity (compose.yaml vs docker-compose.integrated.yml) run: bash tests/docker/compose-parity.sh + + # Single always-reporting status that branch protection requires. + # + # The validate/build/test jobs are gated to trusted author associations so + # untrusted fork code never executes on the self-hosted runners. A job skipped + # by a job-level `if` reports no status, so requiring those job names directly + # leaves fork PRs stuck on "Expected - waiting for status to be reported" + # forever. Requiring this gate instead keeps the contract satisfiable: + # - trusted PR: gate is green only if every gated job actually succeeded + # - fork PR: gated jobs skip, gate reports green so the PR is mergeable + # (fork code is still reviewed by a maintainer before merge) + # + # No checkout here: the gate only inspects upstream job results, so it runs no + # third-party code and is safe on the self-hosted runner. + ci-gate: + if: always() + needs: [validate, build, test] + runs-on: [self-hosted, linux, x64] + name: CI Gate + steps: + - name: Verify gated jobs did not fail + env: + VALIDATE: ${{ needs.validate.result }} + BUILD: ${{ needs.build.result }} + TEST: ${{ needs.test.result }} + run: | + echo "validate=$VALIDATE build=$BUILD test=$TEST" + for result in "$VALIDATE" "$BUILD" "$TEST"; do + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "[X] A required CI job did not pass (result: $result)" + exit 1 + fi + done + echo "[OK] CI gate satisfied (success or skipped-for-fork)"