Documentation
¶
Overview ¶
Package dashboard holds the domain model both forge clients produce and the aggregator that merges them into one snapshot. Neither forge client depends on the other; both only depend on this package's types.
Index ¶
- Constants
- func HumanizeForgeError(kind ForgeErrorKind) string
- func IsBotManagedPR(pr PullRequest) bool
- func NextRefreshDelay(snap Snapshot, interval time.Duration) time.Duration
- func WebhookTargetsPath(rawURL, path string) bool
- type Aggregator
- func (a *Aggregator) EnableAutoUpdateBranch(userID []byte, lister AutoUpdateBranchLister)
- func (a *Aggregator) Get() Snapshot
- func (a *Aggregator) Refresh(ctx context.Context)
- func (a *Aggregator) RefreshRepo(ctx context.Context, forge Forge, owner, name, fullName string) bool
- func (a *Aggregator) Run(ctx context.Context, interval time.Duration)
- func (a *Aggregator) Subscribe() (<-chan Snapshot, func())
- type AutoUpdateBranchLister
- type BranchUpdater
- type CIStatus
- type Check
- type CheckState
- type ClientError
- type Forge
- type ForgeClient
- type ForgeErrorKind
- type ForgeHealth
- type GenericSource
- func (s *GenericSource) EnsureWebhook(ctx context.Context, owner, name, targetURL, secret string) error
- func (s *GenericSource) Fetch(ctx context.Context) Result
- func (s *GenericSource) FetchRepo(ctx context.Context, owner, name, fullName string) ([]PullRequest, []Issue, error)
- func (s *GenericSource) Forge() Forge
- func (s *GenericSource) ListChecks(ctx context.Context, owner, name string, number int) ([]Check, error)
- func (s *GenericSource) MergePullRequest(ctx context.Context, owner, name string, number int) error
- func (s *GenericSource) UpdateBranch(ctx context.Context, owner, name string, number int) (bool, error)
- type Issue
- type Label
- type Manager
- func (m *Manager) Ensure(ctx context.Context, userID []byte, sources []Source)
- func (m *Manager) EnsureIfAbsent(ctx context.Context, userID []byte, sources []Source)
- func (m *Manager) Get(userID []byte) Snapshot
- func (m *Manager) RefreshNow(ctx context.Context, userID []byte) bool
- func (m *Manager) RefreshRepo(ctx context.Context, userID []byte, forge Forge, owner, name, fullName string) bool
- func (m *Manager) Remove(userID []byte)
- func (m *Manager) Running(userID []byte) bool
- func (m *Manager) SetAutoUpdateBranchLister(lister AutoUpdateBranchLister)
- func (m *Manager) Stop()
- func (m *Manager) Subscribe(userID []byte) (<-chan Snapshot, func(), bool)
- type MergeStatus
- type PullRequest
- type PullRequestChecker
- type PullRequestCommenter
- type PullRequestLabeler
- type PullRequestMerger
- type RateLimit
- type RateLimiter
- type Repo
- type RepoRef
- type RepoRefresher
- type Result
- type Snapshot
- type Source
- type WebhookChecker
- type WebhookManager
Constants ¶
const DefaultMaxConcurrency = 8
DefaultMaxConcurrency bounds how many repositories a GenericSource fetches at once. Generous enough to make quick work of a ~100-repo account without opening that many sockets at once for no benefit.
Variables ¶
This section is empty.
Functions ¶
func HumanizeForgeError ¶ added in v0.71.0
func HumanizeForgeError(kind ForgeErrorKind) string
HumanizeForgeError returns forgeErrorDetails' sentence for kind, or a generic fallback for ForgeErrorUnknown and any kind this map hasn't been taught yet — never the underlying error's own text. The real error is still logged server-side (see GenericSource.Fetch's own slog.Warn call) for whoever operates this instance; it just never reaches a client.
func IsBotManagedPR ¶ added in v0.80.0
func IsBotManagedPR(pr PullRequest) bool
IsBotManagedPR reports whether pr is opened and kept up to date by one of the release/dependency bots this account runs — release-please, Dependabot, Renovate — the Go-side port of +page.svelte's own isBotManagedPr, needed here too because the background refresh loop that drives auto-update-branch (#365) has no frontend button to suppress the way a manual click already does.
func NextRefreshDelay ¶ added in v0.75.0
NextRefreshDelay returns how long to wait before the next scheduled refresh, given the snapshot the most recent one just produced. interval unchanged unless some forge's REST or GraphQL budget is critically low, in which case it delays until that budget's own reset (plus backoffBuffer, capped at maxBackoff) instead — never shorter than interval, so a stale or already-past ResetsAt can't speed polling up. The worse of several critical budgets wins.
func WebhookTargetsPath ¶ added in v0.54.0
WebhookTargetsPath reports whether rawURL's path component is exactly path — how a Source recognizes "this hook is the one forge-dashboard itself would have created," regardless of the scheme or host a webhook was configured against. Matching on the path alone, not the full URL, is deliberate: this process may sit behind a reverse proxy whose public origin it has no independent way to know, and the webhook token in the path is already unique enough (256 bits of randomness) that a path match carries no real collision risk. An unparseable rawURL, or an empty path, never matches.
Types ¶
type Aggregator ¶
type Aggregator struct {
// contains filtered or unexported fields
}
Aggregator holds the most recently fetched Snapshot and refreshes it in the background. Get never blocks on a forge: it always returns whatever was last successfully assembled, even if a refresh is running right now or every source just failed.
func NewAggregator ¶
func NewAggregator(sources []Source) *Aggregator
NewAggregator returns an Aggregator whose Get answers an empty snapshot until the first Refresh (or Run) completes.
func (*Aggregator) EnableAutoUpdateBranch ¶ added in v0.80.0
func (a *Aggregator) EnableAutoUpdateBranch(userID []byte, lister AutoUpdateBranchLister)
EnableAutoUpdateBranch turns on the hook that runs after every completed refresh: any pull request the snapshot reports Behind, on a repo lister currently reports enabled for userID, gets UpdateBranch called on it the same way a manual click would — skipped for a bot-managed pull request unless lister currently allows that too, the same restraint the manual button already applies. Both are queried fresh on every refresh, since this Aggregator's own dedicated enable/disable endpoints (and a bot-PR-updates toggle) don't trigger a rebuild the way most other settings changes do.
func (*Aggregator) Get ¶
func (a *Aggregator) Get() Snapshot
Get returns the current snapshot. Safe to call concurrently with Refresh.
func (*Aggregator) Refresh ¶
func (a *Aggregator) Refresh(ctx context.Context)
Refresh fetches every source concurrently and replaces the snapshot with the merged result, and does not return until a pass that started at or after this call has completed — see coalescer's doc comment for the guarantee and the incident behind it. A source's own Fetch never returns an error — a forge it can't reach at all shows up as an unreachable ForgeHealth entry instead, so one broken forge never drops the other's data.
func (*Aggregator) RefreshRepo ¶ added in v0.39.0
func (a *Aggregator) RefreshRepo(ctx context.Context, forge Forge, owner, name, fullName string) bool
RefreshRepo refreshes just the named repository's pull requests and issues, merging the result into the current snapshot in place of that repo's previous entries rather than replacing the whole snapshot — the scoped counterpart to Refresh, for a webhook delivery that already knows exactly which repo changed. Reports false if no configured Source drives forge, or if the one that does can't do a scoped fetch (RepoRefresher, the same optional-capability pattern as RateLimiter) — the caller's own signal to fall back to a full Refresh, so a repo never silently goes unrefreshed just because its Source can't do this yet.
Concurrent calls for the same repo coalesce the same way Refresh's own calls do — see coalescer — keyed so an unrelated repo's own refresh never waits on this one.
func (*Aggregator) Run ¶
func (a *Aggregator) Run(ctx context.Context, interval time.Duration)
Run refreshes immediately, then again roughly every interval, until ctx is canceled — "roughly" because each cycle's actual delay comes from NextRefreshDelay against the snapshot that refresh just produced, so a critically low rate-limit budget pushes the next one out past its own reset instead of firing on schedule into an already-exhausted quota (#381). A plain Timer, not a Ticker, since the delay changes cycle to cycle rather than staying fixed. Meant to run in its own goroutine for the lifetime of the process.
func (*Aggregator) Subscribe ¶ added in v0.23.0
func (a *Aggregator) Subscribe() (<-chan Snapshot, func())
Subscribe returns a channel that receives the new Snapshot after every completed Refresh, and a function to stop receiving them. The channel is buffered by exactly one — a subscriber that falls behind (an SSE client whose write is blocked on a slow network) never makes Refresh itself block; it just misses an intermediate update and catches up on the next one. Call the returned function when done, typically via defer: it closes the channel, so a range over it ends cleanly.
type AutoUpdateBranchLister ¶ added in v0.80.0
type AutoUpdateBranchLister interface {
AutoUpdateBranchRepos(ctx context.Context, userID []byte) (map[string]struct{}, error)
AllowsBotPRUpdates(ctx context.Context, userID []byte) (bool, error)
}
AutoUpdateBranchLister reports what an Aggregator needs to decide which behind pull requests to auto-update for userID (#365): the set of repos enabled for it, and whether bot-managed pull requests are included — both queried fresh on every refresh, from the same source, so they can never drift out of sync with each other. Defined here in the consuming package per go.md, rather than importing internal/settings' concrete *Store directly. settings.Store satisfies this today.
type BranchUpdater ¶ added in v0.63.0
type BranchUpdater interface {
UpdateBranch(ctx context.Context, owner, name string, number int) (accepted bool, err error)
}
BranchUpdater is implemented by a ForgeClient (or, for github.Client, a Source directly) that can bring one of its own pull requests' head branch up to date with its base — checked via a type assertion, the same optional-capability pattern PullRequestMerger uses. accepted is true only for GitHub's async case (the update was scheduled as a background job, not finished yet, per GitHub's own PullRequestsService .UpdateBranch doc comment); every other outcome — Forgejo's own synchronous update, or GitHub's synchronous 200/201 case — returns false, since the update had already finished by the time this returns.
type CIStatus ¶
type CIStatus string
CIStatus is the combined result across every check reported against a pull request's head commit. Matches components.schemas.CIStatus.
type Check ¶ added in v0.79.0
type Check struct {
Name string `json:"name"`
State CheckState `json:"state"`
// URL is that job's own page on the forge that ran it — a GitHub
// Actions job page, a Forgejo Actions job page, or a third-party CI's
// own details page for the legacy commit-status case — never the
// pull request's own page.
URL string `json:"url"`
}
Check is one job/check run against a pull request's head commit — the per-job list a pipeline detail view shows, fetched on demand rather than eagerly for every PR on every refresh (see dashboard.PullRequestChecker). Matches components.schemas.Check.
type CheckState ¶ added in v0.79.0
type CheckState string
CheckState is one job/check's own status — the per-job detail CIStatus deliberately doesn't carry, since CIStatus is the combined result across every one of them. Matches components.schemas.CheckState.
const ( CheckQueued CheckState = "queued" CheckRunning CheckState = "running" CheckSuccess CheckState = "success" CheckFailure CheckState = "failure" CheckCancelled CheckState = "cancelled" CheckSkipped CheckState = "skipped" CheckTimedOut CheckState = "timed_out" )
The states a single check/job can be in, coarse enough for both forges' real values to map onto: GitHub's check-run status/conclusion pair and combined-status "state", and Forgejo/Gitea Actions' own job status.
type ClientError ¶ added in v0.52.0
type ClientError struct {
Kind ForgeErrorKind
Err error
}
ClientError carries a ForgeErrorKind classification alongside the underlying error, the same "structured data alongside a message string" shape internal/github's own apiError.rateLimit already uses — promoted to this package because GenericSource.Fetch, which builds ForgeHealth for any ForgeClient-driven forge, can't see an unexported type in the client package that produced the error.
func (*ClientError) Error ¶ added in v0.52.0
func (e *ClientError) Error() string
func (*ClientError) Unwrap ¶ added in v0.52.0
func (e *ClientError) Unwrap() error
type Forge ¶
type Forge string
Forge names one of the two forges this service talks to. Matches components.schemas.Forge in api/openapi.yaml.
type ForgeClient ¶
type ForgeClient interface {
ListRepos(ctx context.Context) ([]RepoRef, error)
ListOpenPullRequests(ctx context.Context, owner, name, repo string) ([]PullRequest, error)
ListOpenIssues(ctx context.Context, owner, name, repo string) ([]Issue, error)
}
ForgeClient is what a forge-specific client (internal/github, internal/forgejo) provides for GenericSource to drive. Both clients' method sets already match this shape, so neither needs an adapter.
type ForgeErrorKind ¶ added in v0.52.0
type ForgeErrorKind string
ForgeErrorKind classifies why a forge is unreachable, or why a write to it was rejected, into a small, coarse set of actionable categories. Matches components.schemas.ForgeErrorKind. Empty string is the zero value: "nothing to classify," only ever meaningful alongside a non-empty ForgeHealth.Error.
const ( ForgeErrorUnreachable ForgeErrorKind = "unreachable" ForgeErrorNotFound ForgeErrorKind = "not_found" ForgeErrorRateLimited ForgeErrorKind = "rate_limited" // ForgeErrorConflict is only ever returned from a pull-request write — // the forge reports the PR itself isn't currently mergeable, either a // real conflict or a state that changed since the dashboard's last // refresh. ForgeErrorConflict ForgeErrorKind = "conflict" ForgeErrorUnknown ForgeErrorKind = "unknown" )
A forge failure only needs to tell the user one of a handful of things to do about it — retry, check a token, check a URL, wait out a rate limit — so this stays as coarse as MergeStatus does, rather than chasing every status code a forge can return.
type ForgeHealth ¶
type ForgeHealth struct {
Forge Forge `json:"forge"`
Reachable bool `json:"reachable"`
Error string `json:"error,omitempty"`
ErrorKind ForgeErrorKind `json:"errorKind,omitempty"`
RepoCount int `json:"repoCount"`
// RateLimitGraphQL and RateLimitREST are two independent budgets
// (#361) — GitHub tracks REST and GraphQL as separate 5000/hour
// allowances, so a client spending both (this codebase's GitHub
// client does: the main query is GraphQL, checkWebhooks and every
// write action are REST) has two numbers to report, not one. Nil
// independently: a forge that only ever makes REST calls (Forgejo,
// through GenericSource) never populates RateLimitGraphQL at all,
// and RateLimitREST stays nil on a poll that made no REST calls
// (no webhook path configured).
RateLimitGraphQL *RateLimit `json:"rateLimitGraphQL,omitempty"`
RateLimitREST *RateLimit `json:"rateLimitREST,omitempty"`
}
ForgeHealth matches components.schemas.ForgeHealth.
type GenericSource ¶
type GenericSource struct {
// contains filtered or unexported fields
}
GenericSource drives a ForgeClient the same way regardless of which forge it talks to: list repositories, then fetch each one's open pull requests and issues concurrently, bounded, skipping a single repo's failure rather than failing the whole forge.
func NewGenericSource ¶
func NewGenericSource(forge Forge, client ForgeClient, maxConcurrency int) *GenericSource
NewGenericSource returns a Source reporting as forge, driving client with at most maxConcurrency repositories fetched at once.
func (*GenericSource) EnsureWebhook ¶ added in v0.56.0
func (s *GenericSource) EnsureWebhook(ctx context.Context, owner, name, targetURL, secret string) error
EnsureWebhook implements WebhookManager at the Source level by delegating to the underlying client — the same "Source unwraps to its ForgeClient" pattern FetchRepo doesn't need, since ForgeClient's own method set already matches what RepoRefresher wants, but which WebhookManager does need since EnsureWebhook isn't part of the plain ForgeClient interface every client implements.
func (*GenericSource) Fetch ¶
func (s *GenericSource) Fetch(ctx context.Context) Result
Fetch implements Source by listing repos, then fetching each one's open pull requests and issues concurrently, bounded by maxConcurrency. A single repo's failure is logged and skipped, not fatal to the forge.
func (*GenericSource) FetchRepo ¶ added in v0.38.0
func (s *GenericSource) FetchRepo(ctx context.Context, owner, name, fullName string) ([]PullRequest, []Issue, error)
FetchRepo implements RepoRefresher: the same per-repo calls Fetch already makes for every tracked repo, but for just the one a caller (a webhook delivery) already knows the identity of — no ListRepos call needed first.
func (*GenericSource) Forge ¶ added in v0.38.0
func (s *GenericSource) Forge() Forge
Forge implements Source.
func (*GenericSource) ListChecks ¶ added in v0.82.0
func (s *GenericSource) ListChecks(ctx context.Context, owner, name string, number int) ([]Check, error)
ListChecks implements PullRequestChecker at the Source level by delegating to the underlying client, the same "Source unwraps to its ForgeClient" shape EnsureWebhook/MergePullRequest/UpdateBranch already use. #455: this forwarding was missing entirely — internal/forgejo.Client already implements PullRequestChecker, but GenericSource (what BuildSources actually registers for Forgejo) never exposed it, so the type assertion in handlePullRequestChecks always failed for a real Forgejo pull request.
func (*GenericSource) MergePullRequest ¶ added in v0.62.0
MergePullRequest implements PullRequestMerger at the Source level by delegating to the underlying client, the same "Source unwraps to its ForgeClient" shape EnsureWebhook already uses.
func (*GenericSource) UpdateBranch ¶ added in v0.63.0
func (s *GenericSource) UpdateBranch(ctx context.Context, owner, name string, number int) (bool, error)
UpdateBranch implements BranchUpdater at the Source level by delegating to the underlying client, the same "Source unwraps to its ForgeClient" shape EnsureWebhook/MergePullRequest already use.
type Issue ¶
type Issue struct {
Forge Forge `json:"forge"`
Repo string `json:"repo"`
Number int `json:"number"`
Title string `json:"title"`
URL string `json:"url"`
Author string `json:"author"`
Labels []Label `json:"labels"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
Issue matches components.schemas.Issue in api/openapi.yaml.
type Label ¶ added in v0.22.0
Label matches components.schemas.Label — a label's name and its real colour from the forge, not just the name.
type Manager ¶ added in v0.8.0
type Manager struct {
// contains filtered or unexported fields
}
Manager owns one Aggregator per user, each refreshing itself in the background on its own schedule — the per-user equivalent of a single global Aggregator, now that each user's dashboard reflects their own saved forge credentials rather than one shared environment-configured set.
func NewManager ¶ added in v0.8.0
NewManager returns a Manager whose per-user Aggregators refresh every refreshInterval.
func (*Manager) Ensure ¶ added in v0.8.0
Ensure (re)builds userID's Aggregator from sources and starts its background refresh, stopping whatever was running for that user before. Call it once after login (using their currently saved credentials) and again every time those credentials are saved — there's no way to add or remove a Source from a running Aggregator, so a changed configuration means a new one.
func (*Manager) EnsureIfAbsent ¶ added in v0.38.1
EnsureIfAbsent is Ensure, but only takes effect if userID has no Aggregator running yet — the check and the insert happen under the same lock, unlike a separate Running() check followed by a call to Ensure(), which races when several requests for the same user land in the same instant (several browser tabs and an SSE reconnect, all arriving right after a restart wipes the Manager clean): each would see "not running" and each would create its own Aggregator with its own immediate Refresh. Concurrent callers here still each do their own harmless work building sources beforehand; only one gets to actually create the Aggregator and kick off its first Refresh.
func (*Manager) Get ¶ added in v0.8.0
Get returns userID's current snapshot — empty, not nil arrays, if Ensure was never called for them (no credentials saved yet).
func (*Manager) RefreshNow ¶ added in v0.23.0
RefreshNow fetches userID's sources immediately, out of band from their regular refreshInterval ticker — the webhook handler's entry point for turning a forge event into an up-to-date snapshot within seconds rather than waiting for the next scheduled refresh. Reports false if userID has no running Aggregator (Ensure was never called for them), rather than silently doing nothing.
func (*Manager) RefreshRepo ¶ added in v0.39.0
func (m *Manager) RefreshRepo(ctx context.Context, userID []byte, forge Forge, owner, name, fullName string) bool
RefreshRepo delegates to userID's Aggregator — see Aggregator.RefreshRepo. Reports false if userID has no running Aggregator, the same as RefreshNow, and also false wherever Aggregator.RefreshRepo itself would — a caller should fall back to RefreshNow either way, without needing to tell the two apart.
func (*Manager) Remove ¶ added in v0.9.0
Remove stops userID's refresh loop and evicts their snapshot — a no-op if they had none running. Used when an admin revokes or deletes an account, so a locked-out user's background refresh doesn't keep polling either forge for no one.
func (*Manager) Running ¶ added in v0.27.1
Running reports whether userID currently has an Aggregator refreshing in the background. False right after a process restart for every user — the Manager holds no state across one — until something re-Ensures them; a caller can use this to tell "nothing saved in Settings" apart from "just needs rewarming from what's already saved."
func (*Manager) SetAutoUpdateBranchLister ¶ added in v0.80.0
func (m *Manager) SetAutoUpdateBranchLister(lister AutoUpdateBranchLister)
SetAutoUpdateBranchLister turns on the auto-update-branch hook (#365) for every Aggregator this Manager builds from here on — a setter rather than a NewManager parameter so every existing caller (the composition root's own construction, and every test building a Manager with no interest in this feature) is unaffected. Call once, before the first Ensure/EnsureIfAbsent that should carry it; an Aggregator already running when this is called keeps running without it until its own next Ensure rebuilds it.
type MergeStatus ¶ added in v0.50.0
type MergeStatus string
MergeStatus is a pull request's mergeable/blocked state, as coarse as every forge this service talks to can agree on. Matches components.schemas.MergeStatus.
const ( MergeMergeable MergeStatus = "mergeable" MergeConflicting MergeStatus = "conflicting" MergeBlocked MergeStatus = "blocked" MergeUnknown MergeStatus = "unknown" )
A forge only ever reports a handful of real distinctions here — GitHub's own mergeStateStatus has eight values, Forgejo's SDK has one plain bool — so this stays deliberately coarse rather than chasing GitHub's full enum. MergeUnknown covers both "the forge itself doesn't know yet" and "this service couldn't determine it."
type PullRequest ¶
type PullRequest struct {
Forge Forge `json:"forge"`
Repo string `json:"repo"`
Number int `json:"number"`
Title string `json:"title"`
URL string `json:"url"`
Author string `json:"author"`
Draft bool `json:"draft"`
Labels []Label `json:"labels"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CI CIStatus `json:"ci"`
MergeStatus MergeStatus `json:"mergeStatus"`
// Behind is deliberately its own field, not folded into MergeStatus,
// because the two aren't mutually exclusive on every forge: confirmed
// live against a real Forgejo instance (a PR's own Mergeable field
// stayed true after a new commit landed on its base, since Forgejo
// doesn't recompute it synchronously — see mergeStatusFromMergeable's
// own doc comment) — so a Forgejo PR can be both "mergeable" and
// "behind" at once, and folding this into MergeStatus would force
// picking one and losing the other. GitHub's mergeStateStatus is a
// single enum where BEHIND and CLEAN can't both be true, but this
// field stays orthogonal there too, for the same reason CI sits
// beside MergeStatus rather than inside it: one fact per field.
Behind bool `json:"behind"`
// AutoMergeEnabled is nil when the owning forge has no way to report
// this at all (Forgejo, today) — the same "doesn't report it" shape
// RateLimit's own nilable pointer already uses, so a forge with
// nothing to say here never renders as a false "not enabled."
AutoMergeEnabled *bool `json:"autoMergeEnabled,omitempty"`
}
PullRequest matches components.schemas.PullRequest in api/openapi.yaml.
type PullRequestChecker ¶ added in v0.79.0
type PullRequestChecker interface {
ListChecks(ctx context.Context, owner, name string, number int) ([]Check, error)
}
PullRequestChecker is implemented by a ForgeClient (or, for github.Client, a Source directly) that can list the individual job/check runs against one of its own pull requests' head commit — checked via a type assertion, the same optional-capability pattern PullRequestLabeler uses. Fetched on demand by its own handler, never as part of a Source's own Fetch/FetchRepo: the eager refresh loop already re-fetches every open PR's collapsed CIStatus on its own cadence, and adding a per-job fetch to that same loop would multiply its cost by every open PR on every refresh for detail most PRs' rows are never opened for.
type PullRequestCommenter ¶ added in v0.68.0
type PullRequestCommenter interface {
CommentPullRequest(ctx context.Context, owner, name string, number int, body string) error
}
PullRequestCommenter is implemented by a ForgeClient (or, for github.Client, a Source directly) that can post a comment on one of its own pull requests — checked via a type assertion, the same optional-capability pattern BranchUpdater uses. Takes a plain body rather than exposing separate per-command methods: Dependabot's own comment-command interface (`@dependabot rebase`, `@dependabot recreate`) is just a PR comment with specific text, and it's the caller's job (the API handler, not this port) to restrict which bodies actually get sent rather than exposing a generic "post any comment" capability.
type PullRequestLabeler ¶ added in v0.69.0
type PullRequestLabeler interface {
AddLabel(ctx context.Context, owner, name string, number int, label string) error
}
PullRequestLabeler is implemented by a ForgeClient (or, for github.Client, a Source directly) that can add a label to one of its own pull requests — checked via a type assertion, the same optional-capability pattern PullRequestCommenter uses. Implemented by both forges, unlike PullRequestCommenter: Renovate's own rebase/retry trigger is adding its configured label, and Renovate runs on GitHub and Forgejo alike.
type PullRequestMerger ¶ added in v0.62.0
type PullRequestMerger interface {
MergePullRequest(ctx context.Context, owner, name string, number int) error
}
PullRequestMerger is implemented by a ForgeClient (or, for github.Client, a Source directly) that can merge one of its own pull requests — checked via a type assertion, the same optional-capability pattern WebhookManager uses. Takes no merge-method override: the caller always wants the repo's own configured default, and picking a method is each implementation's own job (GitHub asks the forge to pick its default; Forgejo's API has no such default and needs it looked up and passed explicitly).
type RateLimit ¶ added in v0.36.0
type RateLimit struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
ResetsAt time.Time `json:"resetsAt"`
// Cost is the actual point price GitHub charged the call that
// populated this value — GraphQL-specific (#440): REST's own budget
// is a plain per-request count with no separate cost concept, so
// RateLimitREST's own RateLimit never sets this, and it stays at its
// zero value there rather than a fabricated 1.
Cost int `json:"cost,omitempty"`
}
RateLimit matches components.schemas.RateLimit — the forge API's own request budget for the credential the last refresh used. Only some forges expose this (GitHub does, Forgejo doesn't by default), so it's always a pointer: nil means "this forge doesn't report one," not "the budget is zero."
type RateLimiter ¶ added in v0.36.0
RateLimiter is implemented by a ForgeClient that can report its own API rate-limit status — GitHub can; Forgejo can't, since it has no rate limiting of its own by default. GenericSource checks for this via a type assertion rather than adding it to ForgeClient itself, so a client with nothing to report doesn't need a no-op method.
type Repo ¶ added in v0.53.0
type Repo struct {
Forge Forge `json:"forge"`
FullName string `json:"fullName"`
// URL is the repo's own page on its forge, for linking out — GitHub's
// is always github.com/<fullName> (this app has no GitHub Enterprise
// support to point elsewhere); Forgejo's comes straight from the
// forge's own API response, since the instance URL isn't otherwise
// tracked per repo.
URL string `json:"url"`
// HasWebhook is a live signal from the Source itself — the forge's
// own webhook list, matched against this app's expected URL — not
// the settings.Store delivery table. False here doesn't mean "no
// webhook confirmed at all": the API layer ORs this with the
// delivery-based signal, since a Source with no webhook path
// configured, or whose live check failed, always reports false.
HasWebhook bool `json:"hasWebhook"`
// CanManageWebhooks reports whether the signed-in user's own
// permission on this repo is enough to list/create its webhooks —
// GitHub requires ADMIN specifically (WRITE/MAINTAIN can push but
// still 404 on GET .../hooks, indistinguishable from the repo not
// existing, by GitHub's own design); Forgejo's equivalent is
// Permissions.Admin. False for every repo reached through an
// unauthenticated, username-only listing, since there's no
// credential to manage anything with. This is a point-in-time
// snapshot from the same listing call HasWebhook comes from — a
// permission change since the last refresh can still make a
// following EnsureWebhook call fail despite this having said true.
CanManageWebhooks bool `json:"canManageWebhooks"`
}
Repo names one tracked repository, independent of whether it currently has any open pull request or issue — RepoCount alone can only answer "how many," and some consumers (webhook-coverage reporting) need "which," including a repo with nothing open at all right now. Matches components.schemas.Repo.
type RepoRef ¶
type RepoRef struct {
FullName string
Owner string
Name string
// URL is the repo's own page on its forge — see Repo.URL.
URL string
// CanManageWebhooks — see Repo.CanManageWebhooks.
CanManageWebhooks bool
}
RepoRef names one repository a ForgeClient is configured to track — every repo the configured credential has write access to, or, with no credential at all, every public repo a configured username owns.
type RepoRefresher ¶ added in v0.38.0
type RepoRefresher interface {
FetchRepo(ctx context.Context, owner, name, fullName string) (prs []PullRequest, issues []Issue, err error)
}
RepoRefresher is implemented by a Source that can refresh just one named repository instead of its whole configured scope — checked via a type assertion, the same optional-capability pattern RateLimiter uses, so a Source with no such shortcut doesn't need a no-op method. A webhook delivery already names the exact repo that changed; without this, the only way to pick up that change is Fetch's full scope, which for a large tracked-repo count is real, avoidable cost on every single delivery.
type Result ¶
type Result struct {
Health ForgeHealth
PullRequests []PullRequest
Issues []Issue
// Repos is every repo this forge is tracking, regardless of whether
// it currently has anything open — RepoCount alone can't name them,
// and a repo with zero open pull requests/issues would otherwise
// never appear in PullRequests/Issues' own Repo fields either.
Repos []Repo
}
Result is what one forge contributes to a Snapshot. Health.Reachable is false only for a fetch that failed outright (couldn't list repositories at all); a single repository's own fetch failing is logged and skipped by the Source, not surfaced here — a good forge with one broken repo still reports Reachable: true.
type Snapshot ¶
type Snapshot struct {
GeneratedAt time.Time `json:"generatedAt"`
Forges []ForgeHealth `json:"forges"`
PullRequests []PullRequest `json:"pullRequests"`
Issues []Issue `json:"issues"`
Repos []Repo `json:"repos"`
}
Snapshot matches components.schemas.Dashboard — the whole body GET /api/dashboard answers with.
type Source ¶
type Source interface {
Fetch(ctx context.Context) Result
// Forge reports which forge this Source drives — how RefreshRepo
// picks the one matching a webhook delivery's own forge out of an
// Aggregator's sources.
Forge() Forge
}
Source is one forge's half of the aggregate. github.Source and forgejo.Source both implement it; the aggregator knows nothing about either forge beyond this.
type WebhookChecker ¶ added in v0.54.0
WebhookChecker is implemented by a ForgeClient (or, for github.Client, a Source directly) that can report whether a repo already has a webhook pointed at this app — checked via a type assertion, the same optional-capability pattern RateLimiter uses. A client with no webhook path configured reports false for every repo without making an API call at all, not an error — the same "optional, degrades quietly" shape RateLimit's own nil pointer already uses.
type WebhookManager ¶ added in v0.56.0
type WebhookManager interface {
EnsureWebhook(ctx context.Context, owner, name, targetURL, secret string) error
}
WebhookManager is implemented by a ForgeClient (or, for github.Client, a Source directly) that can create or update this app's own webhook on one of its tracked repos — checked via a type assertion, the same optional-capability pattern RepoRefresher uses. A single method rather than separate Create/Edit ones: the caller doesn't know or care whether a matching hook already exists, only that one ends up correctly configured — deciding that, and finding an existing one via WebhookTargetsPath rather than creating a duplicate, is the client's own job.