hub

package
v0.172.11 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

README

Workbench GitHub App

The github.com/sneat-dev/wb/hub package is the server side of bench: the Workbench GitHub App. It owns browser and daemon enrollment, GitHub user OAuth verification, installation entitlements, signed webhook translation, and durable repository-event delivery.

The api/githubapp package in this same module owns only the daemon-facing wire models and client behavior. A host supplies Firebase identity and secret configuration adapters and mounts this package's handler on a githubapp.DocumentStore — in practice githubapp/dalgostore.New(db) over any DALgo engine.

See docs/architecture.md for the trust boundaries, protocols, authentication, wire format, delivery guarantees, and daemon communication diagrams.

Self-hosting

wb daemon serve is the self-hosting unit. Add a hub: section to ~/.config/wb/wb.yaml and restart the daemon; without the section the daemon serves exactly what it served before.

hub:
  store:
    engine: memory                       # memory | ingitdb | openvaultdb
  github:
    token_file: /Users/you/.config/wb/credentials/github.token

What appears where, on the daemon's loopback listener (default 127.0.0.1:8766):

Path Served by
/ and /api/v1/… the existing read-only WB dashboard and API
/v0/workbench/… this package's hub API (hub.NewHandler)
/v0/workbench/dashboard, /stats, /series, /leaderboards, /latest-merges, /worktrees the dashboard read API, answered from this machine's published snapshots (githubapp.RemoteStateReadModel)
/workbench/dashboard/ the embedded bench dashboard from hub/web/dist

Starting the daemon prints one line to stderr naming the engine, the store location and the dashboard URL. wb daemon status repeats it as hub_mounted, hub_engine, hub_store and hub_listen, in text and JSON.

There is no sign-in: only this machine can reach the listener, so the viewer resolver returns one fixed identity. wb daemon serve refuses a non-loopback --listen, which is what makes that safe. On first start the daemon enrols itself against its own hub, writes the machine credential to ~/.config/wb/credentials/hub-local-<machine>.token with mode 0600, and points remote: at http://<listen>; remote.url accepts plain http:// only for loopback hosts. Restarting is a no-op because the credential on disk is resolved through the same bearer path a request takes.

Store engines
  • memory — dalgo2memory on the strict Firestore profile. Nothing survives a restart; it is for trying bench out and for tests, and wb daemon status says so.
  • openvaultdb — dalgo2openvaultdb against a server you already run. Set hub.store.url, and hub.store.database_id if it is not wb.
  • ingitdb — dalgo2ingitdb over a directory of inspectable files, created at hub.store.path (default ~/.wb/hub). inGitDB is schema-first, so hubstore.Open declares every collection in hub.Collections() up front. dalgo2ingitdb's query path returns map[string]any for every row rather than the record factory a DALgo query carries, so githubapp/dalgostore.Query decodes generic map rows; TestInGitDBEngineRunsTheHubJourneys in internal/hubstore walks the whole journey on it.
Webhook mode

Polling is the default and needs nothing but a token, on a 20-minute interval unless hub.github.poll_interval says otherwise (floor 30s). It costs two API calls per repository per tick from the token owner's hourly budget of 5000, which every tool using that account shares, so keep the interval long or use a dedicated token. Register a GitHub App when you want push latency instead. Add:

hub:
  github:
    token_file: /Users/you/.config/wb/credentials/github.token
    app:
      app_id: 1234567
      private_key_file: /Users/you/.config/wb/credentials/wb-app.private-key.pem
      webhook_secret_file: /Users/you/.config/wb/credentials/wb-app.webhook-secret
      public_url: https://bench.example.com

All four fields are required together, both files are read at start, and the webhook secret must be at least 32 bytes — GitHub deliveries are verified against it with HMAC-SHA256 over the raw body, exactly as the hosted instance verifies them. The daemon's start line then ends with webhook=on public_url=…, and wb daemon status reports hub_webhook=true. Set the App's webhook URL to <public_url>/v0/workbench/github/webhook.

In webhook mode the poller is not started at all, even with a token file: the App reports every default-branch push and rename, and the daemon pulls only the repository an event names. token_file is optional in this mode.

GitHub keeps the record of a failed delivery — the App's Advanced tab always lists it and lets you redeliver it by hand — it just never retries one automatically. If the tunnel is down or the daemon is unreachable, every push in that window stays undelivered until something asks GitHub for it. The daemon does that itself: on start, and then hourly, it lists the App's webhook deliveries of the last 72 hours through the App API and redelivers every one whose latest attempt failed. A delivery whose redelivery also fails is retried up to three times, spaced at least an hour apart, before being narrated abandoned and never touched again — except while there is no evidence the operator's own endpoint is answering at all (nothing in the window — a success, or an application-level rejection such as 401 or 503, but not a gateway or tunnel non-answer — has been answered more recently than that delivery's last attempt), in which case the daemon keeps asking GitHub to redeliver it every hour without spending one of those three attempts, so an outage longer than three hours cannot exhaust the budget on its own. That does not mean forever, though: the 72-hour window is measured from each delivery's own first attempt, not its latest one, so a delivery the hub never answers at all is still abandoned once 72 hours have passed since it first arrived — narrated "abandoned: older than 72h" — which bounds an unreachable endpoint to at most about 72 uncounted redeliveries per delivery. The Advanced tab's manual redeliver is the recourse once a delivery has been abandoned. Redelivered events arrive at the normal webhook route and deduplicate by delivery ID exactly like any other delivery, so nothing else needs to know a redelivery happened.

Each redelivery and each abandonment is narrated as redeliver. A sweep that could not reach GitHub is narrated as one failed line and retried after a backoff that starts at the sweep's own hourly interval and doubles up to six hours — honoring a Retry-After or X-RateLimit-Reset GitHub sends on a 403 or 429 instead, when it does — never crashing the daemon.

If two hubs are configured against the same GitHub App (for example a primary and a standby, or two machines sharing one App during a migration), both sweep independently and both may redeliver the same failed GUID. That is harmless — GitHub's redeliver is idempotent per attempt id and the resulting event still deduplicates by delivery ID at the webhook route — but it does mean each hub's own attempt counter only reflects what that hub itself asked for, not a shared budget across both.

The connect, setup and OAuth-callback routes under /v0/workbench/github/installations/ answer 503 installation_connection_unavailable on a self-hosted hub: completing them needs an OAuth client secret and a browser redirect GitHub can reach, which a loopback listener has no way to receive. Install the App from GitHub's own UI instead.

Tunnels

wb never starts a tunnel. GitHub has to reach public_url, so run one yourself with your own credentials, in its own terminal, forwarding to the daemon's loopback port (default 8766):

# cloudflared, named tunnel (one-time: cloudflared tunnel login)
cloudflared tunnel create wb-bench
cloudflared tunnel route dns wb-bench bench.example.com
cloudflared tunnel run --url http://127.0.0.1:8766 wb-bench
# public_url: https://bench.example.com
# cloudflared, quick tunnel — prints a fresh https://<random>.trycloudflare.com
# on every start, so public_url and the App's webhook URL change with it
cloudflared tunnel --url http://127.0.0.1:8766
# ngrok (one-time: ngrok config add-authtoken <your token>)
ngrok http 8766 --url=bench.example.com   # reserved domain, stable URL
ngrok http 8766                           # ephemeral URL, changes per start

