How to Set a Commit Status from GitHub Actions
Post a custom status to a commit via the statuses API so an external gate shows up on the commit and PR.
Use the built-in GITHUB_TOKEN (with statuses: write) to POST to the commit statuses API. The status appears on the commit and the PR checks list with your context name.
Post a status with the API
The token authenticates the statuses API call; context names the check shown on the commit.
permissions:
statuses: write
jobs:
external-gate:
runs-on: ubuntu-latest
steps:
- run: |
curl -sS -X POST \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}" \
-d '{"state":"success","context":"perf-budget","description":"under budget"}'Gotchas
- The job needs
permissions: statuses: writeor the API call 403s. statemust be one oferror,failure,pending,success;contextis the unique check name.- Commit statuses are distinct from check runs - to make a status block merges, add it as a required status check in branch protection.
Verify it actually works
A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push
# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions:block listing every scope the job needs.- Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
actions/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.