9.9 kB · 155 lines
Markdown
at main

GitHub #

Status: draft. The adapter that exports a review to one GitHub pull request and fetches what happens there. It follows the contract in README.md ("integrations §N" below). The review model is in REVIEW-FORMAT.md ("format §N"). The code is in src/main/github/.

1. Destinations #

system github
host github.com, or a GitHub Enterprise Server host
locator <owner>/<name>/pull/<number>: owner and name of [A-Za-z0-9._-], a decimal number
external ids GraphQL node ids, of [A-Za-z0-9_=-], at most 128 characters
API GraphQL at https://api.github.com/graphql, or https://<host>/api/graphql; REST (for the pull request's files) at https://api.github.com, or https://<host>/api/v3
viewer viewer { login id }: viewer is the login, viewer_id the node id
declares export, fetch, describe, checks (integrations §3)
URL https://<host>/<owner>/<name>/pull/<number>, with anything after it (/files, a fragment) ignored; attaches when opened (integrations §3.1). The canonical URL has no suffix
refs head refs/pull/<number>/head → refs/remotes/<remote>/pull/<number> (forks' pull requests have it too); base refs/heads/<baseRefName> → refs/remotes/<remote>/<baseRefName>
names GitHub · pull request · pr · #<number>

For exporting and for Attach, the app suggests pull requests from the clone's GitHub remotes, one whose head is the subject's head first. The user picks one (integrations §3, §3.1).

2. Export mapping #

A review becomes one pull request review.

Local GitHub
change review, approve / request-changes / comment a review with event APPROVE / REQUEST_CHANGES / COMMENT. On the viewer's own pull request, approve and request changes post as COMMENT (shape): GitHub refuses them there.
audit a COMMENT review on the pull request the user picks (shape: no verdict)
thread on lines in the diff a review thread on those lines, RIGHT for head, LEFT for base
thread on lines outside the diff a file thread, with the line range in the text (shape)
thread on a file the pull request does not change, at a commit not in the pull request, or without an anchor part of the review body (shape, info)
kind and severity a bold prefix, like "Issue · major" (info)
suggestion on head lines in the diff a suggestion block; elsewhere a plain code block (shape)
symbol a code span, "in orderPlan" (info)
evidence, when opted in a collapsed block: command, exit code, boundary, and the last 40 lines (at most 6000 characters) of the log, in a fence
generated_by a line "Generated · model" at the top of the body
coverage not sent: GitHub's viewed marks are private to each user
entries, private threads not sent (integrations §1)

"The diff" is the pull request's files at planning time (REST /repos/<owner>/<name>/pulls/<number>/files), whose patch hunks give the lines GitHub accepts comments on. A file with no patch (binary, or too large) takes only file threads.

Bodies are capped at 60000 characters, marker included (GitHub refuses bodies over 65536). The cap is applied before rendering, never by cutting the rendered text: quoted evidence logs are shortened first, then the user's text, each at a line boundary, so that every fence and block stays closed. The plan shows each body as it will be sent, with a shape row for each cut.

The marker is an HTML comment on the last line of every body the adapter sends: <!-- code-browser:review <review-id> --> for the review, and <!-- code-browser:thread <review-id>/<thread-id> --> for a thread's first comment.

3. Sending #

An export takes these steps (integrations §6.4). Each is recorded in progress with the node id it created.

  1. addPullRequestReview with the pull request's id, commitOID the plan's head, and the review body: a pending review, which only the viewer can see.
  2. addPullRequestReviewThread for each thread placed as its own item, on the pending review: path, side, line and startLine for lines, or subjectType: FILE.
  3. submitPullRequestReview with the event.
Step Done when
1 a review by the viewer exists with the marker (state PENDING or later)
2 a thread of that review exists whose first comment has the marker
3 the review's state is not PENDING

The head check of integrations §6.4 compares the plan's head with the pull request's headRefOid. GitHub allows one pending review per user and pull request. A pending review of the viewer that carries this review's marker is step 1's result, found by the check of integrations §6.5. If the viewer has any other pending review there, the export fails with retryable = 0, saying so: it may be the user's own work, and is left alone.

Undoing (integrations §6.4) is deletePullRequestReview on the pending review of step 1, which deletes its threads with it. A submitted review cannot be undone.

4. Fetching #

A fetch reads (integrations §7):

GitHub Remote entry
the pull request the destination's url, title, head (headRefOid), and its description: body, author and author_id from author, base_ref (baseRefName), head_ref (headRefName), created (createdAt), updated (lastEditedAt, else createdAt)
its state state: merged when merged, closed for CLOSED, draft for OPEN with isDraft, else open; remote_state is state, with draft added when isDraft
each review (reviews), the viewer's pending review included kind = 'review', remote_state its state, verdict approve for APPROVED, request-changes for CHANGES_REQUESTED, comment for COMMENTED, none for PENDING or DISMISSED
each review thread (reviewThreads) kind = 'thread', parent_external_id its first comment's review, resolved from isResolved, resolved_by, outdated from isOutdated, the anchor from path, diffSide, startLine and line, anchor_commit from its first comment's commit (for a LEFT thread too: the head it was made on, while its lines are of the base side). Its body is its first comment's text; the first comment is not a row of its own
each later comment of a thread kind = 'comment', parent_external_id the thread
each conversation comment (comments, which belong to no review or thread) kind = 'comment', parent_external_id NULL
  • Pages use GraphQL cursors, 50 threads and 100 comments per page, following every hasNextPage including comments within a thread.
  • author is the login, author_id the account's node id; a deleted account (author null) is ghost with no id. edited_by is editor's id.
  • Times are converted to the store's form (format §4.3): 2026-09-23T14:02:11Z becomes 2026-09-23T14:02:11.000Z.
  • URLs are kept only on the destination's host (integrations §5.3). The jwt parameter is removed from private attachment links (integrations §8).
  • The viewer's own pending review is included, since the check of integrations §6.5 depends on it.
  • body is the pull request's markdown (body), not GitHub's rendered HTML; the app renders it as it renders every fetched body.

4.1 Checks #

Checks come from the head commit's statusCheckRollup.contexts (commits(last: 1), whose oid MUST equal headRefOid, else the fetch stops with fetch_error and is tried again next time), 100 per page, following hasNextPage up to the cap of integrations §5.6.

GitHub Check
a CheckRun name from its checkSuite.workflowRun.workflow.name and name joined by / when it has a workflow, else name; url its url (the run's page on GitHub, never detailsUrl, which may be another host); started and finished from startedAt and completedAt
its status and conclusion QUEUED, WAITING, PENDING, REQUESTED → pending; IN_PROGRESS → running; COMPLETED with SUCCESS → passed, FAILURE, TIMED_OUT, STARTUP_FAILURE, ACTION_REQUIRED → failed, SKIPPED → skipped, CANCELLED, STALE → cancelled, NEUTRAL → neutral; remote_state is status, and conclusion after a space when there is one
a StatusContext (commit status) name from context; state PENDING, EXPECTED → pending, SUCCESS → passed, FAILURE, ERROR → failed; url from targetUrl only when it passes the URL rule (it is usually another host, and is then NULL); started from createdAt

pos is the order GitHub returns them in. A check whose type is neither is skipped and counted in checks_omitted (integrations §5.6).

5. Credentials #

The token comes from the github.auth setting: gh, which runs gh auth token --hostname <host>, or keychain, a token the user saved in the OS keychain for that host. Both follow integrations §8. gh is looked up on PATH and in the usual install directories (apps started from the Dock get a short PATH), never in a repository.

6. Failures and rate limits #

Response Kind (integrations §6.6)
network error before a response, TLS failure not sent
timeout or reset on a mutation, a 5xx maybe sent
a 403 or 429 with retry-after, or x-ratelimit-remaining: 0 (pause until x-ratelimit-reset), a GraphQL RATE_LIMITED error not sent; pauses the host
a GraphQL error of type NOT_FOUND or FORBIDDEN, or a 401, 403 or 404 otherwise permanent
a GraphQL error on a mutation, such as a line outside the diff permanent

A GraphQL response can carry errors with a 200 status. The adapter treats any errors on a mutation as a failure of that step, and on a query as a failed fetch.