mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-16 10:53:25 +00:00
209 lines
8.4 KiB
Markdown
209 lines
8.4 KiB
Markdown
# GitHub API Quirks (Verified)
|
||
|
||
API behaviors that matter for the Copilot review loop. All verified
|
||
against the current API surface — read this before reaching for an
|
||
alternative API or modifying the bundled scripts.
|
||
|
||
## GraphQL trigger — `requestReviewsByLogin` is the supported path
|
||
|
||
```graphql
|
||
mutation($p: ID!) {
|
||
requestReviewsByLogin(input: {
|
||
pullRequestId: $p,
|
||
botLogins: ["copilot-pull-request-reviewer"]
|
||
}) {
|
||
pullRequest { number }
|
||
}
|
||
}
|
||
```
|
||
|
||
Verified empirically against personal repos without Copilot Pro AND
|
||
org repos with Copilot Enterprise. Works for both initial-add and
|
||
re-request (no special re-request mutation).
|
||
|
||
Three GraphQL traps:
|
||
|
||
1. Mutation is **`requestReviewsByLogin`**, NOT `requestReviews`.
|
||
`RequestReviewsInput` (used by `requestReviews`) does not expose a
|
||
`botLogins` field, so it can't request a bot reviewer at all —
|
||
`botLogins` is the central field on `requestReviewsByLogin`.
|
||
2. Field is **`botLogins`**, NOT `userLogins`. The latter returns
|
||
`Could not resolve user with login 'Copilot'`.
|
||
3. Slug is **`copilot-pull-request-reviewer`** (the App slug). The
|
||
display login `Copilot` returns `Could not resolve bot with slug
|
||
'Copilot'`.
|
||
|
||
Verify success via a new `copilot_work_started` event on the issue's
|
||
events feed — `GET /repos/{o}/{r}/issues/{n}/events` (see SKILL.md
|
||
Gotchas "HTTP 200 / exit 0 is NOT proof"). Empirically this event
|
||
type IS exposed on the `/events` endpoint (verified across 20+
|
||
trigger rounds on PR 236); it is not timeline-only.
|
||
`01-request-review.ps1` enforces this by comparing the event `id`
|
||
(monotonic) before and after the trigger.
|
||
|
||
### Other trigger paths — DO NOT USE
|
||
|
||
- **`requestReviews` with `botLogins`** → input type rejects the
|
||
field. Don't try variants.
|
||
- **REST `POST /pulls/<n>/requested_reviewers` with
|
||
`reviewers[]=Copilot`** → can return HTTP 201 while silently
|
||
dropping the bot. Not used by the script.
|
||
- **`gh pr edit --add-reviewer Copilot`** → returns `'Copilot' not
|
||
found` on current `gh`. Not used by the script.
|
||
|
||
## GraphQL `latestReviews` — stale cache, do NOT use
|
||
|
||
```graphql
|
||
# DO NOT — stale projection:
|
||
pullRequest(number:$pr){ latestReviews(first:50){ nodes{...} } }
|
||
|
||
# USE INSTEAD — always current:
|
||
pullRequest(number:$pr){ reviews(last:100){ nodes{...} } }
|
||
```
|
||
|
||
`latestReviews` is a "latest per user" projection with stale-cache
|
||
behavior: a fresh Copilot review can be absent for several minutes
|
||
after submission, while `reviews(last:100)` reflects it immediately.
|
||
Using `latestReviews` for in-flight or convergence checks causes the
|
||
script to operate on an obsolete commit OID — either falsely
|
||
declaring convergence or timing out for a review that already
|
||
exists.
|
||
|
||
`02-check-review-status.ps1` uses `reviews(last:100)` filtered
|
||
client-side to the Copilot reviewer login. It also emits a stderr
|
||
warning when the result is exactly 100 reviews, so the caller knows
|
||
the boundary was hit and the latest Copilot review COULD be older
|
||
than the window — practically only possible if 100+ non-Copilot
|
||
reviews landed after the last Copilot review, which doesn't happen
|
||
in normal use. If you ever see the warning and the loop misbehaves,
|
||
fetch the full review list manually:
|
||
|
||
```bash
|
||
gh pr view <n> --json reviews --jq '.reviews[] | select(.author.login | test("copilot-pull-request-reviewer"))'
|
||
```
|
||
|
||
### Tie-break for multiple Copilot reviews
|
||
|
||
When more than one Copilot review shares the same `submittedAt`
|
||
(rare server-side clock collision under burst re-triggers), the
|
||
script first prefers the review whose `commit.oid == HEAD`, then
|
||
falls back to a stable sort. The intent is "the review that
|
||
matches the current code is the one the agent should reply to" —
|
||
preventing a stale-OID review from winning the tie and falsely
|
||
flipping `ReviewAtHead` to false.
|
||
|
||
## Reply + resolve mutations — both work
|
||
|
||
```graphql
|
||
mutation($tid: ID!, $body: String!) {
|
||
addPullRequestReviewThreadReply(input: {
|
||
pullRequestReviewThreadId: $tid,
|
||
body: $body
|
||
}) { comment { id } }
|
||
}
|
||
|
||
mutation($tid: ID!) {
|
||
resolveReviewThread(input: { threadId: $tid }) {
|
||
thread { isResolved }
|
||
}
|
||
}
|
||
```
|
||
|
||
## `isOutdated` ≠ `isResolved` — current unresolved state is truth
|
||
|
||
A thread can be `isOutdated: true` (Copilot's comment points at lines
|
||
that have since changed) while still `isResolved: false`. These
|
||
threads:
|
||
|
||
- Still need reply + resolve in the per-round loop. A thread can
|
||
become outdated mid-round when your own fix shifts the cited
|
||
lines. Filtering on `!isOutdated` would silently drop those
|
||
threads, leaving the PR's open-conversations list non-empty even
|
||
after the underlying code is fixed.
|
||
- `03-list-open-threads.ps1` therefore lists every unresolved
|
||
thread with no `isOutdated` filter.
|
||
- `10-cleanup-outdated.ps1` is a safety net only — for the rare
|
||
case where a thread becomes outdated AFTER your last per-round
|
||
fetch.
|
||
|
||
## Review latency — don't poll faster than ~3 min
|
||
|
||
Copilot reviews typically post 3–6 minutes after the request,
|
||
occasionally up to ~10 minutes. There is no progress signal;
|
||
polling more often than every ~3 min wastes API budget without
|
||
making the review arrive sooner.
|
||
|
||
## `gh api graphql -F` coerces strings — use `-f` for `String!`
|
||
|
||
The `gh` CLI distinguishes its two flag forms:
|
||
|
||
- `-F key=value` — type inference. Values parsing as int, bool, or
|
||
null are sent as that JSON literal.
|
||
- `-f key=value` — always sends as raw string.
|
||
|
||
For any GraphQL variable declared `String!` (e.g. `owner`, `repo`,
|
||
`body`, `tid`, `after`), use **`-f`** at call sites. A reply body that
|
||
happens to be `"true"`, `"null"`, or all digits would otherwise be
|
||
coerced and the call fails with a type error. Keep `-F` only for
|
||
genuinely numeric or boolean variables (e.g. `pr: Int!`).
|
||
|
||
> Note: the shared `Invoke-Gh` wrapper may internally rewrite
|
||
> `-f field=<body>` into `-F field=@<tempfile>` when the body contains
|
||
> embedded `"` (Windows PowerShell 5.1 native-arg quoting bug — see
|
||
> below). Even via `@file`, `-F` still applies type inference to the
|
||
> file content (gh's documented behaviour) — this rewrite is safe
|
||
> only because the rewrite trigger ("body contains `"`") guarantees
|
||
> the content is a string that no JSON literal (`123`, `true`,
|
||
> `null`, etc.) would match. Treat this `-F ...=@file` usage as an
|
||
> internal transport detail of the wrapper, not as permission to
|
||
> use `-F ...=@file` for arbitrary strings at call sites.
|
||
|
||
```powershell
|
||
# Wrong — body could be coerced AND, under Windows PowerShell 5.1,
|
||
# any embedded `"` in $Body will be mis-split by the native-arg
|
||
# passer (gh sees a truncated body or a "received N args" error).
|
||
gh api graphql -f query=$q -F body=$Body
|
||
|
||
# Right — go through Invoke-Gh / Invoke-GhGraphQL. The shared helper
|
||
# auto-rewrites `-f field=<body>` and `-F field=<body>` pairs whose
|
||
# body contains `"` to `-F field=@<tempfile>` so the value is read
|
||
# from disk and never appears on the command line. This works
|
||
# identically on Windows PowerShell 5.1 and PowerShell 7+.
|
||
Invoke-GhGraphQL -GhArgs @('-f',"query=$q",'-f',"body=$Body") -Context 'reply body'
|
||
```
|
||
|
||
Calling `gh` directly (e.g. via `& gh ...` or raw `gh api graphql`)
|
||
bypasses the cross-version tempfile rewrite — if your value contains
|
||
`"` you'll re-introduce the PowerShell-5.1-only splitting bug. Always
|
||
funnel `gh` calls through `Invoke-Gh` / `Invoke-GhGraphQL`.
|
||
|
||
## Native `gh` exit codes bypass `$ErrorActionPreference`
|
||
|
||
`gh` is a native executable, not a PowerShell cmdlet, so a non-zero
|
||
exit does **not** throw even when `$ErrorActionPreference = 'Stop'`.
|
||
Without an explicit check the script will print misleading success
|
||
messages after a failed API call, and the loop will falsely declare
|
||
convergence on auth issues, rate limits, or transient 5xx.
|
||
|
||
Additional trap: `gh api graphql` can exit 0 for an HTTP 200 whose
|
||
JSON body carries a top-level `errors` array. Treat that as a failed
|
||
call too.
|
||
|
||
The shared helpers in [scripts/_lib.ps1](../scripts/_lib.ps1)
|
||
(`Invoke-Gh` and `Invoke-GhGraphQL`) run `gh` via `& gh @args`
|
||
with stderr redirected to a temp file (`2>$errFile`), then read
|
||
`$LASTEXITCODE` and return `{ExitCode, Stdout, Stderr}`.
|
||
`Invoke-GhGraphQL` additionally parses the GraphQL `errors` array
|
||
on the response body and throws on either failure mode. All
|
||
bundled scripts dot-source `_lib.ps1` and use these wrappers — do
|
||
the same in any new script.
|
||
|
||
## `git stash push` argument order
|
||
|
||
```bash
|
||
git stash push -m "local-build" -- src/path/a src/path/b # correct
|
||
git stash push -- src/path/a src/path/b -m "local-build" # SILENTLY drops -m
|
||
```
|
||
|
||
The `-m` MUST come before the `--` path separator.
|