CI required checks
Your required checks passed. Your tests didn’t run.
How conditionally skipped test jobs can satisfy branch protection, and how always-running gate jobs prevent untested merges.
4 min readBy Abdullah Raheel
A useful optimization opened a policy gap
Unit and E2E jobs started only after a run-tests label was added. That let us defer expensive suites while a pull request was still being prepared, but every merge was still supposed to run them. Branch protection did not enforce that intent.
When the job-level condition evaluated to false, the test worker was skipped. GitHub still gave the skipped job a successful conclusion for required-check purposes. The pull request could therefore look green even though no test command had executed.
Keep the worker conditional
I kept the label because it was useful during review. The mistake was making the conditional worker itself a required check. GitHub could satisfy that check when the worker skipped.
unit-tests:
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'labeled' && github.event.label.name == 'run-tests') ||
(github.event.action == 'synchronize' &&
contains(github.event.pull_request.labels.*.name, 'run-tests'))
steps:
- run: pnpm test:unit:coverageThe worker decides whether to run now. A separate gate decides whether the pull request is safe to merge. Mixing those decisions created the false green state.
Separate the worker from the policy
I added a small gate after each worker. It declares the worker in needs, runs with if: always(), and passes only when the worker result is success. A skipped, cancelled, or failed suite now blocks the merge with a useful message.
ci-gate:
name: CI Gate (Unit)
runs-on: ubuntu-latest
needs: [unit-tests]
if: always()
steps:
- name: Check test results
run: |
result="${{ needs.unit-tests.result }}"
if [[ "$result" != "success" ]]; then
echo "::error::Unit tests did not pass (result: $result)"
exit 1
fi
echo "Unit tests passed"Upstream result Required gate
success pass
failure fail
cancelled fail
skipped fail with an actionable label messageKeep the expensive workers conditional
The gates sit in front of real test suites. Unit coverage included 73 Jest files, enforced thresholds, and coverage ratcheting. Playwright ran in Chromium and WebKit with four workers, two CI retries, and shared authenticated state.
Unit and E2E each received a stable gate name. Branch protection can keep requiring those names even if the worker implementation changes later.
Require the gate, not the worker
Branch protection was changed to require CI Gate (Unit) and CI Gate (E2E), not the conditional workers. That made the merge contract explicit: an expected suite must either run successfully or produce a visible failure explaining why it did not run.
- Keep expensive work conditional when the repository has a clear reason to do so.
- Always aggregate every terminal worker result that matters to merge policy.
- Require the stable aggregator name in branch protection.
- Make skipped validation actionable instead of silently green.

