* feat(scm): GitHub provider adapter — Observe(prURL) → PRObservation
A fresh GitHub SCM provider adapter under
backend/internal/adapters/scm/github/ exposing one method:
(*Provider).Observe(ctx, prURL) (ports.PRObservation, error)
It performs a REST GET on /repos/{o}/{r}/pulls/{n} for the authoritative
draft/merged/closed/head-SHA, one GraphQL query for the reviewDecision +
mergeStateStatus + statusCheckRollup + unresolved review threads, and
(only for failure-class CheckRuns) a REST GET on
/actions/jobs/{job_id}/logs to splice the last 20 lines of the failed
job into the observation.
The package is the observation primitive; the polling loop, cadence
selection, daemon wiring, persistence and webhook receiver are all
intentionally out of scope (separate PRs / lanes).
Closes#27 — this supersedes PR #28's attempt, which targeted types
(domain.SCMProvider / SCMSnapshot / ports.SCMObserveRequest) that the
PR #62 simplification refactor has since removed. The GraphQL queries
and mergeability composition logic are credited to @whoisasx from
PR #28's provider.go; the package was re-implemented against the
current ports.PRObservation seam (post-#62) rather than rebased.
Bot-author detection uses ONLY GitHub's typed signal (__typename
"Bot" / User.Type "Bot"). The strings.Contains(login, "bot") fallback
from PR #28 was intentionally dropped — aa-18's review flagged it as
a false-positive magnet for logins like "robothon" / "lambot123".
46 table-driven tests against httptest.NewServer cover happy path,
draft, merged, closed (not merged), CI passing/failing/pending,
StatusContext legacy, log-tail extraction (and the best-effort
log-fetch failure case), mergeability mergeable/conflicting/blocked
(including ci-failing → blocked even when GitHub still says CLEAN —
the load-bearing aa-18 contract)/unstable/unknown, review
approved/changes-requested/required/none, bot-author filtering
(including the robothon false-positive guard), unresolved-only
threads, all-bots → empty Comments, ETag-304 cache hit, primary +
secondary rate-limit (with errors.As → *RateLimitError), 401 →
ErrAuthFailed, malformed JSON → Fetched:false, network error →
Fetched:false, Authorization Bearer header injection,
StaticTokenSource blank/whitespace rejection, GHTokenSource memoize
+ invalidate.
Verification:
- go build ./... clean
- go vet ./... clean
- gofmt -l backend/internal/adapters/scm/ clean
- golangci-lint run ./... (v2.12, repo .golangci.yml) 0 issues
- go test -race ./internal/adapters/scm/github/... 46/46 PASS
References:
- aa-18 review of PR #28: ~/.ao/agent-reports/aa-18.md
- aa-26 tracker adapter (sibling Go-adapter pattern): #36 / agent-reports/aa-26.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(scm): address greptile review on #69
Four fixes from the greptile review of PR #69:
1. CI rollup pagination (P1) — when GraphQL reports
pageInfo.hasNextPage=true for the statusCheckRollup contexts, a
visible "all passing" set could be hiding a failing context on the
next page. ciSummaryFromGraphQL now degrades Passing / Pending /
Unknown to CIUnknown in that case; a known CIFailing on the visible
page is still safe and is NOT degraded. Also bumped the per-page
limit from 50 to 100 (GraphQL's documented max for the contexts
connection). Two new tests pin both branches.
2. Empty GraphQL inline fragment (P2) — dropped
`... on User { }` from the reviewThreads author selection. The
empty selection set was technically invalid GraphQL and a future
API tightening could reject the query. __typename already tells us
whether the actor is a Bot, so the fragment carried no information.
3. rest.MergeStateStatus dead-code (P2) — the field decoded from the
non-existent REST `merge_state_status` was always empty, making the
firstNonEmpty fallback dead code. Removed the field and switched
the tiebreaker to rest.MergeableState (the actual REST field, upper-
cased so the same switch covers both GraphQL and REST shapes).
4. Wrong Accept header on /actions/jobs/{id}/logs (P2) — GitHub's
REST API validates the Accept header before issuing the 302 to the
log blob; sending text/plain risks a 406. Switched to the canonical
application/vnd.github+json; the redirected blob serves text/plain
regardless.
Verification:
- go build ./... clean
- go vet ./... clean
- golangci-lint run ./... 0 issues
- go test -race ./internal/adapters/scm/github/... 48 / 48 PASS
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>