Skip to content

feat(scm): GitHub provider adapter — Observe(prURL) → PRObservation#69

Merged
harshitsinghbhandari merged 2 commits into
mainfrom
feat/scm-github-provider
Jun 1, 2026
Merged

feat(scm): GitHub provider adapter — Observe(prURL) → PRObservation#69
harshitsinghbhandari merged 2 commits into
mainfrom
feat/scm-github-provider

Conversation

@harshitsinghbhandari
Copy link
Copy Markdown
Collaborator

Summary

Adds backend/internal/adapters/scm/github/, a new package exposing one
method:

(*Provider).Observe(ctx, prURL) (ports.PRObservation, error)

It is the observation primitive the PR Manager / LCM pipeline already
consumes (ports.PRObservation). The poller loop, cadence selection,
daemon wiring, persistence, webhook receiver and Linear/GitLab
providers are intentionally out of scope — separate PRs / lanes.

Closes #27.

Why a fresh package vs a rebase of PR #28

PR #28 (head feat/27, @whoisasx) tried to land the SCM flow against
the old domain.SCMProvider / SCMSnapshot / ports.SCMObserveRequest
contract. PR #62 ("simplify session lifecycle", merged 2026-06-01)
refactored that whole surface away — git grep for any of those types
on main returns zero hits. A rebase would have meant rewriting most of
the package against the new ports.PRObservation seam anyway, so this
branch starts fresh against the current contract.

What is carried forward verbatim from PR #28 (credited to
@whoisasx):

  • The GraphQL query text (reviewDecision + mergeStateStatus +
    statusCheckRollup + reviewThreads in one round trip)
  • The mergeability composition rules
  • The ciFailureLogTailLines = 20 constant and tail-extraction shape

What is deliberately not carried forward:

  • The strings.Contains(login, "bot") fallback in bot-author
    detection — aa-18's review flagged it as a false-positive magnet on
    logins like robothon / lambot123. This package uses only
    GitHub's typed signal (__typename == \"Bot\" /
    User.Type == \"Bot\"). See provider.go:isBotAuthor.

aa-18's review notes:
[~/.ao/agent-reports/aa-18.md (local)] — the three load-bearing
contracts (draft encoding, pending-comments + bot routing,
mergeability composition) are exercised by tests in this package.

What's in the package

File Contents
auth.go TokenSource interface + EnvTokenSource + StaticTokenSource + GHTokenSource (with injectable GH func(ctx) (string, error) so tests never shell out).
client.go HTTP wrapper: ETag cache (FIFO-bounded at 512 entries, GET-only), sentinel errors (ErrNotFound / ErrAuthFailed / ErrRateLimited), RateLimitError{ResetAt, RetryAfter} matchable via errors.As. Mirrors the tracker adapter's pattern.
provider.go Provider.Observe(prURL) → PRObservation. Failed fetch → Fetched:false (no fabricated state).
provider_test.go 46 table-driven tests against httptest.NewServer.
doc.go Package doc with full state-mapping table and out-of-scope notes.

Test coverage (46 tests / 46 pass)

Happy path · draft · merged · closed (not merged) · CI
passing/failing/pending · StatusContext (legacy) failure · log-tail
extraction (tail to 20 lines + best-effort fetch failure stamps a
synthetic sentinel without flipping Fetched) · mergeability
mergeable / conflicting (DIRTY) / blocked (BLOCKED) / unstable (UNSTABLE) /
unknown / CI-failing → MergeBlocked even when GitHub still says
CLEAN (the load-bearing aa-18 contract)
· review
approved / changes_requested / review_required / null / unrecognized ·
bot-author filtering (typename only, robothon false-positive guard,
resolved threads skipped) · all-bot threads → empty Comments + Fetched:true ·
ETag 304 cache hit (sends If-None-Match, replays body) · primary 403
rate-limit (Remaining=0 + Reset) · secondary 403 rate-limit (Retry-After) ·
401 → ErrAuthFailed · malformed JSON → Fetched:false ·
network error → Fetched:false · Authorization Bearer header injection ·
StaticTokenSource blank/whitespace rejection · GHTokenSource memoize +
invalidate.

Verification

  • go build ./...
  • go vet ./...
  • gofmt -l backend/internal/adapters/scm/
  • golangci-lint run ./... (v2.12, repo .golangci.yml strong-ruleset) — 0 issues
  • go test -race ./internal/adapters/scm/github/... — 46 / 46 PASS

Self-review

Reviewed via superpowers:code-reviewer before opening. Strong-recommend
items addressed inline:

  1. Mergeability doc/table aligned with implementation (REST signal is
    a tie-breaker, not co-primary).
  2. ETag cache bounded at 512 entries with FIFO eviction (was unbounded).
  3. ETag cache restricted to GET (mutating methods would mis-replay).
  4. Added the missing ci-failing → MergeBlocked test row.
  5. Added the all-bot-threads → nil-Comments test.
  6. Host check tightened to *.github.com / *.ghe.io (was api.*).

Out of scope (intentionally — separate PRs / lanes)

Test plan

  • go test -race ./internal/adapters/scm/github/... locally — 46 PASS
  • golangci-lint run ./... locally — 0 issues
  • CI green (push then await)

🤖 Generated with Claude Code

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>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