Only /v0/workbench/github/webhook needs to be public. Both tools forward the whole listener, so treat the tunnel URL as a secret unless you put your own access control in front of it: everything else on that port is the unauthenticated loopback dashboard.

Building the dashboard

hub/web/dist is embedded at build time. A clean clone carries only dist/.gitkeep, and /workbench/ then serves a one-line page saying so. To get the real pages:

cd hub/web && pnpm install && pnpm build   # then rebuild wb

The build output stays git-ignored. hub/web/public/.gitkeep is copied back into dist/ by every build, so building never deletes the tracked placeholder go:embed needs.

Released binaries always carry the real pages: .goreleaser.yml's before hooks run pnpm install --frozen-lockfile && pnpm build in hub/web and then assert that hub/web/dist/dashboard/index.html exists, so a release fails rather than shipping the "not built" page.

Documentation

Index

Constants

View Source
const (
	WebhookPath = APIPrefix + "/github/webhook"
	// MinimumWebhookSecretBytes is the configuration-readiness floor for a
	// high-entropy GitHub webhook secret.
	MinimumWebhookSecretBytes = 32

	CoveragePath = APIPrefix + "/coverage"
	MetricsPath  = APIPrefix + "/metrics"
)
View Source
const (
	InstallationConnectPath   = APIPrefix + "/github/installations/connect"
	InstallationContinuePath  = APIPrefix + "/github/installations/continue"
	InstallationAuthorizePath = APIPrefix + "/github/installations/authorize"
	InstallationSetupPath     = APIPrefix + "/github/installations/setup"
	InstallationCallbackPath  = APIPrefix + "/github/installations/callback"
)
View Source
const (
	MetricTypeCoverage      = "test_coverage"
	MetricTypeCommitsPerDay = "commits_per_day"
)
View Source
const APIPrefix = "/v0/workbench"
View Source
const (
	MachineEnrollmentPath = APIPrefix + "/machines/enroll"
)
View Source
const PeersConnectPath = APIPrefix + "/peers/connect"

PeersConnectPath is the route peer-connectivity#req:invite-and-join adds ahead of Task 2's WebSocket upgrade: "The verification route doesn't exist yet. Add GET /v0/workbench/peers/connect to the hub handler: a request without a WebSocket upgrade and with a valid peer bearer returns 200 {schema_version, peer_id, name}." Task 2 adds the upgrade on the same path; this probe stays as a cheap liveness/verification check. This is an addition to the spec text, which only names the path as the session route — see the PR description.

View Source
const StatusPath = APIPrefix + "/github/status"

Variables

View Source
var (
	ErrArtifactNotFound        = errors.New("coverage artifact not found")
	ErrInvalidWorkflowPayload  = errors.New("invalid workflow_run webhook payload")
	ErrCoverageHarvesterClosed = errors.New("coverage harvester is closed or unconfigured")
)
View Source
var (
	ErrUnauthorized             = errors.New("workbench github app authentication failed")
	ErrUnavailable              = errors.New("workbench github app service is unavailable")
	ErrInvalidInstallationState = errors.New("workbench github app installation state is invalid")
)
View Source
var (
	// ErrPeerExists is returned by CreatePeer when a trust document already
	// exists for the given MachineID.
	ErrPeerExists = errors.New("peer record already exists")
	// ErrPeerNotFound is returned by the read and mutate operations below
	// when no trust document exists for the given MachineID or name. A
	// credential with no such record is not a peer at all, which is a
	// distinct, non-error outcome reported through the bool return instead.
	ErrPeerNotFound = errors.New("peer record not found")
)

Functions

func BuildAppJWT added in v0.147.0

func BuildAppJWT(appID int64, privateKeyPEM []byte, now func() time.Time) (string, error)

BuildAppJWT mints the short-lived JWT the GitHub App API authenticates with — the same signing GitHubAppInstallationVerifier does for installation verification, exported so another authenticated caller (the missed-webhook redelivery sweep in hub/redeliver) reuses it rather than duplicating the signing code. now is optional; nil means time.Now.

func Collections added in v0.124.5

func Collections() []string

Collections lists every collection path shape the hub-owned stores write to, in declaration order: a root collection always precedes the subcollections nested beneath it.

A schemaless engine (Firestore, OpenVaultDB, dalgo2memory) never needs this — a collection springs into existence on first write. A schema-first engine does: inGitDB refuses a write to a collection that has no definition on disk, and a nested path must be declared as a subcollection of its root. The dynamic segments in a real path (a machine id, an identity digest, a generation) are document ids, not collection names, so the shapes here are the complete and finite set.

Keep this in step with the collection constants and path helpers in installation_store.go, repository_event_store.go, machine_credential_store.go, machine_snapshot_store.go, poll_observation_store.go and peer_store.go; the store tests assert every one of them is listed.

func MachineID added in v0.124.5

func MachineID(identityID, machineName string) string

MachineID derives the stable machine document id from the identity and the machine name. A self-hosting daemon needs it to recognise the machine it already enrolled without keeping a second record of the id.

func NewHandler

func NewHandler(options HandlerOptions) http.Handler

func NewInstallationStores

NewInstallationStores wires the provider-owned installation stores to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. The returned binding store also implements RepositoryEntitlementResolver and InstallationLifecycleStore, so the same value can be assigned to every matching field on InstallationConnectionService, RepositoryEventService, and StatusService.

func NewMachineStores added in v0.124.5

NewMachineStores wires the hub-owned machine stores to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. One value satisfies both MachineCredentialStore and MachineCredentialResolver, so the same backing records answer enrollment and bearer resolution.

The returned values are what MachineEnrollmentService.Store, NewMachineBearerResolver and MachineSnapshotService.Store expect, which is every persistence a loopback hub needs before an App is involved.

func NewPeerStores added in v0.150.1

func NewPeerStores(backend githubapp.DocumentStore) (PeerTrustStore, PeerStatsStore)

NewPeerStores wires PeerTrustStore and PeerStatsStore to a host's api/githubapp DocumentStore, the same port every other hub store uses.

func NewRepositoryEventStore

func NewRepositoryEventStore(backend githubapp.DocumentStore) (RepositoryEventStore, RepositoryEventStatusStore)

NewRepositoryEventStore wires the provider-owned repository event store to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. The returned value implements both RepositoryEventStore and RepositoryEventStatusStore.

func RetryAfter added in v0.147.0

func RetryAfter(header http.Header) (time.Duration, bool)

RetryAfter parses GitHub's Retry-After response header, in seconds. GitHub sends it on some rate-limited and abuse-detection responses, sometimes instead of the X-RateLimit-* headers above and sometimes alongside them; a caller that wants to honor both prefers this one when present.

Types

type AppInstallationVerifier

type AppInstallationVerifier interface {
	VerifyAppInstallation(context.Context, int64) (VerifiedInstallation, error)
}

type ArtifactDownloader added in v0.171.0

type ArtifactDownloader interface {
	DownloadWorkflowArtifact(ctx context.Context, installationID int64, owner, repo string, runID int64, artifactName string) ([]byte, error)
}

ArtifactDownloader fetches build artifact archive bytes from GitHub.

type CoverageHarvester added in v0.171.0

type CoverageHarvester interface {
	HarvestWorkflowRun(ctx context.Context, delivery WebhookDelivery) error
}

CoverageHarvester processes workflow_run webhook events and harvests published artifacts.

type EnqueueResult

