Status checks
External CI systems publish status into shithub via the check-runs API. Branch-protection rules can require named contexts to pass before a PR merges.
Concepts
- A check run is one invocation:
(repo, head_sha, name)with a state and metadata. - A check suite is the bag of check runs for one head SHA; shithub computes it server-side.
- Branch protection's "required status checks" matches by name on the head commit.
Create a check run
POST /api/v1/repos/{owner}/{repo}/check-runs
Required scope: repo (write).
Request body
{
"name": "ci/lint",
"head_sha": "8c4e3f2a1b…",
"status": "in_progress",
"started_at": "2026-05-09T16:00:00Z",
"details_url": "https://ci.example/runs/12345",
"external_id": "12345",
"output": {
"title": "Lint",
"summary": "Running golangci-lint",
"text": "..."
}
}
| Field | Required | Notes |
|---|---|---|
name |
yes | The context name. Match this on branch-protection rules. |
head_sha |
yes | 40-char SHA-1 of the commit being checked. |
status |
no | queued, in_progress, completed. Default queued. |
conclusion |
iff status=completed |
success, failure, neutral, cancelled, timed_out, action_required. |
started_at |
no | RFC 3339. |
completed_at |
iff status=completed |
RFC 3339. |
details_url |
no | Where humans go to inspect the run. |
external_id |
no | Your system's identifier; echoed back on read. |
output |
no | {title, summary, text}. Summary is markdown. |
Response
201 Created with the full check-run resource:
{
"id": 9876,
"name": "ci/lint",
"head_sha": "8c4e3f2a1b…",
"status": "in_progress",
"conclusion": null,
"started_at": "2026-05-09T16:00:00Z",
"details_url": "https://ci.example/runs/12345",
"external_id": "12345",
"output": { "title": "Lint", "summary": "Running golangci-lint" }
}
Update a check run
PATCH /api/v1/repos/{owner}/{repo}/check-runs/{id}
Required scope: repo (write).
Same body shape as create; partial. Typical use: flip
status: "completed" with a conclusion.
Conclusion semantics
| Conclusion | Effect on PR merge gate |
|---|---|
success |
Counts as passing. |
failure |
Blocks merge. |
neutral |
Doesn't block; doesn't count as passing either. |
cancelled |
Blocks merge. |
timed_out |
Blocks merge. |
action_required |
Blocks merge with a human-action signal. |
List check runs for a commit
GET /api/v1/repos/{owner}/{repo}/commits/{sha}/check-runs
Required scope: repo:read.
Returns the most recent check run per name on this commit. Older runs are still in the database (audit) but not in this listing.
{
"total_count": 2,
"check_runs": [
{"id": 9876, "name": "ci/lint", "status": "completed", "conclusion": "success", ...},
{"id": 9877, "name": "ci/test", "status": "in_progress", ...}
]
}
List check suites for a commit
GET /api/v1/repos/{owner}/{repo}/commits/{sha}/check-suites
Required scope: repo:read.
Returns the rolled-up suite view per head_sha:
{
"total_count": 1,
"check_suites": [
{
"head_sha": "8c4e3f2a1b…",
"status": "in_progress",
"conclusion": null,
"checks_url": "/api/v1/repos/owner/repo/commits/8c4e3f2a1b…/check-runs"
}
]
}
Idempotency
Check-run creates with the same (repo, head_sha, name, external_id) are coalesced — re-publishing the same run from a
retried CI job is safe.
Permissions
The PAT must belong to a user with write access to the repo.
Bot accounts representing CI runners are the typical pattern;
they're regular shithub accounts with PATs scoped to repo and
nothing else.
View source
| 1 | # Status checks |
| 2 | |
| 3 | External CI systems publish status into shithub via the check-runs |
| 4 | API. Branch-protection rules can require named contexts to pass |
| 5 | before a PR merges. |
| 6 | |
| 7 | ## Concepts |
| 8 | |
| 9 | - A **check run** is one invocation: `(repo, head_sha, name)` |
| 10 | with a state and metadata. |
| 11 | - A **check suite** is the bag of check runs for one head SHA; |
| 12 | shithub computes it server-side. |
| 13 | - Branch protection's "required status checks" matches by **name** |
| 14 | on the head commit. |
| 15 | |
| 16 | ## Create a check run |
| 17 | |
| 18 | ``` |
| 19 | POST /api/v1/repos/{owner}/{repo}/check-runs |
| 20 | ``` |
| 21 | |
| 22 | Required scope: `repo` (write). |
| 23 | |
| 24 | ### Request body |
| 25 | |
| 26 | ```json |
| 27 | { |
| 28 | "name": "ci/lint", |
| 29 | "head_sha": "8c4e3f2a1b…", |
| 30 | "status": "in_progress", |
| 31 | "started_at": "2026-05-09T16:00:00Z", |
| 32 | "details_url": "https://ci.example/runs/12345", |
| 33 | "external_id": "12345", |
| 34 | "output": { |
| 35 | "title": "Lint", |
| 36 | "summary": "Running golangci-lint", |
| 37 | "text": "..." |
| 38 | } |
| 39 | } |
| 40 | ``` |
| 41 | |
| 42 | | Field | Required | Notes | |
| 43 | |----------------|----------|------------------------------------------------------------| |
| 44 | | `name` | yes | The context name. Match this on branch-protection rules. | |
| 45 | | `head_sha` | yes | 40-char SHA-1 of the commit being checked. | |
| 46 | | `status` | no | `queued`, `in_progress`, `completed`. Default `queued`. | |
| 47 | | `conclusion` | iff `status=completed` | `success`, `failure`, `neutral`, `cancelled`, `timed_out`, `action_required`. | |
| 48 | | `started_at` | no | RFC 3339. | |
| 49 | | `completed_at` | iff `status=completed` | RFC 3339. | |
| 50 | | `details_url` | no | Where humans go to inspect the run. | |
| 51 | | `external_id` | no | Your system's identifier; echoed back on read. | |
| 52 | | `output` | no | `{title, summary, text}`. Summary is markdown. | |
| 53 | |
| 54 | ### Response |
| 55 | |
| 56 | `201 Created` with the full check-run resource: |
| 57 | |
| 58 | ```json |
| 59 | { |
| 60 | "id": 9876, |
| 61 | "name": "ci/lint", |
| 62 | "head_sha": "8c4e3f2a1b…", |
| 63 | "status": "in_progress", |
| 64 | "conclusion": null, |
| 65 | "started_at": "2026-05-09T16:00:00Z", |
| 66 | "details_url": "https://ci.example/runs/12345", |
| 67 | "external_id": "12345", |
| 68 | "output": { "title": "Lint", "summary": "Running golangci-lint" } |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | ## Update a check run |
| 73 | |
| 74 | ``` |
| 75 | PATCH /api/v1/repos/{owner}/{repo}/check-runs/{id} |
| 76 | ``` |
| 77 | |
| 78 | Required scope: `repo` (write). |
| 79 | |
| 80 | Same body shape as create; partial. Typical use: flip |
| 81 | `status: "completed"` with a `conclusion`. |
| 82 | |
| 83 | ### Conclusion semantics |
| 84 | |
| 85 | | Conclusion | Effect on PR merge gate | |
| 86 | |-------------------|--------------------------------------------------------| |
| 87 | | `success` | Counts as passing. | |
| 88 | | `failure` | Blocks merge. | |
| 89 | | `neutral` | Doesn't block; doesn't count as passing either. | |
| 90 | | `cancelled` | Blocks merge. | |
| 91 | | `timed_out` | Blocks merge. | |
| 92 | | `action_required` | Blocks merge with a human-action signal. | |
| 93 | |
| 94 | ## List check runs for a commit |
| 95 | |
| 96 | ``` |
| 97 | GET /api/v1/repos/{owner}/{repo}/commits/{sha}/check-runs |
| 98 | ``` |
| 99 | |
| 100 | Required scope: `repo:read`. |
| 101 | |
| 102 | Returns the most recent check run **per name** on this commit. |
| 103 | Older runs are still in the database (audit) but not in this |
| 104 | listing. |
| 105 | |
| 106 | ```json |
| 107 | { |
| 108 | "total_count": 2, |
| 109 | "check_runs": [ |
| 110 | {"id": 9876, "name": "ci/lint", "status": "completed", "conclusion": "success", ...}, |
| 111 | {"id": 9877, "name": "ci/test", "status": "in_progress", ...} |
| 112 | ] |
| 113 | } |
| 114 | ``` |
| 115 | |
| 116 | ## List check suites for a commit |
| 117 | |
| 118 | ``` |
| 119 | GET /api/v1/repos/{owner}/{repo}/commits/{sha}/check-suites |
| 120 | ``` |
| 121 | |
| 122 | Required scope: `repo:read`. |
| 123 | |
| 124 | Returns the rolled-up suite view per `head_sha`: |
| 125 | |
| 126 | ```json |
| 127 | { |
| 128 | "total_count": 1, |
| 129 | "check_suites": [ |
| 130 | { |
| 131 | "head_sha": "8c4e3f2a1b…", |
| 132 | "status": "in_progress", |
| 133 | "conclusion": null, |
| 134 | "checks_url": "/api/v1/repos/owner/repo/commits/8c4e3f2a1b…/check-runs" |
| 135 | } |
| 136 | ] |
| 137 | } |
| 138 | ``` |
| 139 | |
| 140 | ## Idempotency |
| 141 | |
| 142 | Check-run creates with the same `(repo, head_sha, name, |
| 143 | external_id)` are coalesced — re-publishing the same run from a |
| 144 | retried CI job is safe. |
| 145 | |
| 146 | ## Permissions |
| 147 | |
| 148 | The PAT must belong to a user with **write access** to the repo. |
| 149 | Bot accounts representing CI runners are the typical pattern; |
| 150 | they're regular shithub accounts with PATs scoped to `repo` and |
| 151 | nothing else. |