| 1 | --- |
| 2 | title: Checks and commit statuses |
| 3 | description: How Diffinery Actions, external CI and bots report check runs and commit statuses on commits, and how they decide required checks in branch protection. |
| 4 | --- |
| 5 | |
| 6 | Checks tell you whether a commit passes your tests. Diffinery Actions reports a check for every job it runs, and any other CI system or bot can report its own results through the GitHub-compatible REST API, as check runs or as commit statuses. Pull requests, commits and the merge queue show them, and branch protection can require some of them to pass before a pull request merges. |
| 7 | |
| 8 | ## Check runs and commit statuses |
| 9 | |
| 10 | There are two kinds of report. Both show up in the same lists and both can satisfy a required check. |
| 11 | |
| 12 | | | Check run | Commit status | |
| 13 | |---|---|---| |
| 14 | | Reported by | Diffinery Actions (one per job), or any account with the write role over `POST /repos/{owner}/{repo}/check-runs` | Any account with the write role over `POST /repos/{owner}/{repo}/statuses/{sha}` | |
| 15 | | Identified by | Its `name` | Its `context` | |
| 16 | | States | `status`: `queued`, `in_progress`, `completed`; when completed, a `conclusion` such as `success` or `failure` | `state`: `pending`, `success`, `failure`, `error` | |
| 17 | | Extra detail | Title, summary, text and line annotations | A one-line description | |
| 18 | | Can be changed | Yes, by the account that created it (`PATCH`) | No: post a new status for the same context; the newest one counts | |
| 19 | | Webhook | None | `status` | |
| 20 | |
| 21 | Use check runs when you want annotations or to update one report as the build progresses. Use commit statuses for the simplest integration: one request per state change. |
| 22 | |
| 23 | ## Checks from Diffinery Actions |
| 24 | |
| 25 | Every job of a [Diffinery Actions](/docs/actions/overview) workflow run reports a check run named `<workflow name> / <job name>`, for example `ci / test` for the job `test` in a workflow with `name: ci`. The check is: |
| 26 | |
| 27 | - created as `queued` when the job is scheduled, set to `in_progress` when it starts, and `completed` with the job's conclusion (for example `success`, `failure`, `cancelled` or `timed_out`) when it ends; |
| 28 | - reported on the commit the run is for. For pull request runs, that is the pull request's head commit, not the test merge commit; |
| 29 | - linked to the job's page (`https://diffinery.com/acme/website/actions/runs/{run_id}/job/{job_id}`); |
| 30 | - reported by the app `diffinery-actions`. |
| 31 | |
| 32 | Re-running a job replaces the check of the same name on the same commit. A job that a failed-job re-run keeps from the earlier attempt keeps its conclusion, and its check run points at the new attempt. Check runs from Actions can't be changed over the REST API: a `PATCH` fails with `check runs from Diffinery Actions can't be changed over the API`. |
| 33 | |
| 34 | A job's `check_run_url` (in `GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs` and `GET /repos/{owner}/{repo}/actions/jobs/{job_id}`) is `https://diffinery.com/api/v3/repos/acme/website/check-runs/{job_id}`. `GET` on it returns the check run that job reports (its latest one, after a re-run), so tools that follow `check_run_url` from a job to its check run work as on GitHub. |
| 35 | |
| 36 | > **Note:** GitHub names an Actions check run after the job only. In Diffinery the name includes the workflow name, so required check names copied from GitHub usually need the `<workflow> / ` prefix. |
| 37 | |
| 38 | A step can also report with the job's own `GITHUB_TOKEN`, within the job's `permissions:`: |
| 39 | |
| 40 | - **Commit statuses** need `statuses: write`. They are reported by `diffinery-actions[bot]` and count toward required checks like any other non-agent report (in a run whose code came from an agent, the token acts as an agent, and its statuses don't count). |
| 41 | - **Check runs** can't be created with the job token: `POST .../check-runs` fails with `this account can't report check runs`. Report a commit status instead, or use a personal token with the `write` scope stored as a secret. |
| 42 | |
| 43 | ```yaml |
| 44 | permissions: |
| 45 | contents: read |
| 46 | statuses: write |
| 47 | steps: |
| 48 | - run: | |
| 49 | curl -sS -X POST -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" \ |
| 50 | "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \ |
| 51 | -d '{"state": "success", "context": "lighthouse", "description": "Score 98"}' |
| 52 | env: |
| 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 54 | ``` |
| 55 | |
| 56 | Without `statuses: write`, the request fails with `403` and `Resource not accessible by integration (the workflow's GITHUB_TOKEN needs permissions: statuses: write)`. |
| 57 | |
| 58 | ## Report results from external CI |
| 59 | |
| 60 | An external CI system (or any script or bot) reports results with a token for an account that has the **write** role on the repository. A personal access token with the `write` scope works. For results that should satisfy required checks, use a regular account, not an agent: reports from agents never satisfy required checks (see [Agents and checks](#agents-and-checks)). |
| 61 | |
| 62 | A common setup is a dedicated account, for example `ci-bot`, added as a collaborator with the write role, with a `write` token stored in your CI system. Bind your required checks to it with [requiredCheckSources](#bind-a-required-check-to-its-reporter) so nobody else can satisfy them. |
| 63 | |
| 64 | ### Report a commit status |
| 65 | |
| 66 | ```sh |
| 67 | SHA=9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d |
| 68 | |
| 69 | # When the build starts |
| 70 | curl -sS -X POST \ |
| 71 | -H "Authorization: Bearer dfy_..." \ |
| 72 | -H "Content-Type: application/json" \ |
| 73 | https://diffinery.com/api/v3/repos/acme/website/statuses/$SHA \ |
| 74 | -d '{ |
| 75 | "state": "pending", |
| 76 | "context": "ci/external", |
| 77 | "description": "Build #812 started", |
| 78 | "target_url": "https://ci.example.com/builds/812" |
| 79 | }' |
| 80 | |
| 81 | # When it finishes |
| 82 | curl -sS -X POST \ |
| 83 | -H "Authorization: Bearer dfy_..." \ |
| 84 | -H "Content-Type: application/json" \ |
| 85 | https://diffinery.com/api/v3/repos/acme/website/statuses/$SHA \ |
| 86 | -d '{ |
| 87 | "state": "success", |
| 88 | "context": "ci/external", |
| 89 | "description": "Build #812 passed in 3m 12s", |
| 90 | "target_url": "https://ci.example.com/builds/812" |
| 91 | }' |
| 92 | ``` |
| 93 | |
| 94 | | Field | Required | Description | |
| 95 | |---|---|---| |
| 96 | | `state` | yes | `pending`, `success`, `failure` or `error`. Anything else fails with `state must be one of error, failure, pending, success`. | |
| 97 | | `context` | no | The name the status is reported under, and the name a required check matches. Default `default`. At most 255 characters (longer values are cut). | |
| 98 | | `description` | no | A short description, shown next to the check. At most 1024 characters (longer values are cut). | |
| 99 | | `target_url` | no | A link to the build. Must start with `http://` or `https://`, otherwise the request fails with `target_url must be an http(s) URL`. | |
| 100 | |
| 101 | The `{sha}` in the path is normally a full commit SHA. Diffinery also accepts anything that resolves to a commit, such as a branch name, and stores the status on the resolved commit. A commit that does not exist fails with `no commit found for SHA: <sha>`. |
| 102 | |
| 103 | The response is `201`: |
| 104 | |
| 105 | ```json |
| 106 | { |
| 107 | "url": "https://diffinery.com/api/v3/repos/acme/website/statuses/9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d", |
| 108 | "avatar_url": "https://diffinery.com/api/avatars/ci-bot", |
| 109 | "id": 17, |
| 110 | "node_id": "U3RhdHVzQ29udGV4dDphY21lL3dlYnNpdGUvMTc", |
| 111 | "state": "success", |
| 112 | "description": "Build #812 passed in 3m 12s", |
| 113 | "target_url": "https://ci.example.com/builds/812", |
| 114 | "context": "ci/external", |
| 115 | "created_at": "2026-10-13T09:40:00Z", |
| 116 | "updated_at": "2026-10-13T09:40:00Z", |
| 117 | "creator": { |
| 118 | "login": "ci-bot", |
| 119 | "id": 1058134509943791, |
| 120 | "node_id": "VXNlcjpjMWIyYTNkNGU1ZjYwNzE4MjkzYTRiNWM2ZDdlOGY5YQ", |
| 121 | "avatar_url": "https://diffinery.com/api/avatars/ci-bot", |
| 122 | "gravatar_id": "", |
| 123 | "url": "https://diffinery.com/api/v3/users/ci-bot", |
| 124 | "html_url": "https://diffinery.com/ci-bot", |
| 125 | "repos_url": "https://diffinery.com/api/v3/users/ci-bot/repos", |
| 126 | "organizations_url": "https://diffinery.com/api/v3/users/ci-bot/orgs", |
| 127 | "type": "User", |
| 128 | "user_view_type": "public", |
| 129 | "site_admin": false |
| 130 | } |
| 131 | } |
| 132 | ``` |
| 133 | |
| 134 | Statuses are append-only: to change a status, post a new one with the same `context`. A commit can have up to 1,000 statuses per context; after that, posting fails with `this SHA and context has reached the maximum number of statuses`. Every new status sends a `status` webhook event. |
| 135 | |
| 136 | ### Read commit statuses |
| 137 | |
| 138 | | Endpoint | Returns | |
| 139 | |---|---| |
| 140 | | `GET /repos/{owner}/{repo}/commits/{ref}/statuses` (also `GET /repos/{owner}/{repo}/statuses/{ref}`) | Every status of the commit, newest first, paginated | |
| 141 | | `GET /repos/{owner}/{repo}/commits/{ref}/status` | The combined status: the newest status of each context, and an overall state | |
| 142 | |
| 143 | `{ref}` can be a SHA, a branch or a tag. An unknown ref returns `404` with `No commit found for the ref`. Reading needs the read role (none for a public repository). |
| 144 | |
| 145 | ```sh |
| 146 | curl -sS -H "Authorization: Bearer dfy_..." \ |
| 147 | https://diffinery.com/api/v3/repos/acme/website/commits/main/status |
| 148 | ``` |
| 149 | |
| 150 | The combined status has these fields: |
| 151 | |
| 152 | | Field | Description | |
| 153 | |---|---| |
| 154 | | `state` | `failure` if any context's newest status is `failure` or `error`; otherwise `pending` if any is `pending` or there are no statuses; otherwise `success` | |
| 155 | | `statuses` | The newest status of each context, sorted by context (paginated with `per_page`, default 100) | |
| 156 | | `sha` | The commit | |
| 157 | | `total_count` | Number of contexts | |
| 158 | | `repository` | The repository object | |
| 159 | | `commit_url`, `url` | API links | |
| 160 | |
| 161 | The combined status only covers commit statuses. Check runs are listed separately (see [Read check runs and check suites](#read-check-runs-and-check-suites)). |
| 162 | |
| 163 | ### Report a check run |
| 164 | |
| 165 | Create the check run when the build starts, keep its `id`, and update it when the build ends: |
| 166 | |
| 167 | ```sh |
| 168 | SHA=9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d |
| 169 | |
| 170 | # Start: returns the check run, including its numeric "id" |
| 171 | curl -sS -X POST \ |
| 172 | -H "Authorization: Bearer dfy_..." \ |
| 173 | -H "Content-Type: application/json" \ |
| 174 | https://diffinery.com/api/v3/repos/acme/website/check-runs \ |
| 175 | -d '{ |
| 176 | "name": "ci/external", |
| 177 | "head_sha": "'"$SHA"'", |
| 178 | "status": "in_progress", |
| 179 | "details_url": "https://ci.example.com/builds/812", |
| 180 | "external_id": "812", |
| 181 | "output": { "title": "Building", "summary": "Build #812 is running" } |
| 182 | }' |
| 183 | |
| 184 | # Finish, with two annotations |
| 185 | curl -sS -X PATCH \ |
| 186 | -H "Authorization: Bearer dfy_..." \ |
| 187 | -H "Content-Type: application/json" \ |
| 188 | https://diffinery.com/api/v3/repos/acme/website/check-runs/31 \ |
| 189 | -d '{ |
| 190 | "conclusion": "failure", |
| 191 | "output": { |
| 192 | "title": "2 lint errors", |
| 193 | "summary": "eslint found 2 problems", |
| 194 | "annotations": [ |
| 195 | { "path": "src/app.ts", "start_line": 12, "end_line": 12, "annotation_level": "failure", "message": "Unexpected any" }, |
| 196 | { "path": "src/util.ts", "start_line": 40, "end_line": 41, "annotation_level": "warning", "title": "Unused variable", "message": "'tmp' is never read" } |
| 197 | ] |
| 198 | } |
| 199 | }' |
| 200 | ``` |
| 201 | |
| 202 | Fields of `POST /repos/{owner}/{repo}/check-runs`: |
| 203 | |
| 204 | | Field | Required | Description | |
| 205 | |---|---|---| |
| 206 | | `name` | yes | The check's name, at most 255 characters. Required checks match this. Otherwise the request fails with `name is required`. | |
| 207 | | `head_sha` | yes | The commit. A branch or tag name also works. Unknown commits fail with `head_sha is not a commit in this repository`. | |
| 208 | | `status` | no | Default `queued`. | |
| 209 | | `conclusion` | no | Setting it marks the check run `completed`. | |
| 210 | | `details_url` | no | Link to the build; the check's name links there in the web UI. | |
| 211 | | `external_id` | no | Your own id for the run. | |
| 212 | | `output` | no | `title`, `summary`, `text` and `annotations`. | |
| 213 | |
| 214 | Fields of `PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}`: |
| 215 | |
| 216 | | Field | Description | |
| 217 | |---|---| |
| 218 | | `name` | Rename the check run. | |
| 219 | | `status` | `queued`, `in_progress`, `completed`, `waiting`, `requested` or `pending`. Other values fail with `invalid status`. | |
| 220 | | `conclusion` | `success`, `failure`, `neutral`, `cancelled`, `skipped`, `timed_out`, `action_required` or `stale`. Other values fail with `invalid conclusion`. Setting a conclusion marks the check run `completed`. `status: completed` without a conclusion fails with `conclusion is required when status is completed`. | |
| 221 | | `details_url` | Must start with `http://` or `https://` (`details_url must be an http(s) URL`). | |
| 222 | | `external_id` | Your own id. | |
| 223 | | `started_at`, `completed_at` | ISO 8601 times. When omitted, Diffinery records when the check run started and completed. | |
| 224 | | `output.title`, `output.summary`, `output.text` | Replace the stored values. Each is cut to 65,535 characters. | |
| 225 | | `output.annotations` | Added to the annotations already stored, like GitHub. At most 50 per request are taken, and a check run keeps at most 1,000. | |
| 226 | |
| 227 | Each annotation can have `path`, `start_line`, `end_line`, `start_column`, `end_column`, `annotation_level` (`notice`, `warning` or `failure`; default `notice`), `title`, `message` and `raw_details`. Read them back with `GET /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations`. The web UI does not display annotations. |
| 228 | |
| 229 | The response of both requests is the check run: |
| 230 | |
| 231 | ```json |
| 232 | { |
| 233 | "id": 31, |
| 234 | "head_sha": "9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d", |
| 235 | "node_id": "Q2hlY2tSdW46OWY4ZTdkNmM1YjRhMzkyODE3MDZmNWU0ZDNjMmIxYTA", |
| 236 | "external_id": "812", |
| 237 | "url": "https://diffinery.com/api/v3/repos/acme/website/check-runs/31", |
| 238 | "html_url": "https://ci.example.com/builds/812", |
| 239 | "details_url": "https://ci.example.com/builds/812", |
| 240 | "status": "completed", |
| 241 | "conclusion": "failure", |
| 242 | "started_at": "2026-10-13T09:36:01Z", |
| 243 | "completed_at": "2026-10-13T09:39:13Z", |
| 244 | "output": { |
| 245 | "title": "2 lint errors", |
| 246 | "summary": "eslint found 2 problems", |
| 247 | "text": null, |
| 248 | "annotations_count": 2, |
| 249 | "annotations_url": "https://diffinery.com/api/v3/repos/acme/website/check-runs/31/annotations" |
| 250 | }, |
| 251 | "name": "ci/external", |
| 252 | "check_suite": { "id": "app-ci-bot-9c4d8e2f1a0b3c5d7e9f2a4b6c8d0e1f3a5b7c9d" }, |
| 253 | "app": { "id": "ci-bot", "slug": "ci-bot", "node_id": "QXBwOmNpLWJvdA", "name": "ci-bot", "owner": null }, |
| 254 | "pull_requests": [], |
| 255 | "run_id": null, |
| 256 | "job_id": null |
| 257 | } |
| 258 | ``` |
| 259 | |
| 260 | - `app` is the login of the account that reported the check run, or `diffinery-actions` for Actions. `run_id` and `job_id` are set only for Actions checks. |
| 261 | - A check run is keyed by commit, name and reporter. Posting again with the same `head_sha` and `name` from the same account updates that check run (same `id`) instead of adding a second one. |
| 262 | - Accounts whose login starts with `diffinery-actions` can't report check runs (`this account can't report check runs`). |
| 263 | |
| 264 | ### Who may update a check run |
| 265 | |
| 266 | Only the account that created a check run can update it. Anyone else, including repository admins, gets `only the app that created a check run can update it`. Check runs from Diffinery Actions can't be updated by anyone over the API. Updating needs the write role, like creating. |
| 267 | |
| 268 | ### Read check runs and check suites |
| 269 | |
| 270 | | Endpoint | Returns | |
| 271 | |---|---| |
| 272 | | `GET /repos/{owner}/{repo}/commits/{sha}/check-runs` | All check runs of a commit (from Actions and external reporters) as `{ total_count, check_runs }`. Filter with `check_name` and `status`. | |
| 273 | | `GET /repos/{owner}/{repo}/check-runs/{check_run_id}` | One check run | |
| 274 | | `GET /repos/{owner}/{repo}/check-runs/{check_run_id}/annotations` | Its annotations | |
| 275 | | `GET /repos/{owner}/{repo}/commits/{ref}/check-suites` | The commit's check suites as `{ total_count, check_suites }` | |
| 276 | | `GET /repos/{owner}/{repo}/check-suites/{check_suite_id}` | One check suite | |
| 277 | | `GET /repos/{owner}/{repo}/check-suites/{check_suite_id}/check-runs` | The check runs of a suite | |
| 278 | |
| 279 | Check suites are derived, not created: each Actions workflow run is one suite (its id is the run id), and each external reporter has one suite per commit (id `app-<login>-<sha>`). A suite is `completed` when all its check runs are, with conclusion `failure` if any of them failed, timed out, was cancelled or needs action, and `success` otherwise. You can't create or re-request suites, and commit statuses are not part of any suite. |
| 280 | |
| 281 | ## Required checks |
| 282 | |
| 283 | A branch protection rule's **Required status checks** (`requiredChecks` in the API) lists check names that must pass on a pull request's head commit before it can merge, and on a merge group's commit before the merge queue merges it. Both check runs and commit statuses count: a required check named `ci/external` is satisfied by a check run named `ci/external` or by a commit status with context `ci/external`. |
| 284 | |
| 285 | To set them in the web UI, open **Settings**, then **Branch protection**, and enter the names in **Required status checks**, one per line, exactly as they appear on pull requests. A name may contain commas, for example `CI / test (ubuntu, 22)`. See [Branch protection](/docs/repositories/branch-protection) for the whole rule. |
| 286 | |
| 287 | ### How a required check is decided |
| 288 | |
| 289 | For each required name, Diffinery looks at every check run and commit status with that name on the commit, then: |
| 290 | |
| 291 | 1. If the rule binds the name to a reporter in `requiredCheckSources`, only reports from that reporter count. |
| 292 | 2. Otherwise, if Diffinery Actions reported a check with that name, only the Actions check counts. Other reports with the same name are ignored. |
| 293 | 3. Reports from agents are ignored. |
| 294 | 4. Of the reports left: |
| 295 | - none: the check has not run, and the pull request is blocked with `required check '<name>' has not run`; |
| 296 | - any completed with a conclusion other than `success`, `neutral` or `skipped` (a status of `failure` or `error` counts as failed): blocked with `required check '<name>' <conclusion>`, for example `required check 'ci/external' failure`; |
| 297 | - any still pending or in progress: blocked with `required check '<name>' is <status>`, for example `required check 'ci / test' is in_progress`; |
| 298 | - otherwise the check passes. |
| 299 | |
| 300 | For commit statuses, only the newest status of each context from each reporter counts. One account's later status never replaces another account's. |
| 301 | |
| 302 | These blockers appear in the pull request's merge box, in `merge_blockers` from `GET /repos/{owner}/{repo}/pulls/{number}`, in `mergeability.blockers` from the MCP `get_pull_request` tool, and in the error of a refused merge. |
| 303 | |
| 304 | ### Bind a required check to its reporter |
| 305 | |
| 306 | Without a binding, any account with the write role could post a passing status with the same name as your external CI. Bind each external required check to the account that reports it with `requiredCheckSources`, a map from check name to the reporter's login (or `diffinery-actions` for Actions): |
| 307 | |
| 308 | ```sh |
| 309 | curl -sS -X PUT \ |
| 310 | -H "Authorization: Bearer dfy_..." \ |
| 311 | -H "Content-Type: application/json" \ |
| 312 | https://diffinery.com/api/v3/repos/acme/website/branches/main/protection \ |
| 313 | -d '{ |
| 314 | "requirePullRequest": true, |
| 315 | "requiredApprovals": 1, |
| 316 | "requiredChecks": ["ci / test", "ci/external"], |
| 317 | "requiredCheckSources": { "ci/external": "ci-bot", "ci / test": "diffinery-actions" } |
| 318 | }' |
| 319 | ``` |
| 320 | |
| 321 | - The request replaces the whole rule, so send every field you want to keep. It needs a token with the `admin` scope for a person with the admin role. Agents can't change branch protection. |
| 322 | - Each value must be a login (letters, digits, `.`, `_`, `-`, `[` and `]`, starting with a letter or digit, up to 100 characters) or `diffinery-actions`, and each name at most 255 characters. Otherwise the request fails with `invalid source for required check '<name>'`, or with `requiredCheckSources must map check names to a reporter login or diffinery-actions` when it isn't an object. |
| 323 | - Logins are compared without regard to case. |
| 324 | - Binding only affects names listed in `requiredChecks`. |
| 325 | - Binding a check to an agent's login means it can never pass, because agent reports are ignored. |
| 326 | |
| 327 | `requiredCheckSources` can only be set through the API, and the **Branch protection** settings page does not show it. Saving the rule from that page keeps the bindings of every check that is still listed in **Required status checks** (also when you rename the rule's pattern). A binding for a check you removed from the list is dropped; add the check back with the API, binding included. |
| 328 | |
| 329 | ### Required checks and the merge queue |
| 330 | |
| 331 | When the base branch uses a [merge queue](/docs/collaboration/merge-queue), required checks are evaluated on each merge group's commit, on the `gh-readonly-queue/<base>/...` branch. Actions workflows with `on: merge_group` (or `on: push` for those branches) report there automatically. An external CI system must also build the merge group branch and report its status or check run on the merge group commit; otherwise the entry waits until the queue's check timeout and leaves the queue. The queue page shows a required check that has not reported yet as `expected`. |
| 332 | |
| 333 | A finished commit status, or a check run sent with `POST .../check-runs` as completed, makes the queue look at the group again at once. Completing a check run with `PATCH` doesn't: the queue only notices it at its next event, at the latest when the check timeout ends. To finish a check that the queue waits on, post the final result with `POST` (the same account, commit and name update the existing check run). |
| 334 | |
| 335 | ### Required checks for deployments |
| 336 | |
| 337 | `POST /repos/{owner}/{repo}/deployments` uses the same rules (without source bindings) for `required_contexts`: without `required_contexts`, every check name on the commit that a non-agent reported must have passed; with a list, those names must have passed; `[]` skips the check. Deployments that a workflow job creates for its `environment:` skip the check. A failing check makes the request fail with `409` and `Conflict: Commit status checks failed for <ref>.` See [Environments](/docs/actions/environments). |
| 338 | |
| 339 | ## Agents and checks |
| 340 | |
| 341 | [Agents](/docs/agents/overview) with the write role can report check runs and commit statuses, and their reports appear on pull requests and commits like any other. But a report from an agent never satisfies a required check, and never fails one either: Diffinery ignores agent reports when it decides required checks, merge queue verdicts and deployment `required_contexts`. This stops an agent from approving its own changes by reporting a passing check. Run the checks you rely on in Diffinery Actions, or report them from a person's account bound with `requiredCheckSources`. |
| 342 | |
| 343 | ## Checks in the web UI |
| 344 | |
| 345 | ### On a pull request |
| 346 | |
| 347 | - The **Conversation** tab's merge box lists every check run and commit status on the head commit, with a summary: **All checks have passed**, **Some checks haven't completed yet**, or the number of failing checks (for example **2 failing checks**), and the number of checks. Each line has a status icon, the name, the check's title (for a commit status, its description) and how long it ran. Below are the approvals and the merge blockers, including any required check that is missing, pending or failed. |
| 348 | - The **Checks** tab shows **Checks for** the head commit's short SHA: each check with its reporter (`diffinery-actions`, the login of an external reporter, or `status` for a commit status), its state, the start of its summary, and its duration. When nothing has reported, it says **No checks have reported for the latest commit.** Under **Workflow runs** are the pull request's Actions runs. |
| 349 | |
| 350 | The check lists update live while the page is open. |
| 351 | |
| 352 | ### On a commit |
| 353 | |
| 354 | The commit page (`https://diffinery.com/acme/website/commit/<sha>`) lists the commit's check runs and statuses under its header, with their titles or descriptions. |
| 355 | |
| 356 | ### Where a check's name links |
| 357 | |
| 358 | | Check | Link | |
| 359 | |---|---| |
| 360 | | From Diffinery Actions | The job's page and log | |
| 361 | | Check run with `details_url`, or commit status with `target_url` | That URL, in a new tab (only `http` and `https` links) | |
| 362 | | Otherwise | No link | |
| 363 | |
| 364 | ## Differences from GitHub |
| 365 | |
| 366 | | GitHub | Diffinery | |
| 367 | |---|---| |
| 368 | | Actions check runs are named after the job | Named `<workflow> / <job>`. | |
| 369 | | Check runs are created by GitHub Apps; personal tokens can't create them | Any account with the write role can create check runs; `app` is the account's login. | |
| 370 | | Creating a check run with an existing name adds a new run | The same account, commit and name update the existing check run. | |
| 371 | | Check suites can be created and re-requested | Check suites are derived from check runs and read-only. | |
| 372 | | Status descriptions are limited to 140 characters | Up to 1,024 characters are kept. | |
| 373 | | `required_status_checks` with an app id restricts who can satisfy a check | `requiredCheckSources` binds a check name to a login or to `diffinery-actions`; when Actions reports a name, it alone decides that check even without a binding. | |
| 374 | | Statuses and check runs can be posted with the workflow's `GITHUB_TOKEN` | Statuses can (with `statuses: write`); check runs can't. | |
| 375 | | `check_run` and `check_suite` webhook events | Not sent. Commit statuses send `status` events. | |
| 376 | | Agents don't exist | Reports from agents never decide a required check. | |
| 377 | |
| 378 | ## Troubleshooting |
| 379 | |
| 380 | | Symptom or message | Cause and fix | |
| 381 | |---|---| |
| 382 | | `required check '<name>' has not run` | No report with exactly that name on the head commit, or only reports that don't count (from agents, from Actions-overridden reporters, or from someone other than the bound source). Compare the name with the **Checks** tab, including the `<workflow> / ` prefix for Actions jobs. | |
| 383 | | A passing external status is ignored | Actions also reports a check with that name, and Actions wins; or `requiredCheckSources` binds the name to a different login; or the reporter is an agent. Rename your context or fix the binding. | |
| 384 | | Bindings disappeared | The check was removed from **Required status checks** when the rule was saved from the **Branch protection** settings page, which drops the binding of a check that is no longer required. Set it again with the API. | |
| 385 | | `write access required` | The token's account lacks the write role, or a workflow's `GITHUB_TOKEN` has no write permission at all (for example `permissions: read-all`). | |
| 386 | | `Resource not accessible by integration (...)` | A workflow's `GITHUB_TOKEN` lacks the permission named in the message. Add `statuses: write` to the job's `permissions:` to post statuses. | |
| 387 | | `this account can't report check runs` | Check runs can't be created with a workflow's `GITHUB_TOKEN`. Post a commit status, or use a personal token. | |
| 388 | | `this token needs the write scope` | The token has only the `read` scope. Use a token with the `write` scope. | |
| 389 | | `only the app that created a check run can update it` | Update the check run with the same account (any of its tokens) that created it. | |
| 390 | | `check runs from Diffinery Actions can't be changed over the API` | Re-run the job instead. | |
| 391 | | `conclusion is required when status is completed` | Send a `conclusion` when completing a check run. | |
| 392 | | `this SHA and context has reached the maximum number of statuses` | You posted 1,000 statuses for one context on one commit. Post less often, for example only on state changes. | |
| 393 | | A merge queue entry waits for an external check | Your CI must report on the merge group commit (`gh-readonly-queue/...` branch), not only on the pull request's head commit. | |