dashboard

package
v0.167.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

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

Examples

Constants

View Source
const (
	DependabotRebaseComment   = "@dependabot rebase"
	DependabotRecreateComment = "@dependabot recreate"
)

DependabotRebaseComment and DependabotRecreateComment are Dependabot's own documented PR-comment commands (docs.github.com/en/code-security/ dependabot/working-with-dependabot/managing-pull-requests-for-dependency- updates) — the single source of truth for the exact text, shared by the manual dependabot-action API handler and the auto-update-branch hook (auto_update_branch.go) so the two can't drift apart.

View Source
const DefaultForceRefreshCooldown = 5 * time.Second

DefaultForceRefreshCooldown is how long ForceRefresh leaves a user alone after one that fetched (#809).

View Source
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 AnnotateStacks added in v0.125.0

func AnnotateStacks(prs []PullRequest)

AnnotateStacks fills Stack, StackedOn and StackChildren on every pull request in prs, replacing whatever was there (#860). Pull request B is stacked on A when B's base branch is A's head branch, in the same repository on the same forge, and neither is a fork. A branch with several open pull requests takes the lowest-numbered one as parent, and a loop of branches is treated as no stack at all. prs holds open pull requests only, so a base branch no open pull request owns is not flagged.

func DependabotCommandsBlockedReason added in v0.104.0

func DependabotCommandsBlockedReason(src Source) string

DependabotCommandsBlockedReason returns src's reason Dependabot commands can't work through it, or "" when they can (or src doesn't say).

func ForgeMessage added in v0.111.1

func ForgeMessage(err error) string

ForgeMessage returns the forge's own reason with the diagnostic prefix stripped, for showing next to the button it explains.

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 IsHousekeepingIssue added in v0.133.0

func IsHousekeepingIssue(issue Issue) bool

IsHousekeepingIssue reports whether issue is one a bot keeps open and rewrites, not work for a person (#980). Exact title match, the one Renovate always uses. The rule lives here so every client lists and counts the same issues instead of each copying the title.

Example

A bot's housekeeping issue is flagged by the server, so every client lists and counts the same issues as work.

package main

import (
	"fmt"

	"github.com/alrayyes/forge-dashboard/internal/dashboard"
)

func main() {
	fmt.Println(dashboard.IsHousekeepingIssue(dashboard.Issue{Title: "Dependency Dashboard"}))
	fmt.Println(dashboard.IsHousekeepingIssue(dashboard.Issue{Title: "Fix the login page"}))
}
Output:
true
false

func IsReadyToMerge added in v0.122.0

func IsReadyToMerge(pr PullRequest) bool

IsReadyToMerge is the Ready quick filter (#807): mergeable, CI green and not a draft. Not readyToMerge in aggregator.go, which only orders the list and lets a pull request with no checks float up too.

func NeedsReview added in v0.122.0

func NeedsReview(pr PullRequest) bool

NeedsReview is the Needs review quick filter (#807): a review is outstanding, meaning the forge requires one or a reviewer was asked. An unreviewed pull request nobody was asked about is merely unreviewed, approved and changes requested aren't waiting on a reviewer, and an unknown review state (no review object) isn't the same as nobody having reviewed. A draft never needs review.

func NextRefreshDelay added in v0.75.0

func NextRefreshDelay(snap Snapshot, interval time.Duration) time.Duration

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 QueueableAction added in v0.149.0

func QueueableAction(a ActionName) bool

QueueableAction reports whether an action may be queued. Only the safe, idempotent ones are: each is re-checked when it runs and does nothing if it no longer applies. Merge and Close are not, since they would act unattended on a pull request whose checks or commits may have changed; for merging, the forge's own auto-merge covers waiting for the reset.

func RateLimitedUntil added in v0.149.0

func RateLimitedUntil(snap Snapshot, forge Forge, now time.Time) (time.Time, bool)

RateLimitedUntil reports whether forge in snap has a spent budget whose reset is still ahead of now, and when the last such budget comes back. The later one wins because the re-check at run time needs every budget: the action itself uses REST and the fresh read of the board may use GraphQL.

func ReviewRequestedFrom added in v0.124.0

func ReviewRequestedFrom(pr PullRequest, login string) bool

ReviewRequestedFrom is "review requested from me" (#695): an open, non-draft pull request that asks login to review. Logins match without regard to case, since a forge treats them that way. An empty login matches nothing.

func ShortSHA added in v0.161.0

func ShortSHA(sha string) string

ShortSHA cuts a commit id to the seven characters a person recognises.

func SortMainBranches added in v0.161.0

func SortMainBranches(rows []MainBranch)

SortMainBranches orders rows failed first, then running, passed and none, each group by repo name, so a broken main is the first thing on the page.

func WebhookTargetsPath added in v0.54.0

func WebhookTargetsPath(rawURL, path string) bool

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 ActionAvailability added in v0.118.0

type ActionAvailability struct {
	Action  ActionName `json:"action"`
	Blocked *Blocked   `json:"blocked,omitempty"`
}

ActionAvailability says one action applies to a pull request and, when it can't be taken yet, why. An action that doesn't apply at all (Update branch on a pull request that isn't behind, a Dependabot command on a Renovate PR) is absent from the list rather than listed as blocked. Matches components.schemas.AllowedAction.

func AllowedActions added in v0.118.0

func AllowedActions(pr PullRequest, renovateAuthors []string) []ActionAvailability

AllowedActions works out which actions a pull request offers, from the pull request's own fields (#805). It holds what the page used to decide for itself, so every client gets the same answer. Live state stays with the client: a rate-limited or unreachable forge, a missing token, an action already in flight.

type ActionCode added in v0.111.1

type ActionCode string

ActionCode is why a pull request action was refused, from a fixed set every client shares. Matches components.schemas.ActionError.code.

const (
	ActionAlreadyMerged       ActionCode = "already_merged"
	ActionAlreadyClosed       ActionCode = "already_closed"
	ActionNotMergeable        ActionCode = "not_mergeable"
	ActionConflict            ActionCode = "conflict"
	ActionBehind              ActionCode = "behind"
	ActionChecksPending       ActionCode = "checks_pending"
	ActionChecksFailing       ActionCode = "checks_failing"
	ActionBlockedByProtection ActionCode = "blocked_by_protection"
	// The next four belong to one action each.
	ActionAlreadyUpToDate     ActionCode = "already_up_to_date"
	ActionAutoMergeNotAllowed ActionCode = "auto_merge_not_allowed"
	ActionReadyToMerge        ActionCode = "ready_to_merge"
	ActionLabelMissing        ActionCode = "label_missing"
	ActionPermission          ActionCode = "permission"
	ActionRateLimited         ActionCode = "rate_limited"
	ActionStacked             ActionCode = "stacked"
	// ActionAlreadyRequested: a Renovate rebase is already asked for (its
	// rebase-check box is ticked), so asking again does nothing.
	ActionAlreadyRequested ActionCode = "already_requested"
	// ActionNotRateLimited: queueing an action only makes sense while the
	// forge is rate limited (#756); otherwise it can be taken directly.
	ActionNotRateLimited ActionCode = "not_rate_limited"
	// ActionNotOpen: a queued action found its pull request gone from the
	// board when the reset came, merged or closed meanwhile.
	ActionNotOpen ActionCode = "not_open"
	ActionUnknown ActionCode = "unknown"
)

The codes, one per reason a client can act on differently.

type ActionName added in v0.118.0

type ActionName string

ActionName is one thing a client can do to a pull request. Matches components.schemas.AllowedAction.action.

const (
	ActionMerge              ActionName = "merge"
	ActionClose              ActionName = "close"
	ActionUpdateBranch       ActionName = "update_branch"
	ActionAutoMerge          ActionName = "auto_merge"
	ActionCancelAutoMerge    ActionName = "cancel_auto_merge"
	ActionDependabotRebase   ActionName = "dependabot_rebase"
	ActionDependabotRecreate ActionName = "dependabot_recreate"
	ActionRenovateRebase     ActionName = "renovate_rebase"
	ActionRerunChecks        ActionName = "rerun_checks"
)

The actions a pull request can offer.

type ActionRefusal added in v0.111.1

type ActionRefusal struct {
	Code     ActionCode
	Message  string
	ResetsAt *time.Time
}

ActionRefusal is the classified outcome of a refused action. Message is short plain words safe to show a person. ResetsAt is set only for ActionRateLimited, and only when the forge said when.

func ClassifyActionRefusal added in v0.111.1

func ClassifyActionRefusal(actionErr error, state *PullRequestState) ActionRefusal

ClassifyActionRefusal classifies a refused Merge; see ClassifyActionRefusalFor.

func ClassifyActionRefusalFor added in v0.112.0

func ClassifyActionRefusalFor(action PullRequestAction, actionErr error, state *PullRequestState) ActionRefusal

ClassifyActionRefusalFor maps a refused action plus a re-read of the pull request's state onto the shared code set. state is nil when it wasn't (or couldn't be) read; the result is then ActionUnknown carrying the forge's own message, except for refusals that need no re-read: a permission or rate-limit refusal says all there is to say by itself, and so does an Update branch conflict or an auto-merge refusal whose forge text names the reason.

func ClassifyForgeRefusal added in v0.118.0

func ClassifyForgeRefusal(err error) ActionRefusal

ClassifyForgeRefusal classifies a refused forge call that has no pull request to re-read, such as creating a webhook on a repo (#762). It shares the permission and rate-limit codes with pull request actions, so a client handles one set. A repo the forge can't find gets code unknown with its own plain message, since a webhook ensure is the only caller that needs it.

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, opts ...AggregatorOption) *Aggregator

NewAggregator returns an Aggregator whose Get answers an empty snapshot until the first Refresh (or Run) completes.

func (*Aggregator) AutoMergeReport added in v0.135.0

func (a *Aggregator) AutoMergeReport() AutoMergeReport

AutoMergeReport returns a copy of what the pass knows, with merges older than ten minutes left out.

func (*Aggregator) EnableAutoMerge added in v0.135.0

func (a *Aggregator) EnableAutoMerge(userID []byte, store AutoMergeStore)

EnableAutoMerge turns on the hook that runs after every refresh: a Forgejo pull request the user armed gets merged, the way a click on Merge would merge it, once its checks pass and the forge calls it mergeable. Nothing else is ever merged: a pull request the user didn't arm is never touched.

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 brought up to date the same way a manual click would — except a release-please pull request, which is always skipped (release-please regenerates its own branch and changelog together on every push to the base branch, so a generic branch update is a genuine risk of fighting its own next run, and it has no dedicated rebase/label action the way Dependabot and Renovate do). A Dependabot pull request gets DependabotRebaseComment instead of the generic UpdateBranch, mirroring the manual Dependabot: Rebase button, with a follow-up DependabotRecreateComment if that rebase leaves its CI failing (#540). A Renovate pull request instead gets lister's own configured rebase label added (AddLabel), mirroring the manual Renovate: Rebase button — Renovate has no recreate equivalent, so no watch state to track (#541). Both settings are queried fresh on every refresh, since this Aggregator's own dedicated enable/disable endpoints don't trigger a rebuild the way most other settings changes do.

func (*Aggregator) EnableQueuedActions added in v0.153.0

func (a *Aggregator) EnableQueuedActions(userID []byte, store QueuedActionStore)

EnableQueuedActions turns on running the actions userID queued for a forge's rate-limit reset. Nothing else is ever run from the queue.

func (*Aggregator) FirstRefreshDone added in v0.109.0

func (a *Aggregator) FirstRefreshDone() bool

FirstRefreshDone reports whether the first Refresh has completed, with every source's result or its unreachable ForgeHealth entry in the snapshot. A forge that failed still counts: done means "tried", not "succeeded".

func (*Aggregator) Get

func (a *Aggregator) Get() Snapshot

Get returns the current snapshot. Safe to call concurrently with Refresh.

func (*Aggregator) MarkSettled added in v0.119.1

func (a *Aggregator) MarkSettled(forge Forge, repo string, number int)

MarkSettled records that a merge or close of forge/repo#number just succeeded (#835). The pull request leaves the snapshot at once, subscribers are told, and later refreshes keep it off the board while the forge still lists it as open. The server knows the action worked, so it decides what is listed instead of leaving every client to wait for the forge to catch up.

func (*Aggregator) PollCI added in v0.92.1

func (a *Aggregator) PollCI(ctx context.Context)

PollCI refreshes every repo that currently has at least one open pull request whose CI is still pending — the only case a check finishing could actually change, and the only one worth spending an extra scoped fetch on between full Refresh passes. Fills the gap a working webhook would otherwise close: confirmed live, twice independently, that real Forgejo instances silently drop the "status" webhook event from a hook's persisted event list even though the create/edit call reports success (#177) — so without this, a finished check on Forgejo waits out the full Run interval (20 minutes by default) before the dashboard reflects it. Goes through RefreshRepo, not a bespoke fetch — same keyed coalescer, so a repo a webhook delivery (on forges where it does work, e.g. GitHub) already refreshed seconds ago is never refreshed twice for the same reason.

func (*Aggregator) ProcessQueuedActions added in v0.153.0

func (a *Aggregator) ProcessQueuedActions(ctx context.Context)

ProcessQueuedActions runs every queued action whose reset has passed. It reads the board fresh first (the refresh skips a forge until its reset, so this is the first real read), then decides each action against what the forge shows now, runs it only if it still applies, and records the outcome.

func (*Aggregator) QueuedActionsChanged added in v0.153.0

func (a *Aggregator) QueuedActionsChanged()

QueuedActionsChanged tells the scheduler the queue changed, and subscribers to read it again, since a queued action rides next to the snapshot and not in it.

func (*Aggregator) RecordBotRequest added in v0.132.0

func (a *Aggregator) RecordBotRequest(forge Forge, repo string, number int, bot BotName, action BotAction)

RecordBotRequest notes that a bot was asked to rebase forge/repo#number. The pull request carries the request from now on, subscribers are told, and later snapshots move it along. A pull request not on the board is ignored, and a second request replaces the first.

func (*Aggregator) RecordUpdateRequest added in v0.133.0

func (a *Aggregator) RecordUpdateRequest(forge Forge, repo string, number int)

RecordUpdateRequest notes that the forge accepted an Update branch for forge/repo#number. The pull request carries the request from now on, subscribers are told, and later snapshots clear it once the branch is no longer behind. A pull request not on the board is ignored, and a second request replaces the first.

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, attrs ...any) 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.

attrs are slog key/value pairs added to the "scoped refresh failed" line, so a caller that knows why it refreshed (a webhook delivery's id and pull request number) can have the failure say so (#966).

func (*Aggregator) Republish added in v0.149.0

func (a *Aggregator) Republish()

Republish tells every subscriber the current snapshot again, for a change a client reads next to it that the snapshot itself doesn't hold, like a queued action (#756).

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) RunCIPoll added in v0.92.1

func (a *Aggregator) RunCIPoll(ctx context.Context, interval time.Duration)

RunCIPoll runs PollCI on a fixed interval until ctx is cancelled — PollCI's own ticker-loop counterpart, the same relationship Run has to Refresh. A plain Ticker, unlike Run's rate-limit-aware Timer: PollCI's own per-tick cost is already bounded to just the repos with something pending, so it doesn't need the same backoff Run's account-wide pass does. Meant to run in its own goroutine for the lifetime of the process, alongside Run.

func (*Aggregator) RunQueuedActions added in v0.153.0

func (a *Aggregator) RunQueuedActions(ctx context.Context)

RunQueuedActions is the scheduler: it sleeps until the earliest queued action's reset and then runs what is due, until ctx ends. It reads the queue from the store every time, so a restart loses nothing and a cancel or a new item is seen at the next wake-up.

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.

func (*Aggregator) Unsettle added in v0.144.1

func (a *Aggregator) Unsettle(forge Forge, repo string, number int)

Unsettle forgets a MarkSettled for forge/repo#number, for a pull request that was reopened: the next refresh lists it again instead of holding it back. The snapshot itself is left to that refresh.

func (*Aggregator) WaitFirstRefresh added in v0.147.3

func (a *Aggregator) WaitFirstRefresh(ctx context.Context, limit time.Duration)

WaitFirstRefresh blocks until the first refresh has completed, ctx is done or max passes, whichever comes first. A snapshot read before that is the empty one, which a page shows as "no pull requests" until the fetch lands (#1194).

type AggregatorOption added in v0.119.1

type AggregatorOption func(*Aggregator)

AggregatorOption configures NewAggregator.

func WithClock added in v0.132.0

func WithClock(now func() time.Time) AggregatorOption

WithClock sets the clock bot request deadlines read, for tests.

func WithQueuedActionBuffer added in v0.153.0

func WithQueuedActionBuffer(d time.Duration) AggregatorOption

WithQueuedActionBuffer sets how long past a reset the server waits before it runs a queued action. It defaults to backoffBuffer, the same pad the refresh back-off uses, so the forge has rolled its counter over.

func WithSettleWindow added in v0.119.1

func WithSettleWindow(d time.Duration) AggregatorOption

WithSettleWindow sets how long MarkSettled keeps a pull request off the board while the forge still lists it.

type AutoMergeFailure added in v0.135.0

type AutoMergeFailure struct {
	UpdatedAt time.Time
	Refusal   ActionRefusal
}

AutoMergeFailure is the refusal of the last merge tried on a pull request. It speaks for that version of the pull request only: once UpdatedAt moves, the pass tries again and the old refusal no longer applies.

type AutoMergeReport added in v0.135.0

type AutoMergeReport struct {
	Failures map[string]AutoMergeFailure
	Merged   []AutoMerged
}

AutoMergeReport is what the pass knows that the snapshot doesn't: why each armed pull request's last merge was refused, keyed "forge/owner/repo#number", and what it merged in the last few minutes, newest first.

type AutoMergeState added in v0.135.0

type AutoMergeState string

AutoMergeState is whether an armed pull request's auto-merge will clear by itself. Matches components.schemas.AutoMergeStatus.state.

const (
	AutoMergeWaiting AutoMergeState = "waiting"
	AutoMergeStopped AutoMergeState = "stopped"
)

The states of an armed pull request.

type AutoMergeStatus added in v0.135.0

type AutoMergeStatus struct {
	State   AutoMergeState `json:"state"`
	Code    ActionCode     `json:"code,omitempty"`
	Message string         `json:"message"`
}

AutoMergeStatus says in words where an armed pull request stands. Matches components.schemas.AutoMergeStatus.

func AutoMergeStatusOf added in v0.135.0

func AutoMergeStatusOf(pr PullRequest, failure *AutoMergeFailure) AutoMergeStatus

AutoMergeStatusOf explains an armed pull request. A refusal of the current version outranks what the fields say, since it is what actually happened when the merge was tried. A rate limit only waits.

type AutoMergeStore added in v0.135.0

type AutoMergeStore interface {
	AutoMergeIntents(ctx context.Context, userID []byte) (map[string]struct{}, error)
	CancelAutoMerge(ctx context.Context, userID []byte, forge, repoFullName string, number int) error
}

AutoMergeStore is what an Aggregator needs to merge the pull requests a user armed (the app's own Forgejo auto-merge): the armed set, keyed "forge/owner/repo#number", and a way to drop one. settings.Store satisfies it. Defined here, in the consuming package.

type AutoMerged added in v0.135.0

type AutoMerged struct {
	Forge  Forge
	Repo   string
	Number int
	At     time.Time
}

AutoMerged is a pull request this app merged for its user, kept briefly so the page can say so after the pull request has left the board.

type AutoUpdateBranchLister added in v0.80.0

type AutoUpdateBranchLister interface {
	AutoUpdateBranchRepos(ctx context.Context, userID []byte) (map[string]struct{}, error)
	// RenovateRebaseLabel reports userID's own configured Renovate rebase
	// label (Credentials.RenovateRebaseLabelOrDefault) — what a behind
	// Renovate pull request gets labeled with instead of the generic
	// UpdateBranch call (#541), mirroring the manual Renovate: Rebase
	// button.
	RenovateRebaseLabel(ctx context.Context, userID []byte) (string, error)
	// RenovateAuthors reports the logins userID has said are Renovate's
	// (Settings), so a Forgejo or GitLab Renovate account is recognised
	// (#1062).
	RenovateAuthors(ctx context.Context, userID []byte) ([]string, 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. Defined here in the consuming package per go.md, rather than importing internal/settings' concrete *Store directly. settings.Store satisfies this today.

type Blocked added in v0.118.0

type Blocked struct {
	Code    ActionCode `json:"code"`
	Message string     `json:"message"`
	Next    string     `json:"next,omitempty"`
}

Blocked is why an offered action can't be taken right now. Code comes from the same set as ActionError.code. Message is plain words for a person, and Next says what unlocks it, when something does.

type BotAction added in v0.132.0

type BotAction string

BotAction is what was asked of the bot. Renovate's rebase label is a BotRebase.

const (
	BotRebase   BotAction = "rebase"
	BotRecreate BotAction = "recreate"
)

The actions a bot request can carry.

type BotName added in v0.132.0

type BotName string

BotName names the dependency bot a request went to.

const (
	BotDependabot BotName = "dependabot"
	BotRenovate   BotName = "renovate"
)

The bots this app can ask to rebase a pull request.

type BotPhase added in v0.132.0

type BotPhase string

BotPhase is where a bot request stands.

const (
	BotQueued   BotPhase = "queued"
	BotRebasing BotPhase = "rebasing"
	BotExpired  BotPhase = "expired"
)

The phases of a bot request. Queued means asked and nothing seen yet, rebasing means the bot pushed and CI hasn't restarted, expired means the bot didn't act in time.

type BotRequest added in v0.132.0

type BotRequest struct {
	Bot         BotName   `json:"bot"`
	Action      BotAction `json:"action"`
	Phase       BotPhase  `json:"phase"`
	RequestedAt time.Time `json:"requestedAt"`
	// ExpiresAt is when the server stops waiting in the current phase.
	ExpiresAt time.Time `json:"expiresAt"`
	// AcknowledgedAt is when this app first saw Dependabot's thumbs-up on the
	// command comment (#1082). Nil until then, and always nil for Renovate.
	AcknowledgedAt *time.Time `json:"acknowledgedAt,omitempty"`
	// CommentURL is the command comment on the forge, once this app has
	// found it.
	CommentURL string `json:"commentUrl,omitempty"`
}

BotRequest is a Dependabot or Renovate rebase asked for through this app and not settled yet (#808). The server owns it, so a reload shows it again and every client agrees. It matches components.schemas.BotRequest.

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.

const (
	CISuccess CIStatus = "success"
	CIFailure CIStatus = "failure"
	CIPending CIStatus = "pending"
	CINone    CIStatus = "none"
)

The combined result across every check on a pull request's head commit.

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"`
	// Required reports whether the base branch's protection makes this
	// check block the merge. A pointer so "unknown" (nil, omitted from
	// the JSON) stays distinct from "advisory" (false): a token that
	// can't read protection, or a check whose protection name can't be
	// mapped, must not read as advisory.
	Required *bool `json:"required,omitempty"`
	// DurationSeconds is how long a completed check ran (#697), nil when
	// the forge didn't say or it hasn't finished.
	DurationSeconds *int `json:"durationSeconds,omitempty"`
	// FailedStep names the step of a failed job that broke, and Excerpt is
	// the tail of its log as plain text, capped in length (#697). Both are
	// empty when the forge can't provide them (permission, expired logs, a
	// check that is not a job); nothing is invented.
	FailedStep string `json:"failedStep,omitempty"`
	Excerpt    string `json:"excerpt,omitempty"`
}

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 ChecksRerunner added in v0.135.0

type ChecksRerunner interface {
	RerunFailedChecks(ctx context.Context, owner, name string, number int) error
}

ChecksRerunner is implemented by a ForgeClient (or, for github.Client, a Source directly) that can rerun the failed jobs of the CI on one of its pull requests (#698) — checked via a type assertion, the same optional- capability pattern PullRequestMerger uses. It reruns only what failed. An error of kind ForgeErrorConflict means there was no failed job it could rerun.

type ClientError added in v0.52.0

type ClientError struct {
	Kind ForgeErrorKind
	Err  error
	// RateLimit is the budget the failing response reported, when it
	// reported one. Lets a write's caller say when a rate limit clears.
	RateLimit *RateLimit
}

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 CommandAck added in v0.137.0

type CommandAck struct {
	CommentURL   string
	Acknowledged bool
}

CommandAck is what a forge shows of one Dependabot command comment (#1082): where the comment is and whether Dependabot reacted to it with a thumbs-up, which is how it says it received the command.

type DependabotAckReader added in v0.137.0

type DependabotAckReader interface {
	DependabotAck(ctx context.Context, owner, name string, number int, command string, since time.Time) (CommandAck, error)
}

DependabotAckReader is implemented by a Source that can look up the command comment this app posted on a pull request and whether Dependabot has reacted to it. command is the exact comment body, and since is when the request was made, so an older comment with the same body is not mistaken for it. A pull request with no such comment yet gives a zero CommandAck. Optional, like PullRequestCommenter: only GitHub has Dependabot.

type DependabotCommandBlocker added in v0.104.0

type DependabotCommandBlocker interface {
	DependabotCommandsBlockedReason() string
}

DependabotCommandBlocker is implemented by a Source whose comment credential Dependabot would refuse (#666) — Dependabot only honours commands from a user with push access, never a GitHub App. An empty reason means commands work. Optional, the same pattern PullRequestCommenter uses: a Source that doesn't implement it is treated as unblocked.

type FailedRepo added in v0.147.1

type FailedRepo struct {
	FullName     string
	PullRequests bool
	Issues       bool
	// MainBranch means the default-branch read failed (#1224).
	MainBranch bool
}

FailedRepo is one repo a Source could not fully fetch, and which parts.

type Forge

type Forge string

Forge names one of the two forges this service talks to. Matches components.schemas.Forge in api/openapi.yaml.

const (
	ForgeGitHub  Forge = "github"
	ForgeForgejo Forge = "forgejo"
)

The only two forges this service knows how to talk to.

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"
	ForgeErrorUnauthorized ForgeErrorKind = "unauthorized"
	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"`
	// StaleSince is set when this refresh failed and the pull requests,
	// issues and repos shown for the forge are the last good ones, fetched
	// at this time (#922). Absent when the data is current, and for a forge
	// that has never fetched successfully, which has nothing to show.
	StaleSince *time.Time `json:"staleSince,omitempty"`
	// 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"`
	// DependabotCommandsBlocked, when non-empty, is why a "@dependabot"
	// comment sent through this forge's credential would be refused
	// (#666): Dependabot ignores GitHub Apps. The frontend locks the
	// Dependabot buttons with this exact text.
	DependabotCommandsBlocked string `json:"dependabotCommandsBlocked,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) ClosePullRequest added in v0.95.2

func (s *GenericSource) ClosePullRequest(ctx context.Context, owner, name string, number int) error

ClosePullRequest implements PullRequestCloser at the Source level by delegating to the underlying client, the same "Source unwraps to its ForgeClient" shape EnsureWebhook/MergePullRequest/UpdateBranch already use. Same #455 shape: internal/forgejo.Client already implements PullRequestCloser, but GenericSource (what BuildSources actually registers for Forgejo) never exposed it, so the type assertion in handlePullRequestClose always failed for a real Forgejo pull request (confirmed live: closing a real tracked pull request answered "forgejo doesn't support closing pull requests" even though the underlying client genuinely can).

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

func (s *GenericSource) MergePullRequest(ctx context.Context, owner, name string, number int) error

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) ReadPullRequestState added in v0.111.1

func (s *GenericSource) ReadPullRequestState(ctx context.Context, owner, name string, number int) (PullRequestState, error)

ReadPullRequestState implements PullRequestStateReader at the Source level by delegating to the underlying client, the same shape MergePullRequest uses. The client's own error is returned as is, so its ClientError classification survives.

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

type Label struct {
	Name  string `json:"name"`
	Color string `json:"color"`
}

Label matches components.schemas.Label — a label's name and its real colour from the forge, not just the name.

type MainBranch added in v0.161.0

type MainBranch struct {
	Forge    Forge            `json:"forge"`
	FullName string           `json:"fullName"`
	URL      string           `json:"url"`
	Branch   string           `json:"branch"`
	Status   MainBranchStatus `json:"status"`
	// Commit is nil when the forge named no commit.
	Commit *MainBranchCommit `json:"commit"`
	// FailingJobs names the failed jobs, or the failed workflow when its jobs
	// couldn't be read. Never nil: the contract promises an array.
	FailingJobs []string `json:"failingJobs"`
	// FinishedAt and DurationSeconds are nil while the pipeline runs, and when
	// the forge didn't say.
	FinishedAt      *time.Time `json:"finishedAt"`
	DurationSeconds *int       `json:"durationSeconds"`
	// RunURL is where to see the run on the forge, falling back to URL.
	RunURL string `json:"runUrl"`
	// StaleSince is set on a row carried over from an earlier refresh because
	// its forge couldn't be read (#922): when it was last fetched. The API
	// derives the stale flag from it.
	StaleSince *time.Time `json:"staleSince"`
}

MainBranch is one tracked repo and the CI status of its default branch. Matches components.schemas.MainBranch.

type MainBranchCommit added in v0.161.0

type MainBranchCommit struct {
	// SHA is shortened to seven characters.
	SHA     string `json:"sha"`
	Subject string `json:"subject"`
	Author  string `json:"author"`
	// CommittedAt is nil when the forge didn't say.
	CommittedAt *time.Time `json:"committedAt"`
}

MainBranchCommit is the newest commit on a default branch. Matches components.schemas.MainBranchCommit.

type MainBranchCounts added in v0.161.0

type MainBranchCounts struct {
	Failed  int `json:"failed"`
	Passed  int `json:"passed"`
	Running int `json:"running"`
	None    int `json:"none"`
	Total   int `json:"total"`
}

MainBranchCounts is how many rows each status holds. Matches components.schemas.MainBranchCounts.

func CountMainBranches added in v0.161.0

func CountMainBranches(rows []MainBranch) MainBranchCounts

CountMainBranches tallies rows by status.

type MainBranchReader added in v0.161.0

type MainBranchReader interface {
	ReadMainBranch(ctx context.Context, repo RepoRef) (MainBranch, error)
}

MainBranchReader is implemented by a ForgeClient that can read the CI status of a repo's default branch — checked via a type assertion, the same optional-capability pattern WebhookChecker uses. GitHub doesn't implement it: its answer rides the repository query the refresh already makes. The caller fills Forge, FullName and URL; the reader fills the rest.

type MainBranchStatus added in v0.161.0

type MainBranchStatus string

MainBranchStatus is the result of the latest pipeline on a repo's default branch (#1224). Matches components.schemas.MainBranchStatus.

const (
	MainBranchFailed  MainBranchStatus = "failed"
	MainBranchPassed  MainBranchStatus = "passed"
	MainBranchRunning MainBranchStatus = "running"
	MainBranchNone    MainBranchStatus = "none"
)

The four things a default branch's newest commit can be in. Failed wins over Running: a pipeline with one job down is broken however many others still run.

func MainBranchStatusOfCI added in v0.161.0

func MainBranchStatusOfCI(ci CIStatus) MainBranchStatus

MainBranchStatusOfCI maps a combined CIStatus onto a default branch's status.

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

func NewManager(refreshInterval time.Duration) *Manager

NewManager returns a Manager whose per-user Aggregators refresh every refreshInterval.

func (*Manager) AutoMergeReport added in v0.135.0

func (m *Manager) AutoMergeReport(userID []byte) AutoMergeReport

AutoMergeReport delegates to userID's Aggregator; empty for a user with none.

func (*Manager) Ensure added in v0.8.0

func (m *Manager) Ensure(ctx context.Context, userID []byte, sources []Source)

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

func (m *Manager) EnsureIfAbsent(ctx context.Context, userID []byte, sources []Source)

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) FindPullRequest added in v0.133.0

func (m *Manager) FindPullRequest(userID []byte, forge Forge, repo string, number int) (PullRequest, bool)

FindPullRequest returns the pull request userID's board holds for forge/repo/number. False when it isn't there, which says nothing against the pull request: the board may just not have caught up with it yet.

func (*Manager) FirstRefreshComplete added in v0.109.0

func (m *Manager) FirstRefreshComplete() bool

FirstRefreshComplete is the readiness answer for the dashboard half of /readyz. It is true when no Aggregator exists yet (Aggregators are built lazily per signed-in user, so a fresh process has nothing to wait for), and once any Aggregator has finished its first refresh, whether every forge answered or not. After that it stays true for the life of the Manager: forge reachability is dashboard data, not readiness.

func (*Manager) ForceRefresh added in v0.138.0

func (m *Manager) ForceRefresh(ctx context.Context, userID []byte) (retryAfter time.Duration, ok bool)

ForceRefresh is RefreshNow for a user's own "refresh now" request, with a per-user cooldown so that call can't be used to spend a forge's API budget. A call inside the window does not fetch: it returns the time left, and the caller serves the current snapshot. A call that fetched returns zero. ok is false when userID has no running Aggregator, as with RefreshNow.

Webhooks keep calling RefreshNow: a forge event isn't a user's click, and throttling it would leave the board stale.

func (*Manager) Get added in v0.8.0

func (m *Manager) Get(userID []byte) Snapshot

Get returns userID's current snapshot — empty, not nil arrays, if Ensure was never called for them (no credentials saved yet).

func (*Manager) MarkSettled added in v0.119.1

func (m *Manager) MarkSettled(userID []byte, forge Forge, repo string, number int)

MarkSettled delegates to userID's Aggregator — see Aggregator.MarkSettled. A user with no running Aggregator has no board to update, so it does nothing.

func (*Manager) QueuedActionsChanged added in v0.153.0

func (m *Manager) QueuedActionsChanged(userID []byte)

QueuedActionsChanged delegates to userID's Aggregator, see Aggregator.QueuedActionsChanged. A user with no running Aggregator has nothing to wake and no subscribers, so it does nothing.

func (*Manager) RecordBotRequest added in v0.132.0

func (m *Manager) RecordBotRequest(userID []byte, forge Forge, repo string, number int, bot BotName, action BotAction)

RecordBotRequest delegates to userID's Aggregator — see Aggregator.RecordBotRequest. A user with no running Aggregator has no board to show it on, so it does nothing.

func (*Manager) RecordUpdateRequest added in v0.133.0

func (m *Manager) RecordUpdateRequest(userID []byte, forge Forge, repo string, number int)

RecordUpdateRequest delegates to userID's Aggregator — see Aggregator.RecordUpdateRequest. A user with no running Aggregator has no board to show it on, so it does nothing.

func (*Manager) RefreshNow added in v0.23.0

func (m *Manager) RefreshNow(ctx context.Context, userID []byte) bool

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, attrs ...any) 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

func (m *Manager) Remove(userID []byte)

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) Republish added in v0.149.0

func (m *Manager) Republish(userID []byte)

Republish delegates to userID's Aggregator, see Aggregator.Republish. A user with no running Aggregator has no subscribers, so it does nothing.

func (*Manager) Running added in v0.27.1

func (m *Manager) Running(userID []byte) bool

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) SetAutoMergeStore added in v0.135.0

func (m *Manager) SetAutoMergeStore(store AutoMergeStore)

SetAutoMergeStore turns on the Forgejo auto-merge hook for every Aggregator this Manager builds from here on, the same way SetAutoUpdateBranchLister does: call once, before the first Ensure.

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.

func (*Manager) SetCIPollInterval added in v0.92.1

func (m *Manager) SetCIPollInterval(d time.Duration)

SetCIPollInterval turns on the CI-status poll fallback (#177) for every Aggregator this Manager builds from here on — a setter rather than a NewManager parameter, the same reason SetAutoUpdateBranchLister is one: every existing caller (the composition root's own construction, and every test 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. Zero (the default) leaves the poll off entirely — Aggregator.PollCI/RunCIPoll's own doc comments cover why a real deployment wants it on.

func (*Manager) SetFirstRefreshGrace added in v0.127.0

func (m *Manager) SetFirstRefreshGrace(d time.Duration)

SetFirstRefreshGrace changes how long readiness waits for a first refresh before it counts as ready anyway (#893). A setter rather than a NewManager parameter, for the same reason SetCIPollInterval is one.

func (*Manager) SetForceRefreshCooldown added in v0.138.0

func (m *Manager) SetForceRefreshCooldown(d time.Duration)

SetForceRefreshCooldown changes the window ForceRefresh throttles inside. A setter, like SetCIPollInterval, so NewManager's callers are unaffected.

func (*Manager) SetQueuedActionStore added in v0.153.0

func (m *Manager) SetQueuedActionStore(store QueuedActionStore)

SetQueuedActionStore turns on running the actions users queued for a rate limit's reset (#756) for every Aggregator this Manager builds from here on, the way SetAutoMergeStore does: call once, before the first Ensure.

func (*Manager) Stop added in v0.8.0

func (m *Manager) Stop()

Stop cancels every running per-user refresh loop.

func (*Manager) Subscribe added in v0.23.0

func (m *Manager) Subscribe(userID []byte) (<-chan Snapshot, func(), bool)

Subscribe delegates to userID's Aggregator — see Aggregator.Subscribe. Reports false if userID has no running Aggregator.

func (*Manager) Unsettle added in v0.144.1

func (m *Manager) Unsettle(userID []byte, forge Forge, repo string, number int)

Unsettle delegates to userID's Aggregator — see Aggregator.Unsettle.

func (*Manager) WaitFirstRefresh added in v0.147.3

func (m *Manager) WaitFirstRefresh(ctx context.Context, userID []byte, limit time.Duration)

WaitFirstRefresh holds until userID's Aggregator has finished its first refresh, for up to limit, so a read right after a restart or a settings save gets the board and not the empty snapshot that precedes it. It returns at once for a user with no Aggregator, or one that has refreshed already.

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"
	MergeUnstable    MergeStatus = "unstable"
	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"`
	// BaseBranch and HeadBranch name the branch the pull request targets and
	// the branch it comes from (#860). A stack is worked out from them: B is
	// stacked on A when B's base branch is A's head branch.
	BaseBranch string `json:"baseBranch"`
	HeadBranch string `json:"headBranch"`
	// CrossRepository is true when the head branch lives in another
	// repository (a fork). Such a pull request is never part of a stack.
	CrossRepository bool `json:"crossRepository"`
	// Stack, StackedOn and StackChildren say where the pull request sits in
	// a stack of pull requests (#860); AnnotateStacks fills them. Stack and
	// StackedOn are null when there is none, and StackChildren is always a
	// list.
	Stack         *StackPosition `json:"stack"`
	StackedOn     *StackRef      `json:"stackedOn"`
	StackChildren []int          `json:"stackChildren"`

	// BotRequest is a Dependabot or Renovate rebase asked for through this
	// app and not settled yet (#808), or nil.
	BotRequest *BotRequest `json:"botRequest"`
	// UpdateRequest is an Update branch the forge accepted and that hasn't
	// shown up in a snapshot yet (#982).
	UpdateRequest *UpdateRequest `json:"updateRequest"`
	// RequestedReviewerLogins lists who was asked to review this pull
	// request, by login (#695). Teams have no login and are left out.
	// Always a list, empty when nobody was asked, so a client can range
	// over it without a nil check.
	RequestedReviewerLogins []string `json:"requestedReviewerLogins"`
	// HeadSHA is the commit the pull request's head branch points at (#759).
	// A bot's rebase moves it, which shows the bot acted even when the pull
	// request is still reported behind, or wasn't behind to begin with.
	// Empty when the forge didn't say.
	HeadSHA string `json:"headSha"`
	// Empty reports whether merging this pull request would produce an
	// empty commit — its content already landed on the base branch some
	// other way (confirmed live: a mechanical version-bump PR whose branch
	// had already been merged into master through another route, leaving
	// Merge silently a no-op with nothing in the UI explaining why).
	// Deliberately conservative rather than
	// nilable like AutoMergeEnabled: a source that can't tell (the
	// unauthenticated GitHub REST fallback, or a Forgejo pull request
	// whose Additions/Deletions/ChangedFiles came back nil) just leaves
	// this false, the same as a real non-empty diff — never a false
	// positive that would hide Merge on a pull request that still has
	// something to merge.
	Empty bool `json:"empty"`
	// 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"`
	// AutoMergeAllowed is GitHub's per-viewer viewerCanEnableAutoMerge
	// answer (#738): false when GitHub would refuse "Enable auto-merge",
	// e.g. a stacked PR whose base branch has no protection rule. Nil when
	// unknown (the REST fallback, Forgejo), so clients keep today's
	// behaviour rather than treating unknown as refused.
	AutoMergeAllowed *bool `json:"autoMergeAllowed,omitempty"`
	// Review is nil when the forge couldn't report review state (see
	// ReviewState), the same omit-when-unknown shape as AutoMergeEnabled.
	Review *ReviewState `json:"review,omitempty"`
}

PullRequest matches components.schemas.PullRequest in api/openapi.yaml.

type PullRequestAction added in v0.112.0

type PullRequestAction string

PullRequestAction names the action that was refused, since the same state says different things about different actions: a conflicting PR explains a refused Merge or Update branch, but not a refused Close.

const (
	PullRequestActionMerge           PullRequestAction = "merge"
	PullRequestActionClose           PullRequestAction = "close"
	PullRequestActionUpdateBranch    PullRequestAction = "update_branch"
	PullRequestActionAutoMerge       PullRequestAction = "auto_merge"
	PullRequestActionCancelAutoMerge PullRequestAction = "cancel_auto_merge"
	PullRequestActionDependabot      PullRequestAction = "dependabot"
	PullRequestActionRenovateRebase  PullRequestAction = "renovate_rebase"
	PullRequestActionRerunChecks     PullRequestAction = "rerun_checks"
)

The actions that answer a refusal with an ActionRefusal.

type PullRequestAutoMergeDisabler added in v0.166.0

type PullRequestAutoMergeDisabler interface {
	DisableAutoMerge(ctx context.Context, owner, name string, number int) error
}

PullRequestAutoMergeDisabler is implemented by a ForgeClient (or, for github.Client, a Source directly) that can take a pull request's own native auto-merge back off: GitHub's disablePullRequestAutoMerge mutation (#1293). Forgejo's auto-merge is this app's own intent and is cancelled without calling the forge.

type PullRequestAutoMerger added in v0.95.0

type PullRequestAutoMerger interface {
	EnableAutoMerge(ctx context.Context, owner, name string, number int) error
}

PullRequestAutoMerger is implemented by a ForgeClient (or, for github.Client, a Source directly) that can arm a pull request's own native auto-merge — checked via a type assertion, the same optional-capability pattern PullRequestMerger uses. Unlike MergePullRequest, GitHub's own enablePullRequestAutoMerge mutation asks for an explicit merge method rather than picking the repo's configured default itself, so implementations look the repo's allowed methods up and pick one the same way MergePullRequest's own mergeMethodFor does — no override exposed here either (#526).

type PullRequestBodyEditor added in v0.139.0

type PullRequestBodyEditor interface {
	PullRequestBody(ctx context.Context, owner, name string, number int) (string, error)
	SetPullRequestBody(ctx context.Context, owner, name string, number int, body string) error
}

PullRequestBodyEditor is implemented by a Source that can read and replace the description of one of its own pull requests — checked via a type assertion, like PullRequestLabeler. Renovate's rebase checkbox lives in that description.

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 PullRequestCloser added in v0.88.0

type PullRequestCloser interface {
	ClosePullRequest(ctx context.Context, owner, name string, number int) error
}

PullRequestCloser is implemented by a ForgeClient (or, for github.Client, a Source directly) that can close one of its own pull requests without merging it — checked via a type assertion, the same optional-capability pattern PullRequestMerger uses. For a PR that turns out not to need merging at all (a duplicate, one whose content already landed another way), Close is the action that actually applies, not Merge.

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 PullRequestKind added in v0.135.0

type PullRequestKind string

PullRequestKind is what sort of pull request this is. Matches components.schemas.PullRequest.kind.

const (
	KindRelease    PullRequestKind = "release"
	KindDependency PullRequestKind = "dependency"
	KindRegular    PullRequestKind = "regular"
)

The kinds a pull request can be.

func KindOf added in v0.135.0

func KindOf(pr PullRequest, renovateAuthors []string) PullRequestKind

KindOf is the one place that decides a pull request's kind (#716), from the same release-please and bot-author rules the allowed actions and the auto-update pass use. A release label wins: it is the only signal for release-please's pull requests, and it is a stronger statement than an author.

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 PullRequestState added in v0.111.1

type PullRequestState struct {
	Merged        bool
	Closed        bool
	Draft         bool
	Conflicting   bool
	Behind        bool
	Blocked       bool
	ChecksFailing bool
	// ConflictUnconfirmed is set when the forge says the pull request isn't
	// mergeable but can't say why. Forgejo's mergeable flag is false for a
	// conflict, and equally while it is still checking or when the check
	// errored, so a conflict is the probable cause and not a proven one.
	ConflictUnconfirmed bool
	// ChangesWorkflows is set when the pull request changes a file under
	// .github/workflows/. GitHub won't update such a branch for an app
	// without the Workflows permission (#919).
	ChangesWorkflows bool
}

PullRequestState is a live re-read of one pull request, just enough to explain why an action on it was refused. Forge-neutral: GitHub fills the flags from mergeable_state, Forgejo (which has no equivalent) only knows merged, closed, draft and an overall not-mergeable verdict, which it reports as ConflictUnconfirmed or, for an empty diff, Blocked.

type PullRequestStateReader added in v0.111.1

type PullRequestStateReader interface {
	ReadPullRequestState(ctx context.Context, owner, name string, number int) (PullRequestState, error)
}

PullRequestStateReader is implemented by a ForgeClient (or, for github.Client, a Source directly) that can re-read one pull request's current state — checked via a type assertion, the same optional- capability pattern PullRequestMerger uses.

type QueuedAction added in v0.149.0

type QueuedAction struct {
	ID     string       `json:"id"`
	Action ActionName   `json:"action"`
	Forge  Forge        `json:"forge"`
	Repo   string       `json:"fullName"`
	Number int          `json:"number"`
	Status QueuedStatus `json:"status"`
	// RunAt is when the forge's budget comes back, which is when the server
	// looks at the pull request again and decides.
	RunAt     time.Time `json:"runAt"`
	CreatedAt time.Time `json:"createdAt"`
	// FinishedAt, Code and Message are set once the status is final. Code is
	// from the ActionCode set (already_merged, conflict, rate_limited...), and
	// Message is plain words safe to show a person.
	FinishedAt *time.Time `json:"finishedAt,omitempty"`
	Code       ActionCode `json:"code,omitempty"`
	Message    string     `json:"message,omitempty"`
}

QueuedAction is a safe action a user asked to run once a rate-limited forge resets (#756). The server owns it, per user, so a reload or a restart shows it again. It matches components.schemas.QueuedAction.

type QueuedActionStore added in v0.153.0

type QueuedActionStore interface {
	QueuedActions(ctx context.Context, userID []byte) ([]QueuedAction, error)
	UpdateQueuedAction(ctx context.Context, userID []byte, q QueuedAction) (bool, error)
	PruneQueuedActions(ctx context.Context, userID []byte, cutoff time.Time) error
	RenovateRebaseLabel(ctx context.Context, userID []byte) (string, error)
	RenovateAuthors(ctx context.Context, userID []byte) ([]string, error)
}

QueuedActionStore is what an Aggregator needs to run the actions a user queued for the reset (#756): their queue, a way to save an outcome, the settings a Renovate rebase uses. settings.Store satisfies it. Defined here, in the consuming package.

type QueuedStatus added in v0.149.0

type QueuedStatus string

QueuedStatus is where a queued action stands.

const (
	QueuedWaiting QueuedStatus = "queued"
	QueuedDone    QueuedStatus = "done"
	QueuedSkipped QueuedStatus = "skipped"
	QueuedFailed  QueuedStatus = "failed"
)

The statuses of a queued action. Queued waits for the reset. The other three are final: done ran, skipped found the pull request no longer needed it, and failed ran and the forge refused.

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"`
	// Severity is how worried a client should be about this budget (#806).
	// The API fills it in when it answers, from the clock, so it never goes
	// stale in a snapshot; it is empty inside the aggregator's own snapshot.
	Severity RateLimitSeverity `json:"severity,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."

func (RateLimit) SeverityAt added in v0.119.0

func (r RateLimit) SeverityAt(now time.Time) RateLimitSeverity

SeverityAt grades r as of now. A spent budget whose reset time has passed is not exceeded any more, since the forge has refilled it; the reading is just stale, so it grades low until the next snapshot says otherwise.

Example

A rate-limit budget is graded in one place: ok, then warning under 20% left, low under 5%, and exceeded once nothing is left and the reset is ahead.

package main

import (
	"fmt"
	"time"

	"github.com/alrayyes/forge-dashboard/internal/dashboard"
)

func main() {
	now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
	reset := now.Add(10 * time.Minute)

	for _, remaining := range []int{4000, 800, 100, 0} {
		rl := dashboard.RateLimit{Limit: 5000, Remaining: remaining, ResetsAt: reset}
		fmt.Println(remaining, rl.SeverityAt(now))
	}
}
Output:
4000 ok
800 warning
100 low
0 exceeded

type RateLimitSeverity added in v0.119.0

type RateLimitSeverity string

RateLimitSeverity grades a RateLimit. Matches components.schemas.RateLimit.severity.

const (
	RateLimitOK       RateLimitSeverity = "ok"
	RateLimitWarning  RateLimitSeverity = "warning"
	RateLimitLow      RateLimitSeverity = "low"
	RateLimitExceeded RateLimitSeverity = "exceeded"
)

The grades: Exceeded means the budget is spent and not seen to have reset, Low means under lowBudgetFraction of it is left, and Warning means under warningBudgetFraction is left but not yet low.

type RateLimiter added in v0.36.0

type RateLimiter interface {
	RateLimit(ctx context.Context) (RateLimit, error)
}

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 RebaseCheckbox added in v0.139.0

type RebaseCheckbox int

RebaseCheckbox is what a pull request body says about Renovate's `rebase-check` checkbox.

const (
	// RebaseCheckboxAbsent means the body has no rebase-check line.
	RebaseCheckboxAbsent RebaseCheckbox = iota
	// RebaseCheckboxUnticked means the box is there and unchecked.
	RebaseCheckboxUnticked
	// RebaseCheckboxTicked means the box is already checked, so a rebase is queued.
	RebaseCheckboxTicked
)

func TickRebaseCheckbox added in v0.139.0

func TickRebaseCheckbox(body string) (string, RebaseCheckbox)

TickRebaseCheckbox checks Renovate's rebase-check box in body. It returns the new body, changed in that one character only, and what the body held before. A body with the box already checked or missing comes back as is.

type RenovateRebaseOutcome added in v0.139.0

type RenovateRebaseOutcome int

RenovateRebaseOutcome is how a Renovate rebase request was made.

const (
	// RenovateRebaseTicked means the body's checkbox was checked.
	RenovateRebaseTicked RenovateRebaseOutcome = iota
	// RenovateRebaseLabeled means there was no checkbox, so the label was added.
	RenovateRebaseLabeled
	// RenovateRebaseAlreadyRequested means the box was already checked.
	RenovateRebaseAlreadyRequested
)

func RequestRenovateRebase added in v0.139.0

func RequestRenovateRebase(ctx context.Context, src any, labeler PullRequestLabeler, owner, name string, number int, label string) (RenovateRebaseOutcome, error)

RequestRenovateRebase asks Renovate to rebase owner/name#number. It checks the rebase-check box in the pull request body, which needs no setup on the repo, and falls back to adding label when src can't edit the body or the body has no such box. Forge errors come back unwrapped: the API handler shows their text to the person, and the client already says where it came from.

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"`
	// StaleSince is set when this refresh could not fetch some of the repo's
	// pull requests or issues, and the ones shown are the last good ones,
	// fetched at this time (#1194). Absent when the repo's data is current.
	StaleSince *time.Time `json:"staleSince,omitempty"`
}

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
	// DefaultBranch is the repo's default branch, "" when the forge says none.
	DefaultBranch string
}

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
	// FailedRepos names the repos whose pull requests or issues could not be
	// fetched this time while the forge itself answered. Their rows are
	// missing from PullRequests and Issues, which says nothing about whether
	// they are gone, so the aggregator keeps the last ones it had (#1194).
	FailedRepos []FailedRepo
	// MainBranches is one row per repo this forge reads the default-branch CI
	// of (#1224). A repo whose read failed is in FailedRepos and missing here.
	MainBranches []MainBranch
}

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 and named in FailedRepos — a good forge with one broken repo still reports Reachable: true.

type ReviewDecision added in v0.107.0

type ReviewDecision string

ReviewDecision is where a pull request stands on code review. Matches components.schemas.ReviewState's decision.

const (
	ReviewApproved         ReviewDecision = "approved"
	ReviewChangesRequested ReviewDecision = "changes_requested"
	ReviewRequired         ReviewDecision = "review_required"
	ReviewNone             ReviewDecision = "none"
)

The coarse review states both forges can map onto. GitHub reports its own reviewDecision (null when no review is required, which becomes ReviewNone or a derived value); Forgejo has no such field, so DeriveReviewDecision builds one from its reviews and requested reviewers.

func DeriveReviewDecision added in v0.107.0

func DeriveReviewDecision(approvals, changesRequested, requested int) ReviewDecision

DeriveReviewDecision builds a decision from counts, for forges with no decision field of their own: a standing change request wins, then any approval, then an outstanding review request, else nothing.

type ReviewState added in v0.107.0

type ReviewState struct {
	Decision           ReviewDecision `json:"decision"`
	Approvals          int            `json:"approvals"`
	RequestedReviewers int            `json:"requestedReviewers"`
}

ReviewState matches components.schemas.ReviewState. A nil *ReviewState on PullRequest means the forge couldn't report review state, never "no reviews" (that is Decision == ReviewNone).

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"`
	// MainBranches is the CI status of every tracked repo's default branch
	// (#1224), one row per repo, served by GET /api/main-branches.
	MainBranches []MainBranch `json:"mainBranches"`
}

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 StackPosition added in v0.125.0

type StackPosition struct {
	// Position is 1 for the bottom of the stack, 2 for a pull request
	// stacked directly on it, and so on.
	Position int `json:"position"`
	// Size is how many open pull requests the stack holds.
	Size int `json:"size"`
}

StackPosition is where a pull request sits in a stack of pull requests (#860). Matches components.schemas.StackPosition.

type StackRef added in v0.125.0

type StackRef struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
}

StackRef names the pull request another is stacked on. Matches components.schemas.StackRef.

type UpdatePhase added in v0.133.0

type UpdatePhase string

UpdatePhase is where an Update branch request stands.

const (
	UpdateQueued  UpdatePhase = "queued"
	UpdateExpired UpdatePhase = "expired"
)

The phases of an Update branch request. Queued means the forge accepted it and no snapshot shows the branch caught up yet, expired means it still hasn't after updateQueuedWait.

type UpdateRequest added in v0.133.0

type UpdateRequest struct {
	Phase       UpdatePhase `json:"phase"`
	RequestedAt time.Time   `json:"requestedAt"`
	// ExpiresAt is when the server stops waiting in the current phase.
	ExpiresAt time.Time `json:"expiresAt"`
}

UpdateRequest is an Update branch the forge accepted and that no snapshot has shown landing yet (#982). The server owns it, so a reload shows it again and every client agrees. It matches components.schemas.UpdateRequest.

type WebhookChecker added in v0.54.0

type WebhookChecker interface {
	HasWebhook(ctx context.Context, owner, name string) (bool, error)
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL