126 lines
5.9 KiB
Go
126 lines
5.9 KiB
Go
// Package github observes GitHub pull requests for AO's SCM integrations.
|
|
//
|
|
// The compatibility exported surface is:
|
|
//
|
|
// (*Provider).Observe(ctx, prURL) (ports.PRObservation, error)
|
|
//
|
|
// It performs a REST GET on /repos/{o}/{r}/pulls/{n} for the authoritative
|
|
// state booleans (draft / merged / closed / head SHA), one GraphQL query
|
|
// for the reviewDecision + mergeStateStatus + statusCheckRollup + review
|
|
// threads, and (only for CheckRuns that concluded failure-class) a REST
|
|
// GET on /repos/{o}/{r}/actions/jobs/{job_id}/logs to splice the last 20
|
|
// lines of the failed job into the observation.
|
|
//
|
|
// The provider-neutral SCM observer uses the same Provider for lower-level
|
|
// primitives: repo/commit ETag guards, branch-to-PR detection, GraphQL PR
|
|
// batching, failed-job log tails, and review-thread pagination. The polling
|
|
// loop itself is intentionally not in this package; it lives in
|
|
// internal/observe/scm.
|
|
//
|
|
// # State mapping
|
|
//
|
|
// Each ports.PRObservation field is derived as follows:
|
|
//
|
|
// - Fetched: false if any required REST/GraphQL call fails; true
|
|
// only once all the calls have succeeded. Log-tail
|
|
// fetch failures are best-effort: the LogTail is
|
|
// stamped with a "<log fetch failed: ...>" sentinel
|
|
// and the observation still surfaces as Fetched=true.
|
|
//
|
|
// - URL, Number: the URL the caller passed (validated) plus the
|
|
// number from REST pulls/{n}.
|
|
//
|
|
// - Draft: REST pulls/{n}.draft.
|
|
//
|
|
// - Merged: REST pulls/{n}.merged OR a non-null merged_at.
|
|
//
|
|
// - Closed: REST pulls/{n}.state == "closed" AND NOT Merged.
|
|
// (Closed and Merged are mutually exclusive.)
|
|
//
|
|
// - CI: derived from the latest commit's statusCheckRollup contexts
|
|
// (CheckRun + StatusContext). Failed if ANY context concluded in a
|
|
// failure class (failure / cancelled / timed_out / action_required /
|
|
// error). Pending if any context is still running / queued.
|
|
// Passing if all non-skipped contexts concluded SUCCESS / NEUTRAL.
|
|
// Unknown otherwise. Empty rollup falls back to the rollup-level
|
|
// "state" field.
|
|
//
|
|
// - Review: from GraphQL pullRequest.reviewDecision:
|
|
//
|
|
// | GraphQL | domain.ReviewDecision |
|
|
// |------------------------|-------------------------|
|
|
// | APPROVED | ReviewApproved |
|
|
// | CHANGES_REQUESTED | ReviewChangesRequest |
|
|
// | REVIEW_REQUIRED | ReviewRequired |
|
|
// | null / unknown | ReviewNone |
|
|
//
|
|
// - Mergeability: composed in priority order; the first rule that
|
|
// matches wins. The primary signal is the GraphQL pullRequest
|
|
// payload; the REST pulls/{n} response is consulted only as a
|
|
// tiebreaker when GraphQL is empty or has not yet been computed.
|
|
// Rules:
|
|
// (1) mergeStateStatus == DIRTY -> MergeConflicting
|
|
// (2) mergeStateStatus == BLOCKED -> MergeBlocked
|
|
// (3) mergeStateStatus == UNSTABLE -> MergeUnstable
|
|
// (4) GraphQL mergeable == CONFLICTING -> MergeConflicting
|
|
// (5) reviewDecision == changes_requested -> MergeBlocked
|
|
// (6) CI == failing -> MergeBlocked
|
|
// (7) REST mergeable_state pin — a TIE-BREAKER, not a terminal
|
|
// step: "dirty"->MergeConflicting, "blocked"->MergeBlocked,
|
|
// "unstable"->MergeUnstable, "clean"->MergeMergeable ONLY if
|
|
// GraphQL says MERGEABLE or REST mergeable bool is true
|
|
// (otherwise stays unknown — REST lags GraphQL).
|
|
// (8) mergeable == MERGEABLE AND mergeStateStatus == CLEAN
|
|
// -> MergeMergeable
|
|
// (9) otherwise -> MergeUnknown
|
|
//
|
|
// - Checks[]: one entry per rollup context. For CheckRun rows we use
|
|
// name + conclusion + detailsUrl + the head SHA as the CommitHash;
|
|
// for StatusContext rows we use context + state + targetUrl. LogTail
|
|
// is populated ONLY for failure-class CheckRun entries, by fetching
|
|
// /actions/jobs/{job_id}/logs and tailing to the last 20 lines.
|
|
//
|
|
// - Comments[]: one entry per unresolved review-thread comment.
|
|
// Resolved threads are skipped client-side (Resolved on the
|
|
// observation is therefore always false). Bot authors are detected
|
|
// via GitHub's __typename == "Bot" or User.Type == "Bot" and
|
|
// dropped — the legacy strings.Contains(login, "bot") fallback was
|
|
// intentionally NOT carried forward (it false-positives on logins
|
|
// like "robothon" / "lambot123"; aa-18's review of PR #28 flagged
|
|
// this).
|
|
//
|
|
// # Errors
|
|
//
|
|
// The Client classifies HTTP failures into three sentinels:
|
|
//
|
|
// - ErrNotFound — 404 (PR doesn't exist or token can't see it)
|
|
// - ErrAuthFailed — 401, or 403 without rate-limit signals
|
|
// - ErrRateLimited — 403 with X-RateLimit-Remaining=0, 403 with the
|
|
// secondary "abuse detection" body, or 429
|
|
// (also returns *RateLimitError with ResetAt /
|
|
// RetryAfter — match via errors.As)
|
|
//
|
|
// All other transport failures (decode errors, network failures, GraphQL
|
|
// "errors" array) bubble up as wrapped errors with Fetched=false on the
|
|
// observation, so the PR Manager keeps the prior row rather than
|
|
// fabricating a closed/merged transition from a failed observation.
|
|
//
|
|
// # Caching
|
|
//
|
|
// The legacy Observe path's Client maintains an in-memory ETag cache per
|
|
// (method, path, query).
|
|
// On the second observation of the same PR the REST GET sends
|
|
// If-None-Match and replays the cached body on a 304 — GraphQL is always
|
|
// re-fetched because it doesn't expose ETag-based revalidation.
|
|
//
|
|
// The provider-neutral observer owns its own ETag cache and calls explicit
|
|
// provider guard methods that do not mutate the legacy Client cache.
|
|
//
|
|
// # Out of scope (intentionally — these are different PRs / lanes)
|
|
//
|
|
// - Webhook ingestion (this package is polling-only).
|
|
// - Linear / GitLab providers (separate PRs).
|
|
// - Issue tracking (separate lane, see internal/adapters/tracker).
|
|
// - Comment-injection-into-session-context (Messenger lane, not SCM).
|
|
package github
|