chore: publish from main

This commit is contained in:
github-actions[bot]
2026-07-01 01:10:23 +00:00
parent fa007c62f7
commit 5075e262b5
25 changed files with 3124 additions and 0 deletions
@@ -0,0 +1,208 @@
# 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 36 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.