dashboard

package
v0.77.2 Latest Latest
Warning

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

Go to latest
Published: Sep 19, 2026 License: AGPL-3.0 Imports: 8 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

Constants

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 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 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 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 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) 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 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 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.

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"`
	// 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) 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) 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 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) 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) 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) 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) 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) 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) 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.

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 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"`
}

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

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 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

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