type EnqueueResult struct {
	Enqueued  int  `json:"enqueued"`
	Duplicate bool `json:"duplicate"`
}

type EnrolledIdentity

type EnrolledIdentity struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name,omitempty"`
}

type EnrolledMachine

type EnrolledMachine struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type FormatType added in v0.172.0

type FormatType string

FormatType defines formatting rules for a metric value.

const (
	FormatPercentage FormatType = "percentage"
	FormatInteger    FormatType = "integer"
	FormatFloat      FormatType = "float"
	FormatDuration   FormatType = "duration"
)

type GitHubAppInstallationVerifier

type GitHubAppInstallationVerifier struct {
	Client        *http.Client
	AppID         int64
	PrivateKeyPEM []byte
	APIBaseURL    string
	Now           func() time.Time
}

func (GitHubAppInstallationVerifier) VerifyAppInstallation

func (verifier GitHubAppInstallationVerifier) VerifyAppInstallation(ctx context.Context, installationID int64) (VerifiedInstallation, error)

type GitHubIdentityVerifier

type GitHubIdentityVerifier interface {
	ExchangeGitHubOAuthCode(context.Context, string) (string, error)
	VerifyGitHubIdentity(context.Context, string) (VerifiedGitHubIdentity, error)
}

type GitHubOAuthVerifier

type GitHubOAuthVerifier struct {
	Client       *http.Client
	ClientID     string
	ClientSecret string
	CallbackURL  string
	OAuthBaseURL string
	APIBaseURL   string
}

func (GitHubOAuthVerifier) ExchangeGitHubOAuthCode

func (verifier GitHubOAuthVerifier) ExchangeGitHubOAuthCode(ctx context.Context, code string) (string, error)

func (GitHubOAuthVerifier) Validate

func (verifier GitHubOAuthVerifier) Validate() error

func (GitHubOAuthVerifier) VerifyGitHubIdentity

func (verifier GitHubOAuthVerifier) VerifyGitHubIdentity(ctx context.Context, token string) (VerifiedGitHubIdentity, error)

type HTTPArtifactDownloader added in v0.171.0

type HTTPArtifactDownloader struct {
	Client      *http.Client
	APIBaseURL  string
	TokenSource func(ctx context.Context, installationID int64) (string, error)
}

HTTPArtifactDownloader fetches artifacts via the GitHub REST API.

func (HTTPArtifactDownloader) DownloadWorkflowArtifact added in v0.171.0

func (d HTTPArtifactDownloader) DownloadWorkflowArtifact(ctx context.Context, installationID int64, owner, repo string, runID int64, artifactName string) ([]byte, error)

type HandlerOptions

type HandlerOptions struct {
	ViewerResolver    ViewerResolver
	MachineBearer     MachineBearerResolver
	Enrollment        *MachineEnrollmentService
	Snapshots         *MachineSnapshotService
	Installations     *InstallationConnectionService
	RepositoryEvents  *RepositoryEventService
	Status            *StatusService
	Coverage          RepositoryCoverageStore
	Metrics           RepositoryMetricsStore
	CoverageHarvester CoverageHarvester
	Projection        ProjectionProcessor
	WebhookSecret     []byte
	AllowedOrigin     string
	// Narrate receives one line for every delivery the handler itself
	// rejects, written before the response to GitHub is sent. Deliveries the
	// handler accepts are narrated by RepositoryEventService instead, so each
	// delivery produces exactly one line. Optional.
	Narrate func(narrate.Line)
	// DisableSelfHostedEnrollment unmounts POST MachineEnrollmentPath
	// (peer-connectivity#req:admin-requires-owner-credential): a self-hosted
	// hub's loopback listener is reachable through a tunnel or proxy, which is
	// not proof of the local operator, so self-hosted enrollment moves to the
	// daemon's owner-token RPC service instead. The zero value (false) mounts
	// the route exactly as before, which is what the hosted instance's own
	// OAuth-viewer-gated deployment keeps doing without changing a line here.
	DisableSelfHostedEnrollment bool
}

type IdentityInstallationBinding

type IdentityInstallationBinding struct {
	IdentityID   string               `firestore:"identity_id"`
	GitHubUserID int64                `firestore:"github_user_id"`
	GitHubLogin  string               `firestore:"github_login"`
	Installation VerifiedInstallation `firestore:"installation"`
	VerifiedAt   time.Time            `firestore:"verified_at"`
}

type InstallationBindingStore

type InstallationBindingStore interface {
	IdentityHasInstallation(context.Context, string, int64) (bool, error)
	// CompleteIdentityInstallationBinding atomically verifies and consumes the
	// pending OAuth state and upserts only the explicitly selected installation.
	// It must reject a different GitHub user when the Workbench identity already
	// has bindings. A replay or state mismatch returns ErrInvalidInstallationState.
	CompleteIdentityInstallationBinding(context.Context, InstallationStateDigest, InstallationState, time.Time, IdentityInstallationBinding) error
	ListIdentityInstallationBindings(context.Context, string) ([]IdentityInstallationBinding, error)
}

type InstallationConnectRequest

type InstallationConnectRequest struct {
	InstallationID int64 `json:"installation_id,omitempty"`
}

type InstallationConnectResponse

type InstallationConnectResponse struct {
	ConnectURL string    `json:"connect_url"`
	ExpiresAt  time.Time `json:"expires_at"`
}

type InstallationConnectionService

type InstallationConnectionService struct {
	States            InstallationStateStore
	AppVerifier       AppInstallationVerifier
	OAuthVerifier     GitHubIdentityVerifier
	Bindings          InstallationBindingStore
	InstallURL        string
	OAuthAuthorizeURL string
	OAuthClientID     string
	OAuthCallbackURL  string
	SuccessURL        string
	StateSecret       []byte
	StateLifetime     time.Duration
	Random            io.Reader
	Now               func() time.Time
}

func (InstallationConnectionService) AuthorizeOpener

func (InstallationConnectionService) Begin

func (InstallationConnectionService) CompleteOAuth

func (service InstallationConnectionService) CompleteOAuth(ctx context.Context, state, code, browserContinuation, oauthCredential string) (InstallationRedirect, error)

func (InstallationConnectionService) CompleteSetup

func (service InstallationConnectionService) CompleteSetup(ctx context.Context, state string, installationID int64, browserContinuation string) (InstallationRedirect, error)

func (InstallationConnectionService) Continue

func (service InstallationConnectionService) Continue(ctx context.Context, state, openerChallenge string) (InstallationContinuation, error)

type InstallationContinuation

type InstallationContinuation struct {
	RedirectURL string
	ExpiresAt   time.Time
	// contains filtered or unexported fields
}

type InstallationLifecycleAction

type InstallationLifecycleAction string
const (
	InstallationSuspended           InstallationLifecycleAction = "suspended"
	InstallationRevoked             InstallationLifecycleAction = "revoked"
	InstallationRepositoriesRemoved InstallationLifecycleAction = "repositories_removed"
	InstallationUserAccessRemoved   InstallationLifecycleAction = "user_access_removed"
	GitHubUserAuthorizationRevoked  InstallationLifecycleAction = "github_user_authorization_revoked"
	RepositoryUserAccessRemoved     InstallationLifecycleAction = "repository_user_access_removed"
)

type InstallationLifecycleEvent

