102 lines
4.9 KiB
Go
102 lines
4.9 KiB
Go
package github
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
// ErrInvalidPRURL signals that a caller-supplied PR URL could not be parsed.
|
|
// Used by AssociateExistingPRByURL so HTTP callers can translate the failure
|
|
// into a 400 instead of a generic 500.
|
|
var ErrInvalidPRURL = errors.New("invalid PR URL")
|
|
|
|
// ErrTaskNotFound is the sentinel that cleanup paths check to distinguish
|
|
// "the task is already gone — fine, mop up the dedup row" from a real
|
|
// upstream failure. Adapter implementations of TaskDeleter wrap this when
|
|
// the task domain reports a missing row so the github layer can recognize
|
|
// the case without string-matching the underlying error message.
|
|
var ErrTaskNotFound = errors.New("github: task not found for cleanup")
|
|
|
|
// ErrSelfApprove is returned by SubmitReview when the authenticated user
|
|
// attempts to APPROVE their own PR. GitHub rejects this with a 422; we
|
|
// catch it server-side so the UI sees a clean, typed error rather than a
|
|
// generic upstream failure when the frontend's visibility guard is bypassed.
|
|
var ErrSelfApprove = errors.New("cannot approve your own pull request")
|
|
|
|
// ErrInvalidToken is returned by ConfigureToken when the supplied PAT fails
|
|
// validation against the GitHub API. Wrapped around the underlying client
|
|
// error so HTTP callers can distinguish a validation failure (HTTP 400)
|
|
// from a secret-store write failure (HTTP 500).
|
|
var ErrInvalidToken = errors.New("invalid token")
|
|
|
|
// ErrRepoNotResolvable signals that GitHub reported the repository as
|
|
// missing, deleted, or inaccessible to the authenticated principal. The PR
|
|
// watch flood storm this fixes (SyncWatchesBatched hammering the same dead
|
|
// repo on every 5s frontend poll) classifies upstream errors via
|
|
// isRepoNotResolvableErr and feeds the result into a 10-minute negative
|
|
// cache so subsequent watch syncs short-circuit before acquiring the gh
|
|
// throttle. The cache is also flushed on token swap/clear (see
|
|
// Service.ClearRepoErrorCache wired from ConfigureToken/ClearToken) so a
|
|
// re-auth doesn't have to wait out the TTL. Use errors.Is(err,
|
|
// ErrRepoNotResolvable) at boundaries (WS handler, poller) where the
|
|
// caller needs to differentiate a "stop retrying" failure from a
|
|
// transient upstream blip.
|
|
var ErrRepoNotResolvable = errors.New("github: repository not resolvable")
|
|
|
|
// errStoreUnavailable is returned from service methods when no Store is
|
|
// wired (Provide can return a Service with store == nil when the SQLite
|
|
// repos aren't configured). Returning a typed error instead of nil-
|
|
// dereferencing keeps the watch reset handlers from panicking under that
|
|
// degenerate config — the caller surfaces a 5xx, the process keeps running.
|
|
var errStoreUnavailable = errors.New("github: store not configured")
|
|
|
|
// repoNotResolvableSubstrings matches the wire-format substrings GitHub's
|
|
// GraphQL/REST APIs use for a deterministic "repository does not exist /
|
|
// not accessible" outcome:
|
|
// - "Could not resolve to a Repository" — GraphQL response for a missing
|
|
// or deleted repo (the dominant signal in the storm logs)
|
|
// - "Resource not accessible by integration" — GitHub Apps / fine-grained
|
|
// PAT scope mismatch. Treated as "dead end" here because retrying the
|
|
// same call with the same token will never succeed; a token swap
|
|
// evicts the cache via ClearRepoErrorCache.
|
|
//
|
|
// Raw "HTTP 404 / Not Found" is intentionally NOT matched here. A 404 on
|
|
// /repos/{owner}/{repo}/pulls/{N} can legitimately mean "the PR was
|
|
// deleted" while the repo itself is fine — a false-positive there would
|
|
// negative-cache the whole repo for 10 minutes off a single stale PR
|
|
// number. The GraphQL "Could not resolve to a Repository" string IS
|
|
// repo-scoped by construction and remains the canonical signal.
|
|
var repoNotResolvableSubstrings = []string{
|
|
"could not resolve to a repository",
|
|
"resource not accessible by integration",
|
|
}
|
|
|
|
// isRepoNotResolvableErr reports whether err carries a deterministic
|
|
// "repository missing / inaccessible" signal we should not keep retrying
|
|
// against in a tight loop. Errors flagged here are negative-cached for
|
|
// 10 minutes via Service.repoErrorCache; the cache is evicted on watch
|
|
// (re)creation and on token swap/clear so a freshly-linked repo or a
|
|
// re-auth is probed immediately. The match is case-insensitive so we
|
|
// don't depend on a stable error casing across gh CLI versions and
|
|
// GraphQL response variants.
|
|
//
|
|
// Bare HTTP 404 errors are NOT classified — too ambiguous (missing PR
|
|
// number vs missing repo) and a false positive poisons the whole repo
|
|
// for 10 minutes. Callers that know they're hitting a repo-root
|
|
// endpoint should wrap with ErrRepoNotResolvable explicitly.
|
|
func isRepoNotResolvableErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if errors.Is(err, ErrRepoNotResolvable) {
|
|
return true
|
|
}
|
|
s := strings.ToLower(err.Error())
|
|
for _, sub := range repoNotResolvableSubstrings {
|
|
if strings.Contains(s, sub) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|