Adds a new backend/internal/adapters/scm/github/ package that implements (*Provider).Observe(ctx, prURL) → ports.PRObservation — the observation primitive the PR Manager and LCM pipeline consume. The package replaces the abandoned PR #28 implementation, rebasing against the current ports.PRObservation contract rather than the obsolete domain.SCMProvider surface.

  • auth.go: Three TokenSource implementations (static, env, gh-shell-out) with TTL-memoization and invalidation on auth failures.
  • client.go: HTTP wrapper with FIFO-bounded ETag/304 cache (512 entries), sentinel errors (ErrNotFound / ErrAuthFailed / ErrRateLimited), and the fetchPlainText path correctly sending application/vnd.github+json so the /actions/jobs/{id}/logs redirect isn't rejected.
  • provider.go: REST + GraphQL observation with CI-pagination degradation guard (hasNextPage → CIUnknown), the aa-18 contract (CI-failing → MergeBlocked even when mergeStateStatus is CLEAN), and bot-author filtering via __typename only.

Confidence Score: 5/5

Safe to merge — all required network calls fail safely to Fetched=false, the CI-pagination guard and aa-18 mergeability contract are both exercised by tests, and the previously flagged issues (pagination truncation, empty GraphQL fragment, REST merge_state_status ghost field, fetchPlainText Accept header) are all resolved in this revision.

The only new findings are two inert dead-code/cleanup items: an unreachable author["type"] branch in isBotAuthor (the GraphQL query never requests that field) and a url selection in the comment fragment that has nowhere to land in PRCommentObservation. Neither affects runtime behavior. The core observation path, error classification, ETag caching, and bot-filtering logic are all correct and well-tested.

backend/internal/adapters/scm/github/provider.go — the dead author["type"] branch in isBotAuthor and the unused url comment field are cosmetic but worth tidying before the poller builds on top of this package.

Important Files Changed

Filename Overview
backend/internal/adapters/scm/github/auth.go Introduces TokenSource interface with three implementations (StaticTokenSource, EnvTokenSource, GHTokenSource); memoization and cache invalidation are correctly guarded by sync.Mutex.
backend/internal/adapters/scm/github/client.go HTTP wrapper with ETag/304 cache (FIFO-bounded at 512), sentinel error classification, and bearer-token injection; Accept header for fetchPlainText correctly uses vnd.github+json.
backend/internal/adapters/scm/github/provider.go Core observation logic is sound; two minor issues: isBotAuthor checks author["type"] which is never populated by the GraphQL query (dead code), and the comment query fetches url which PRCommentObservation has no field for.
backend/internal/adapters/scm/github/provider_test.go 46 table-driven tests covering happy path, all CI/review/mergeability states, pagination degradation, log-tail enrichment, ETag caching, and error classification; loop variable capture in install() is safe on Go 1.22+.
backend/internal/adapters/scm/github/doc.go Accurate package-level documentation with full state-mapping table; doc.go references "User.Type == 'Bot'" which should be removed once the dead branch in isBotAuthor is dropped.

Sequence Diagram

sequenceDiagram
    participant Caller as PR Manager / LCM
    participant P as Provider.Observe
    participant R as GitHub REST API
    participant G as GitHub GraphQL API
    participant L as GitHub Actions Logs

    Caller->>P: Observe(ctx, prURL)
    P->>R: "GET /repos/{o}/{r}/pulls/{n}"
    alt 304 Not Modified
        R-->>P: 304 + replay cached body
    else 200 OK
        R-->>P: restPull (draft/merged/state/head SHA)
    end
    P->>G: POST /graphql
    G-->>P: PR graph data
    loop Each failing CheckRun
        P->>L: "GET /repos/{o}/{r}/actions/jobs/{id}/logs"
        L-->>P: plain-text log (tail 20 lines)
    end
    P-->>Caller: "PRObservation{Fetched:true}"
Loading

Reviews (2): Last reviewed commit: "fix(scm): address greptile review on #69" | Re-trigger Greptile

Comment thread backend/internal/adapters/scm/github/provider.go
Comment thread backend/internal/adapters/scm/github/provider.go
Comment thread backend/internal/adapters/scm/github/provider.go Outdated
Comment thread backend/internal/adapters/scm/github/client.go Outdated
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>
@harshitsinghbhandari harshitsinghbhandari merged commit f9b08aa into main Jun 1, 2026
7 checks passed
yyovil added a commit that referenced this pull request Jun 2, 2026
Re-integrate the agent-adapter layer onto main's rewritten session lane.

main (#62/#66/#67/#68/#69) moved the session manager into session_manager
plus a service layer, reworked the domain (Activity, IsTerminated), and
removed the tmux runtime. This merge:

- Keeps the rich, hooks-capable ports.Agent (in internal/ports) as the
  canonical agent contract; session_manager.Manager now resolves a real
  adapter per session via ports.AgentResolver (from cfg.Harness on Spawn,
  the stored harness on Restore) and builds RuntimeConfig.Argv.
- Drops the tmux runtime; standardizes on zellij.
- Re-adds the daemon's per-session agent resolver wiring
  (buildAgentResolver over the claude-code + codex registry, AO_AGENT
  default), ready to plug into the httpd session-route slot.

go build, go vet, and go test -race are green; the zellij/tmux real
integration tests are environment-gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate GitHub SCM end-to-end in Go rewrite

1 participant