type InstallationLifecycleEvent struct {
	DeliveryID     string                      `firestore:"delivery_id"`
	Action         InstallationLifecycleAction `firestore:"action"`
	InstallationID int64                       `firestore:"installation_id"`
	RepositoryIDs  []int64                     `firestore:"repository_ids,omitempty"`
	RepositoryID   int64                       `firestore:"repository_id,omitempty"`
	GitHubUserID   int64                       `firestore:"github_user_id,omitempty"`
}

type InstallationLifecycleStore

type InstallationLifecycleStore interface {
	// ApplyInstallationLifecycle is idempotent by DeliveryID and atomically
	// removes or disables every affected entitlement before it returns success.
	// A GitHubUserAuthorizationRevoked event applies to every binding for that
	// GitHub user; RepositoryUserAccessRemoved applies only to its exact
	// installation, repository, and GitHub user tuple.
	ApplyInstallationLifecycle(context.Context, InstallationLifecycleEvent) error
}

type InstallationOpenerAuthorizationRequest

type InstallationOpenerAuthorizationRequest struct {
	State     string `json:"state"`
	Challenge string `json:"challenge"`
}

type InstallationRedirect

type InstallationRedirect struct {
	RedirectURL string
	ExpiresAt   time.Time
	// contains filtered or unexported fields
}

type InstallationState

type InstallationState struct {
	Kind                       installationStateKind `firestore:"kind"`
	IdentityID                 string                `firestore:"identity_id"`
	InstallationID             int64                 `firestore:"installation_id,omitempty"`
	BrowserContinuationDigest  string                `firestore:"browser_continuation_digest"`
	OpenerChallengeDigest      string                `firestore:"opener_challenge_digest,omitempty"`
	OpenerChallengeExpiresAt   time.Time             `firestore:"opener_challenge_expires_at,omitempty"`
	OpenerAuthorizedAt         time.Time             `firestore:"opener_authorized_at,omitempty"`
	OAuthAccessTokenCiphertext string                `firestore:"oauth_access_token_ciphertext,omitempty"`
	IssuedAt                   time.Time             `firestore:"issued_at"`
	ExpiresAt                  time.Time             `firestore:"expires_at"`
}

type InstallationStateDigest

type InstallationStateDigest [sha256.Size]byte

type InstallationStateStore

type InstallationStateStore interface {
	// IssueInstallationState rejects an existing digest so a random-state
	// collision cannot overwrite another pending authorization.
	IssueInstallationState(context.Context, InstallationStateDigest, InstallationState) error
	// ReadInstallationState returns a pending, unexpired state without consuming
	// it. Store availability errors must remain distinguishable from
	// ErrInvalidInstallationState so callers can safely retry transient failures.
	ReadInstallationState(context.Context, InstallationStateDigest, time.Time) (InstallationState, error)
	// TransitionInstallationState atomically verifies current against the stored
	// pending state, consumes it, and issues next. A replay, expired state,
	// mismatch, or next-digest collision returns ErrInvalidInstallationState.
	TransitionInstallationState(context.Context, InstallationStateDigest, InstallationState, time.Time, InstallationStateDigest, InstallationState) error
	// ReplaceInstallationState atomically verifies current against the stored
	// pending state and replaces it under the same digest. It is used to retain
	// an encrypted exchanged OAuth credential before any fallible post-exchange
	// reads. A replay, expiry, or mismatch returns ErrInvalidInstallationState.
	ReplaceInstallationState(context.Context, InstallationStateDigest, InstallationState, time.Time, InstallationState) error
}

type Machine

type Machine struct {
	ID         string
	Name       string
	IdentityID string
	Scopes     []MachineScope
}

type MachineBearerResolver

type MachineBearerResolver interface {
	ResolveMachineBearer(*http.Request) (Machine, error)
}

func NewMachineBearerResolver

func NewMachineBearerResolver(credentials MachineCredentialResolver, pepper []byte) MachineBearerResolver

func NewPeerAwareBearerResolver added in v0.150.1

func NewPeerAwareBearerResolver(inner MachineBearerResolver, trust PeerTrustResolver) MachineBearerResolver

NewPeerAwareBearerResolver returns a MachineBearerResolver that refuses a blocked peer's credential after delegating ordinary resolution to inner. A nil trust resolver makes this a pass-through, so a caller that has not wired peer storage yet (or a hosted deployment that never will) keeps exactly today's behaviour.

type MachineCredentialBinding

type MachineCredentialBinding struct {
	MachineID           string         `firestore:"machine_id"`
	MachineName         string         `firestore:"machine_name"`
	IdentityID          string         `firestore:"identity_id"`
	IdentityDisplayName string         `firestore:"identity_display_name,omitempty"`
	IssuedAt            time.Time      `firestore:"issued_at"`
	Scopes              []MachineScope `firestore:"scopes"`
}

type MachineCredentialResolver

type MachineCredentialResolver interface {
	ResolveMachineCredential(context.Context, MachineTokenDigest) (MachineCredentialBinding, error)
}

type MachineCredentialStore

type MachineCredentialStore interface {
	RotateMachineCredential(context.Context, MachineCredentialBinding, MachineTokenDigest) (MachineCredentialBinding, error)
}

type MachineEnrollmentRequest

type MachineEnrollmentRequest struct {
	Name string `json:"name"`
}

type MachineEnrollmentResponse

type MachineEnrollmentResponse struct {
	Machine    EnrolledMachine  `json:"machine"`
	Identity   EnrolledIdentity `json:"identity"`
	Token      string           `json:"token"`
	EnrolledAt time.Time        `json:"enrolled_at"`
}

type MachineEnrollmentService

type MachineEnrollmentService struct {
	Store  MachineCredentialStore
	Pepper []byte
	Random io.Reader
	Now    func() time.Time
}

func (MachineEnrollmentService) Enroll

type MachineIndex added in v0.150.1

type MachineIndex interface {
	HasCredential(context.Context, string) (bool, error)
}

