A workflow can produce evidence for a merge decision, but YAML alone does not make that evidence mandatory. A dependable build gate connects the intended event, the exact revision being tested, meaningful test results, and a repository rule requiring the correct check. Review those connections together. A green badge on some earlier run is not proof that the proposed merge has satisfied today’s requirements.
Separate execution from enforcement
Files under .github/workflows/ describe triggers and jobs. The on key chooses events; branch protection or rulesets separately decide which checks must pass before merging. Record the stable check name and review the rule whenever it changes. Distinct workflows should avoid ambiguous job names, and the expected check source should match the trusted application.
Check the revision shown in the pull request’s checks box. A pull_request run commonly tests a synthetic merge commit, not simply the contributor’s head commit. Merge queues need the separate merge_group trigger for their candidate combinations. A successful local build or manually dispatched workflow cannot automatically substitute for the required event and revision.
Make the dependency result explicit
Jobs without dependencies can run in parallel, while steps within a job normally run sequentially. needs establishes prerequisites; conditions determine whether work executes after failure or skipping. A workflow containing only one required test job does not need an artificial dependency or event condition. Requiring those keys everywhere is not a useful correctness test.
For an aggregate gate, wait for every mandatory job and inspect its result explicitly. Use if: ${{ always() }} so a failed prerequisite does not simply skip the gate, then require actual success. If optional work may be skipped, encode that exception deliberately. Do not let a generic success message, or continue-on-error on mandatory tests, turn missing evidence into a passing gate.
The needs context contains direct dependencies, not every job earlier in the dependency graph. If the gate must inspect both a build and a test that depends on that build, declare both jobs directly in the gate’s needs. Otherwise the build’s result is not exposed there merely because it is a transitive prerequisite.
Permissions describe the token, not all credentials
permissions controls GITHUB_TOKEN; it does not constrain an unrelated secret credential passed to a command. A restrictive workflow-level declaration can be inherited by jobs, so repeating it in every job is unnecessary. Grant additional access only where required. The offline exercise below uses no token because it neither checks out private source nor calls GitHub APIs.
Ordinary fork pull requests normally receive a read-only token and no repository secrets, subject to repository settings. Secrets are not themselves read-only tokens. Never move untrusted pull-request code into a privileged pull_request_target execution just to obtain secrets. Log masking is a defensive aid, not permission to print credentials or a guarantee that transformed values cannot leak.
When it breaks
A workflow omitted by path or branch filters can leave its required check pending. A conditionally skipped job, however, can satisfy a required-check rule without running tests. These are different outcomes. Rename checks carefully, and verify the failing, skipped, and cancelled paths as well as the successful case on the hosting service.
Caches require compatible keys and verified dependency installation. Ordinary pull-request caches belong to their merge reference; matching keys alone do not make them restorable by the base branch. Avoid unsupported blanket claims that every fork cache can poison main. Also separate test cancellation from deployment recovery: cancelling an in-progress job does not undo external changes it already made.
Check it quickly
The exercise isolates one aggregate gate decision: accept a prerequisite only when its result is exactly success. In a real workflow, the result would come from needs.test.result, with the prerequisite declared in needs. Here the inputs are deliberately synthetic. It is a local decision exercise, not a workflow validator or a replacement for the project’s real tests.
Save this standard-library probe as gate_check.py and run python3 gate_check.py with Python 3 and Bash available. It runs the shell predicate with synthetic results and includes a deliberately failing test assertion. It writes no files and contacts no service.
import os
import subprocess
import sys
subprocess.run([sys.executable, "-c", "assert 2 + 2 == 4"], check=True)
negative = subprocess.run([sys.executable, "-c", "assert 2 + 2 == 5"],
capture_output=True, text=True)
assert negative.returncode != 0 and "AssertionError" in negative.stderr
print("test-negative-control: rejected")
command = 'test "$TEST_RESULT" = success'
for result in ("success", "failure", "cancelled", "skipped", "", "unexpected"):
run = subprocess.run(["bash", "--noprofile", "--norc", "-c", command],
env={"PATH": os.environ["PATH"], "TEST_RESULT": result},
capture_output=True, text=True)
accepted = run.returncode == 0
assert accepted == (result == "success"), (result, run)
print(f"{result or '<missing>'}: {'accept' if accepted else 'reject'}")
print("PASS: local predicate only; GitHub enforcement not exercised")
Expect the negative control to be rejected, only success to be accepted, and the final scope reminder. This verifies the local command and its rejection cases. It does not parse arbitrary YAML, evaluate GitHub expressions, schedule jobs, inspect repository rules, or prove merge blocking. Those remain hosted integration checks against the actual repository configuration.
Further reading
Sources
- GitHub: workflow syntax, dependencies, conditions and token-permission inheritance, checked 2026-10-15.
- GitHub: understanding Actions, workflows, events, jobs and steps, checked 2026-10-15.
- GitHub: troubleshooting required status checks, revision identity, skipped checks and merge queues, checked 2026-10-15.
- GitHub: workflow events, fork pull requests and privileged trigger boundaries, checked 2026-10-15.
- GitHub: dependency caching, branch and pull-request cache scopes, checked 2026-10-15.
- GitHub: using secrets, secret availability and log handling, checked 2026-10-15.
- GitHub: needs context, direct dependencies and job-result values, checked 2026-10-18.