MachineIndex answers whether any credential — peer or not — already exists for a MachineID, without exposing the credential body. Invite uses it to refuse a name already held by a non-peer credential (peer-connectivity#req:invite-and-join): the peer trust store alone cannot tell "no peer" from "an enrolled machine that keeps its name".

func NewMachineIndex added in v0.150.1

func NewMachineIndex(backend githubapp.DocumentStore) MachineIndex

NewMachineIndex wires MachineIndex to the same enrollment collection machineCredentialStore writes, without exposing RotateMachineCredential or ResolveMachineCredential to a caller that only needs "does this name already have a credential".

type MachineScope

type MachineScope string
const (
	ScopeSnapshotPublish MachineScope = "machine_snapshot:publish"
	ScopeSnapshotRead    MachineScope = "machine_snapshot:read"
	ScopeEventsPoll      MachineScope = "repository_events:poll"
	ScopeEventsAck       MachineScope = "repository_events:ack"
	// ScopePeerSession is the sole scope a peer credential carries
	// (peer-connectivity#req:invite-and-join). A peer token is deliberately
	// refused by every route gated on the enrollment scopes above, so the
	// session it opens can never become a second consumer of its own queue.
	ScopePeerSession MachineScope = "peer:session"
)

type MachineSnapshotService

type MachineSnapshotService struct {
	Store MachineSnapshotStore
	Now   func() time.Time
}

func (MachineSnapshotService) List

func (MachineSnapshotService) Publish

type MachineSnapshotStore

type MachineSnapshotStore interface {
	StoreLatest(context.Context, StoredMachineSnapshot) (MachineSnapshotStoreResult, error)
	ListLatest(context.Context) ([]StoredMachineSnapshot, error)
}

type MachineSnapshotStoreResult

type MachineSnapshotStoreResult struct {
	Current StoredMachineSnapshot
	Updated bool
}

func ResolveLatestMachineSnapshot

func ResolveLatestMachineSnapshot(current *StoredMachineSnapshot, candidate StoredMachineSnapshot) (MachineSnapshotStoreResult, error)

type MachineTokenDigest

type MachineTokenDigest [sha256.Size]byte

func DigestMachineToken

func DigestMachineToken(token string, pepper []byte) (MachineTokenDigest, error)

type MetricDimension added in v0.172.0

type MetricDimension struct {
	Name           string         `json:"name"`
	Value          float64        `json:"value"`
	FormattedValue string         `json:"formatted_value"`
	Status         MetricStatus   `json:"status"`
	Details        map[string]any `json:"details,omitempty"`
}

MetricDimension represents a single slice along the primary dimension (e.g. package, date).

type MetricStatus added in v0.172.0

type MetricStatus string

MetricStatus represents the health evaluation for a metric.

const (
	StatusPassed  MetricStatus = "passed"
	StatusWarning MetricStatus = "warning"
	StatusFailed  MetricStatus = "failed"
	StatusNeutral MetricStatus = "neutral"
)

type MetricTypeDefinition added in v0.172.0

type MetricTypeDefinition struct {
	Type           string     `json:"type"`
	Title          string     `json:"title"`
	Description    string     `json:"description"`
	Unit           string     `json:"unit"`
	Format         FormatType `json:"format"`
	DimensionLabel string     `json:"dimension_label"`
	GoodThreshold  float64    `json:"good_threshold"`
	WarnThreshold  float64    `json:"warn_threshold"`
	HigherIsBetter bool       `json:"higher_is_better"`
}

MetricTypeDefinition defines the presentation and evaluation rules for a metric category.

func DefaultMetricTypes added in v0.172.0

func DefaultMetricTypes() []MetricTypeDefinition

DefaultMetricTypes returns standard built-in metric type definitions.

type PeerAdminService added in v0.150.1

type PeerAdminService struct {
	// Credentials, Index, Trust and Stats are the seams Block, Unblock,
	// Disconnect, resolvePeer and RefuseIfPeerNameCollision use. Invite does
	// not use Credentials: see Backend.
	Credentials MachineCredentialStore
	Index       MachineIndex
	Trust       PeerTrustStore
	Stats       PeerStatsStore
	// Backend is the raw document store Invite writes the credential, trust
	// and statistics documents against inside one UpdateAtomic transaction,
	// so a partial invite (a credential with no matching trust document, or
	// the reverse) can never be observed. DocumentStore's contract promises
	// serializable transaction semantics, so two concurrent invites of the
	// same name always resolve to exactly one winner with a working token;
	// the loser's writes (including its own randomly minted token) are
	// discarded by the retried transaction rather than partially applied,
	// and it observes ErrPeerExists-shaped refusal instead.
	Backend githubapp.DocumentStore
	Pepper  []byte
	Random  io.Reader
	Now     func() time.Time
	// LocalIdentityID is the fixed self-hosted identity every peer credential
	// is minted under (hub.MachineID's first argument), matching
	// ensureLocalEnrollment's "local".
	LocalIdentityID string
	// HubMachineName is the hub's own machine name. invite-and-join refuses
	// it outright: the hub is never its own peer.
	HubMachineName string
	// MemoryEngine is true when the hub's store engine is "memory". Invite
	// refuses admission in that case, per invite-and-join: a peer credential
	// minted against a store that discards on exit could never be reasoned
	// about as durable state.
	MemoryEngine bool
}

PeerAdminService implements every admission and trust change peer-connectivity#req:admin-requires-owner-credential requires: invite, rotate, block, unblock and disconnect. It is deliberately not reachable from any HTTP route; cmd/wb mounts it only on the daemon's owner-token unix-socket RPC.

func (*PeerAdminService) Block added in v0.150.1

func (service *PeerAdminService) Block(ctx context.Context, nameOrID string) (PeerRecord, error)

Block closes the live session (a no-op until Task 2) and refuses future sessions and HTTP calls with that credential. The record, cursor and lifetime counters are unchanged.

func (*PeerAdminService) Disconnect added in v0.150.1

func (service *PeerAdminService) Disconnect(ctx context.Context, nameOrID string) (PeerDisconnectResult, error)

Disconnect closes the live session only; the peer stays trusted and reconnects by itself. Task 2 adds the session registry this needs; until then it answers "no live session" for any known peer, and an error for an unknown one.

func (*PeerAdminService) Invite added in v0.150.1

func (service *PeerAdminService) Invite(ctx context.Context, name string, rotate bool) (PeerInviteResult, error)

Invite mints a peer credential and its trust/statistics records. name must not already belong to the hub's own machine, an existing non-peer credential, or an existing peer unless rotate is true. The credential, trust and statistics documents are written inside one atomic transaction: see Backend's doc comment for why.

func (*PeerAdminService) RefuseIfPeerNameCollision added in v0.150.1

func (service *PeerAdminService) RefuseIfPeerNameCollision(ctx context.Context, name string) error

RefuseIfPeerNameCollision reports whether name is off-limits to the owner RPC's plain (non-peer) "enroll" route: the hub's own machine name, or a name already held by a peer trust document. It does not apply to ensureLocalEnrollment's own self-bootstrap, which enrolls the hub's own machine name under the "local" identity by design and must keep doing so.

func (*PeerAdminService) Unblock added in v0.150.1

func (service *PeerAdminService) Unblock(ctx context.Context, nameOrID string) (PeerRecord, error)

Unblock allows sessions again and sets reset_pending, so the peer's next redial reconciles from the hub's heads instead of resuming a cursor that may have gone stale while it was refused.

type PeerDisconnectResult added in v0.150.1

type PeerDisconnectResult struct {
	Peer         PeerRecord
	Disconnected bool
	Message      string
}

PeerDisconnectResult is what `wb peers disconnect` reports. Until Task 2 adds the live-session registry, there is never a live session to close, so Disconnected is always false and Message says so plainly.

type PeerInviteResult added in v0.150.1

type PeerInviteResult struct {
	PeerID    string
	Name      string
	Token     string
	CreatedAt time.Time
	Rotated   bool
}

PeerInviteResult is what `wb peers invite` and the owner RPC return: the token is present exactly once, here, and never retrievable again.

type PeerRecord added in v0.150.1

type PeerRecord struct {
	MachineID      string    `firestore:"machine_id"`
	Name           string    `firestore:"name"`
	IdentityID     string    `firestore:"identity_id"`
	NodeID         string    `firestore:"node_id,omitempty"`
	Trust          PeerTrust `firestore:"trust"`
	CreatedAt      time.Time `firestore:"created_at"`
	TrustChangedAt time.Time `firestore:"trust_changed_at"`
	ResetPending   bool      `firestore:"reset_pending"`
}

PeerRecord is the trust document: display name and owning identity, the bound node ID (empty until Task 2 binds it), trust state, created and trust-changed times, and reset_pending. Only admin operations (hub.PeerAdminService) write it, as transactional field merges.

type PeerStats added in v0.150.1

type PeerStats struct {
	MachineID        string    `firestore:"machine_id"`
	LastSeenAt       time.Time `firestore:"last_seen_at,omitempty"`
	LastConnectedAt  time.Time `firestore:"last_connected_at,omitempty"`
	WBVersion        string    `firestore:"wb_version,omitempty"`
	OS               string    `firestore:"os,omitempty"`
	Arch             string    `firestore:"arch,omitempty"`
	Protocol         int       `firestore:"protocol,omitempty"`
	RXPayloadBytes   uint64    `firestore:"rx_payload_bytes"`
	TXPayloadBytes   uint64    `firestore:"tx_payload_bytes"`
	RXMessages       uint64    `firestore:"rx_messages"`
	TXMessages       uint64    `firestore:"tx_messages"`
	RXEvents         uint64    `firestore:"rx_events"`
	TXEvents         uint64    `firestore:"tx_events"`
	Connections      uint64    `firestore:"connections"`
	ConnectedSeconds uint64    `firestore:"connected_seconds"`
}

PeerStats is the statistics document: reported version/platform/protocol and lifetime counters. Every counter is zero until Task 6 fills it; this task only creates the document at invite time, so later writers have somewhere to merge into.

type PeerStatsStore added in v0.150.1

type PeerStatsStore interface {
	// CreateStats writes a new, all-zero statistics document for machineID.
	// It is idempotent: an existing document is left untouched rather than
	// reset, so a later writer's progress survives a repeated invite.
	CreateStats(context.Context, string) error
	// GetStats reads the statistics document. found is false when none
	// exists yet.
	GetStats(context.Context, string) (stats PeerStats, found bool, err error)
}

PeerStatsStore is the statistics document's persistence seam.

type PeerTrust added in v0.150.1

type PeerTrust string

PeerTrust is the one bit of trust state a peer record carries: active peers are admitted, blocked peers are refused everywhere a credential is resolved.

const (
	PeerTrustActive  PeerTrust = "active"
	PeerTrustBlocked PeerTrust = "blocked"
)

type PeerTrustResolver added in v0.150.1

type PeerTrustResolver interface {
	GetPeer(context.Context, string) (record PeerRecord, found bool, err error)
}

PeerTrustResolver is the read the peer-aware bearer resolver needs: does a peer record exist for this MachineID, and is it blocked. It is the narrow slice of PeerTrustStore that authentication depends on.

type PeerTrustStore added in v0.150.1

type PeerTrustStore interface {
	// CreatePeer writes a new trust document. It fails with ErrPeerExists if
	// one already exists for record.MachineID.
	CreatePeer(context.Context, PeerRecord) error
	// GetPeer reads the trust document by MachineID. found is false, with a
	// nil error, when no peer record exists — the credential is not a peer.
	GetPeer(context.Context, string) (record PeerRecord, found bool, err error)
	// FindPeerByName reads the trust document by display name, for `<peer>`
	// arguments and invite's existing-name refusal.
	FindPeerByName(context.Context, string) (record PeerRecord, found bool, err error)
	// ListPeers returns every peer trust document, for `wb peers list`.
	ListPeers(context.Context) ([]PeerRecord, error)
	// UpdateTrust reads the current record inside a transaction, applies
	// mutate, and writes the result back — the "transactional field merge"
	// every admin write is. mutate must not change MachineID, Name,
	// IdentityID, or CreatedAt; it returns ErrPeerNotFound when machineID has
	// no trust document.
	UpdateTrust(ctx context.Context, machineID string, mutate func(*PeerRecord)) (PeerRecord, error)
}

PeerTrustStore is the trust document's persistence seam, backed by the same DocumentStore port every other hub store uses.

type PendingRefresh

type PendingRefresh struct {
	ID             string    `json:"id"`
	Repository     string    `json:"repository"`
	Event          string    `json:"event,omitempty"`
	QueuedAt       time.Time `json:"queued_at"`
	InstallationID string    `json:"installation_id,omitempty"`
	MachineID      string    `json:"machine_id,omitempty"`
}

type PollObservation added in v0.124.6

type PollObservation struct {
	// Repository is the canonical `github.com/owner/repository` this
	// observation is keyed by, lowercased the way machine snapshots are.
	Repository string `firestore:"repository"`
	// FullName is `owner/repository` exactly as GitHub reported it, so a
	// rename is detected against what the API said rather than against a
	// normalisation of it.
	FullName string `firestore:"full_name"`
	// DefaultBranch is the branch name GitHub reported, without a ref prefix.
	DefaultBranch string `firestore:"default_branch"`
	// SHA is the object ID at the head of DefaultBranch.
	SHA string `firestore:"sha"`
	// ObservedAt is when the poller read those values.
	ObservedAt time.Time `firestore:"observed_at"`
}

PollObservation is the last state the polling ingester saw for one repository. It is the poller's entire memory: without it every restart would re-enqueue the current head as though it had just been pushed.

Only the three fields the poller compares are kept. Nothing here is a secret: a repository's name, its default branch, and the object ID at that branch's head are all already part of the repository-event contract.

type PollObservationStore added in v0.124.6

type PollObservationStore interface {
	// LoadPollObservation returns the stored observation for a repository.
	// The boolean is false when the poller has never seen it, which is the
	// signal to record without enqueueing.
	LoadPollObservation(context.Context, string) (PollObservation, bool, error)
	// SavePollObservation replaces the observation for observation.Repository.
	SavePollObservation(context.Context, PollObservation) error
	// DeletePollObservation drops the observation a rename re-keyed away
	// from. Deleting one that is not there is not an error.
	DeletePollObservation(context.Context, string) error
}

PollObservationStore persists what the poller last saw, keyed by the canonical repository name.

func NewPollObservationStore added in v0.124.6

func NewPollObservationStore(backend githubapp.DocumentStore) PollObservationStore

NewPollObservationStore binds the poller's memory to a host's githubapp.DocumentStore, the same seam every other hub-owned store writes through.

type ProjectionProcessor

type ProjectionProcessor interface {
	ProcessProjection(context.Context, WebhookDelivery, string) error
}

ProjectionProcessor lets an existing dashboard projection subsystem consume the same already authenticated delivery while that subsystem is migrated.

type QueuedWorkStore added in v0.150.1

type QueuedWorkStore interface {
	// QueuedWork reads workbench_repository_event_queues/<machineID>'s
	// queued_work field. found is false when the machine has no queue-state
	// document yet — LAG then shows "-", not zero, since "no document" and
	// "a document reporting zero" are different facts once Task 3 starts
	// writing it.
	QueuedWork(ctx context.Context, machineID string) (count int64, found bool, err error)
}

QueuedWorkStore is the narrow read the peers read model needs: how much queued work a machine's queue holds, for `wb peers list`'s LAG column (peer-connectivity#req:peer-is-persistent-state, #req:peers-api).

func NewQueuedWorkStore added in v0.150.1

func NewQueuedWorkStore(backend githubapp.DocumentStore) QueuedWorkStore

NewQueuedWorkStore wires QueuedWorkStore to the same backend document store repositoryEventStore itself uses, reading the exact per-machine queue-state document Acknowledge above reads and writes.

type RateLimit added in v0.147.0

type RateLimit struct {
	Known     bool
	Remaining int
	Reset     time.Time
}

RateLimit is what GitHub's X-RateLimit-* response headers said about the remaining budget and its next reset. Known is false when neither header was present or parseable, which every caller treats as "nothing to act on" rather than a zero budget.

This is the one place that parsing exists: hub/poller and hub/redeliver both read GitHub REST responses and both need it, and a second copy would be exactly the kind of drift a shared seam is for.

func ReadRateLimit added in v0.147.0

func ReadRateLimit(header http.Header) RateLimit

ReadRateLimit parses the X-RateLimit-Remaining and X-RateLimit-Reset response headers GitHub's REST API sends on almost every response.

type RepositoryCoverageStore added in v0.171.0

type RepositoryCoverageStore interface {
	SaveCoverage(ctx context.Context, record StoredRepositoryCoverage) error
	GetCoverage(ctx context.Context, repository string) (StoredRepositoryCoverage, bool, error)
	ListCoverage(ctx context.Context) ([]StoredRepositoryCoverage, error)
}

RepositoryCoverageStore persists and lists repository test coverage reports.

func NewRepositoryCoverageStore added in v0.171.0

func NewRepositoryCoverageStore(backend githubapp.DocumentStore) RepositoryCoverageStore

NewRepositoryCoverageStore binds the repository coverage store to backend.

type RepositoryEntitlementResolver

type RepositoryEntitlementResolver interface {
	IdentityHasRepositoryEntitlement(context.Context, string, int64, int64) (bool, error)
}

type RepositoryEventService

type RepositoryEventService struct {
	Snapshots    MachineSnapshotStore
	Entitlements RepositoryEntitlementResolver
	Lifecycle    InstallationLifecycleStore
	Store        RepositoryEventStore
	PollInterval time.Duration
	Sleep        func(context.Context, time.Duration) error
	Now          func() time.Time
	// Narrate receives one line for every delivery the service decides about:
	// queued, ignored, dropped as a duplicate, or applied as a lifecycle
	// change. A delivery the service returns an error for is narrated by the
	// HTTP handler instead, so every delivery produces exactly one line.
	// Optional; the hosted instance leaves it unset.
	Narrate func(narrate.Line)
}

func (RepositoryEventService) Acknowledge

func (RepositoryEventService) EnqueueWebhook

func (service RepositoryEventService) EnqueueWebhook(ctx context.Context, delivery WebhookDelivery) (EnqueueResult, error)

func (RepositoryEventService) Poll

func (service RepositoryEventService) Poll(ctx context.Context, machine Machine, cursor string, limit int, wait time.Duration) (repositoryevent.PollResponse, error)

type RepositoryEventStatusStore

type RepositoryEventStatusStore interface {
	IdentityRepositoryEventStatus(context.Context, string) (*StatusDelivery, []PendingRefresh, []StatusError, error)
}

type RepositoryMetric added in v0.172.0

type RepositoryMetric struct {
	Repository     string            `json:"repository"`
	Owner          string            `json:"owner"`
	Name           string            `json:"name"`
	MetricType     string            `json:"metric_type"`
	ReportedAt     time.Time         `json:"reported_at"`
	Ref            string            `json:"ref,omitempty"`
	SHA            string            `json:"sha,omitempty"`
	Value          float64           `json:"value"`
	FormattedValue string            `json:"formatted_value"`
	Status         MetricStatus      `json:"status"`
	Metadata       map[string]any    `json:"metadata,omitempty"`
	Dimensions     []MetricDimension `json:"dimensions,omitempty"`
}

RepositoryMetric represents a point-in-time multi-dimensional metric for a repository.

func CoverageToRepositoryMetric added in v0.172.0

func CoverageToRepositoryMetric(record StoredRepositoryCoverage) RepositoryMetric

CoverageToRepositoryMetric converts a StoredRepositoryCoverage record into a generic RepositoryMetric.

type RepositoryMetricsStore added in v0.172.0

type RepositoryMetricsStore interface {
	SaveMetric(ctx context.Context, metric RepositoryMetric) error
	GetMetric(ctx context.Context, repository, metricType string) (RepositoryMetric, bool, error)
	ListMetrics(ctx context.Context, metricType string) ([]RepositoryMetric, error)
	ListMetricTypes(ctx context.Context) ([]MetricTypeDefinition, error)
}

RepositoryMetricsStore provides persistent storage and querying for repository metrics.

func NewRepositoryMetricsStore added in v0.172.0

func NewRepositoryMetricsStore(backend githubapp.DocumentStore, coverageStore RepositoryCoverageStore) RepositoryMetricsStore

NewRepositoryMetricsStore creates a new generic metrics store backed by DALgo and optional coverage store adapter.

type StatusConnection

type StatusConnection struct {
	State      string `json:"state"`
	AppName    string `json:"app_name,omitempty"`
	Account    string `json:"account,omitempty"`
	ConnectURL string `json:"connect_url,omitempty"`
}

type StatusDelivery

type StatusDelivery struct {
	LastReceived     *StatusDeliveryMarker `json:"last_received,omitempty"`
	LastAcknowledged *StatusDeliveryMarker `json:"last_acknowledged,omitempty"`
}

type StatusDeliveryMarker

type StatusDeliveryMarker struct {
	DeliveryID string    `json:"delivery_id,omitempty"`
	Event      string    `json:"event,omitempty"`
	OccurredAt time.Time `json:"occurred_at"`
}

type StatusError

type StatusError struct {
	Code           string `json:"code"`
	Message        string `json:"message"`
	Action         string `json:"action,omitempty"`
	ActionURL      string `json:"action_url,omitempty"`
	InstallationID string `json:"installation_id,omitempty"`
}

type StatusInstallation

type StatusInstallation struct {
	ID                  string `json:"id"`
	Account             string `json:"account"`
	AccountType         string `json:"account_type,omitempty"`
	State               string `json:"state"`
	RepositorySelection string `json:"repository_selection,omitempty"`
	Repositories        int    `json:"repositories,omitempty"`
	ManageURL           string `json:"manage_url,omitempty"`
}

type StatusMachine

type StatusMachine struct {
	ID         string     `json:"id"`
	Name       string     `json:"name"`
	State      string     `json:"state,omitempty"`
	LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
}

type StatusResponse

type StatusResponse struct {
	GeneratedAt      time.Time            `json:"generated_at"`
	Connection       StatusConnection     `json:"connection"`
	Installations    []StatusInstallation `json:"installations"`
	Machines         []StatusMachine      `json:"machines"`
	Delivery         *StatusDelivery      `json:"delivery,omitempty"`
	PendingRefreshes []PendingRefresh     `json:"pending_refreshes"`
	Errors           []StatusError        `json:"errors"`
}

type StatusService

type StatusService struct {
	Bindings  InstallationBindingStore
	Snapshots MachineSnapshotStore
	Events    RepositoryEventStatusStore
	AppName   string
	Now       func() time.Time
}

func (StatusService) Read

func (service StatusService) Read(ctx context.Context, viewer Viewer) (StatusResponse, error)

type StoredMachineSnapshot

type StoredMachineSnapshot struct {
	IdentityID string                   `firestore:"identity_id"`
	MachineID  string                   `firestore:"machine_id"`
	Snapshot   machinesnapshot.Snapshot `firestore:"snapshot"`
	ReceivedAt time.Time                `firestore:"received_at"`
	Digest     string                   `firestore:"digest"`
}

type StoredRepositoryCoverage added in v0.171.0

type StoredRepositoryCoverage struct {
	Repository     string                            `json:"repository"`
	Owner          string                            `json:"owner"`
	Name           string                            `json:"name"`
	Ref            string                            `json:"ref"`
	SHA            string                            `json:"sha"`
	WorkflowRunID  int64                             `json:"workflow_run_id,omitempty"`
	WorkflowRunURL string                            `json:"workflow_run_url,omitempty"`
	ReportedAt     time.Time                         `json:"reported_at"`
	Status         quality.Status                    `json:"status"`
	Statements     int                               `json:"statements"`
	Covered        int                               `json:"covered"`
	Percentage     float64                           `json:"percentage"`
	Modules        []quality.ModuleCoverageSummary   `json:"modules,omitempty"`
	Packages       map[string]quality.PackageSummary `json:"packages,omitempty"`
}

StoredRepositoryCoverage is the persisted coverage record for one repository.

type VerifiedGitHubIdentity

type VerifiedGitHubIdentity struct {
	UserID        int64                  `json:"user_id"`
	Login         string                 `json:"login"`
	Installations []VerifiedInstallation `json:"installations"`
}

func (VerifiedGitHubIdentity) Validate

func (identity VerifiedGitHubIdentity) Validate() error

type VerifiedInstallation

type VerifiedInstallation struct {
	ID                  int64                `json:"id" firestore:"id"`
	Account             string               `json:"account" firestore:"account"`
	AccountType         string               `json:"account_type,omitempty" firestore:"account_type,omitempty"`
	RepositorySelection string               `json:"repository_selection" firestore:"repository_selection"`
	Repositories        []VerifiedRepository `json:"repositories" firestore:"repositories"`
	State               string               `json:"state,omitempty" firestore:"state,omitempty"`
	ManageURL           string               `json:"manage_url,omitempty" firestore:"manage_url,omitempty"`
}

type VerifiedRepository

type VerifiedRepository struct {
	ID         int64  `json:"id" firestore:"id"`
	Repository string `json:"repository" firestore:"repository"`
}

type Viewer

type Viewer struct {
	Authenticated bool
	IdentityID    string
	DisplayName   string
}

type ViewerResolver

type ViewerResolver interface {
	Viewer(*http.Request) (Viewer, error)
}

type WebhookDelivery

type WebhookDelivery struct {
	ID         string
	Event      string
	Repository string
	Payload    []byte
}

type WebhookRedeliveryRecord added in v0.147.0

type WebhookRedeliveryRecord struct {
	// GUID is the delivery GUID GitHub keeps stable across every attempt and
	// every redelivery of one logical delivery.
	GUID string `firestore:"guid"`
	// Attempts is how many times this hub has asked GitHub to redeliver this
	// GUID. It never exceeds MaxAttempts in the redeliver package.
	Attempts int `firestore:"attempts"`
	// Abandoned is set once Attempts has been exhausted with the delivery
	// still failing. An abandoned GUID is never retried again.
	Abandoned bool `firestore:"abandoned"`
	// LastAttemptAt is when this record was last written, whether by a
	// redelivery or by being marked abandoned. It is the retention clock.
	LastAttemptAt time.Time `firestore:"last_attempt_at"`
	// LastAttemptDeliveryID is the GitHub delivery attempt id that was this
	// GUID's latest listed attempt the last time this hub acted on it. A
	// later sweep counts a redelivery attempt against this GUID only when
	// its now-latest listed attempt was answered and carries a different id
	// than this one: an id comparison, not a timestamp comparison, because
	// GitHub's delivered_at has whole-second resolution on GitHub's own
	// clock while this hub's own attempt time is local and sub-second, so a
	// fast redeliver round trip routinely lands in the same GitHub-clock
	// second as the request that caused it.
	LastAttemptDeliveryID int64 `firestore:"last_attempt_delivery_id"`
	// FirstDeliveredAt is the delivered_at of this GUID's earliest attempt
	// this hub has seen, set once when the record is first created and never
	// overwritten after. Because every uncounted redelivery creates a new
	// attempt with a fresh delivered_at, the GUID's own latest attempt never
	// ages out of the App API's listing on its own; the 72-hour bound in
	// spec/features/peer-connectivity/README.md is measured from this field,
	// not from LastAttemptAt, so an unreachable endpoint cannot keep a GUID
	// alive forever.
	FirstDeliveredAt time.Time `firestore:"first_delivered_at"`
}

WebhookRedeliveryRecord is what the hub remembers about one App webhook delivery GUID across missed-webhook recovery sweeps: how many times this hub has already asked GitHub to redeliver it, and whether it has given up. Nothing here is a secret or a payload: a GUID, a small counter and a timestamp are all a redelivery decision needs.

type WebhookRedeliveryStore added in v0.147.0

type WebhookRedeliveryStore interface {
	// LoadWebhookRedelivery returns the stored record for a GUID. The boolean
	// is false when the sweep has never acted on it, which is the signal to
	// treat it as zero prior attempts.
	LoadWebhookRedelivery(ctx context.Context, guid string) (WebhookRedeliveryRecord, bool, error)
	// SaveWebhookRedelivery replaces the record for record.GUID.
	SaveWebhookRedelivery(ctx context.Context, record WebhookRedeliveryRecord) error
	// ListWebhookRedeliveries returns every stored record, for the retention
	// sweep to filter by age.
	ListWebhookRedeliveries(ctx context.Context) ([]WebhookRedeliveryRecord, error)
	// DeleteWebhookRedeliveries removes the named GUIDs' records. Deleting a
	// GUID that is not there is not an error. Callers must keep each call
	// within the store's transaction write bound.
	DeleteWebhookRedeliveries(ctx context.Context, guids []string) error
}

WebhookRedeliveryStore persists the missed-webhook recovery sweep's memory, keyed by GitHub's delivery GUID, with the 7-day retention the feature specifies.

func NewWebhookRedeliveryStore added in v0.147.0

func NewWebhookRedeliveryStore(backend githubapp.DocumentStore) WebhookRedeliveryStore

NewWebhookRedeliveryStore binds the missed-webhook recovery sweep's memory to a host's githubapp.DocumentStore, the same seam every other hub-owned store writes through.

type WorkflowRunHarvester added in v0.171.0

type WorkflowRunHarvester struct {
	Store      RepositoryCoverageStore
	Downloader ArtifactDownloader
	Now        func() time.Time
	Narrate    func(narrate.Line)
}

WorkflowRunHarvester extracts and persists coverage summaries from successful default-branch workflow runs.

func (WorkflowRunHarvester) HarvestWorkflowRun added in v0.171.0

func (h WorkflowRunHarvester) HarvestWorkflowRun(ctx context.Context, delivery WebhookDelivery) error

Directories

Path Synopsis
Package narrate renders the one console line the hub writes for every event it handles, whether the event arrived by webhook or was produced by the poller.
Package narrate renders the one console line the hub writes for every event it handles, whether the event arrived by webhook or was produced by the poller.
Package poller is the ingester a self-hosted bench needs by default.
Package poller is the ingester a self-hosted bench needs by default.
Package redeliver recovers GitHub App webhook deliveries the hub's own downtime lost.
Package redeliver recovers GitHub App webhook deliveries the hub's own downtime lost.
Package web embeds the built bench dashboard so a self-hosted hub serves the same pages the hosted instance does, with no Node runtime and no configuration.
Package web embeds the built bench dashboard so a self-hosted hub serves the same pages the hosted instance does, with no Node runtime and no configuration.

Jump to

Keyboard shortcuts

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