github

package
v0.0.0-...-17c6387 Latest Latest
Warning

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

Go to latest
Published: Apr 12, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

View Source
const IssueDetailWorstCase = 2

IssueDetailWorstCase is the maximum API calls an issue detail fetch can make (detail + comments).

View Source
const PRDetailWorstCase = 8

PRDetailWorstCase is the maximum API calls a PR detail fetch can make (detail + GetUser + comments + reviews + commits + force-push events + combined status + check runs).

View Source
const RateReserveBuffer = 200

Variables

This section is empty.

Functions

func DeriveOverallCIStatus

func DeriveOverallCIStatus(
	runs []*gh.CheckRun,
	combined *gh.CombinedStatus,
) string

DeriveOverallCIStatus computes an aggregate CI status from check runs and the legacy combined status API. The combined status API only reports on commit statuses (the older mechanism); repos using only GitHub Actions check runs will have an empty or "pending" combined state even when all checks pass. This function merges both sources to produce the correct overall status.

func DeriveReviewDecision

func DeriveReviewDecision(reviews []*gh.PullRequestReview) string

DeriveReviewDecision computes the aggregate review decision from a list of reviews. It keeps the latest APPROVED or CHANGES_REQUESTED review per user. Returns "changes_requested" if any user has that state, "approved" if at least one approval exists, or "" if no actionable reviews are present.

func FilterWorkflowRunsAwaitingApproval

func FilterWorkflowRunsAwaitingApproval(
	runs []*gh.WorkflowRun,
	number int,
	headSHA string,
) []*gh.WorkflowRun

FilterWorkflowRunsAwaitingApproval narrows action-required workflow runs down to those that target the given PR number and head SHA.

func IsNotModified

func IsNotModified(err error) bool

IsNotModified returns true if the error represents a 304 Not Modified response from the GitHub API.

func NormalizeCIChecks

func NormalizeCIChecks(
	runs []*gh.CheckRun,
	combined *gh.CombinedStatus,
) string

NormalizeCIChecks merges check runs and commit statuses into a single JSON string of CICheck objects. Commit statuses (used by GitHub Apps like roborev) use the older status API and need to be mapped into the same shape as check runs.

func NormalizeCheckRuns

func NormalizeCheckRuns(runs []*gh.CheckRun) string

NormalizeCheckRuns converts GitHub check runs to a JSON string of CICheck objects.

func NormalizeCommentEvent

func NormalizeCommentEvent(mrID int64, c *gh.IssueComment) db.MREvent

NormalizeCommentEvent converts a GitHub IssueComment to a db.MREvent.

func NormalizeCommitEvent

func NormalizeCommitEvent(mrID int64, c *gh.RepositoryCommit) db.MREvent

NormalizeCommitEvent converts a GitHub RepositoryCommit to a db.MREvent. Author is taken from the GitHub user login if available, falling back to the git commit author name.

func NormalizeForcePushEvent

func NormalizeForcePushEvent(mrID int64, fp ForcePushEvent) db.MREvent

func NormalizeIssue

func NormalizeIssue(repoID int64, ghIssue *gh.Issue) *db.Issue

NormalizeIssue converts a GitHub Issue to a db.Issue.

func NormalizeIssueCommentEvent

func NormalizeIssueCommentEvent(issueID int64, c *gh.IssueComment) db.IssueEvent

NormalizeIssueCommentEvent converts a GitHub IssueComment to a db.IssueEvent.

func NormalizePR

func NormalizePR(repoID int64, ghPR *gh.PullRequest) *db.MergeRequest

NormalizePR converts a GitHub PullRequest to a db.MergeRequest. If the PR is merged, State is set to "merged". LastActivityAt is initialized to UpdatedAt.

func NormalizeReviewEvent

func NormalizeReviewEvent(mrID int64, r *gh.PullRequestReview) db.MREvent

NormalizeReviewEvent converts a GitHub PullRequestReview to a db.MREvent.

func WithSyncBudget

func WithSyncBudget(ctx context.Context) context.Context

WithSyncBudget marks a context so that HTTP calls made with it count against the sync budget. Background sync entry points (RunOnce, syncWatchedMRs) inject this; user-initiated server handler paths do not.

Types

type BulkIssue

type BulkIssue struct {
	Issue            *gh.Issue
	Comments         []*gh.IssueComment
	CommentsComplete bool
}

BulkIssue holds an issue and its nested comments from a single GraphQL query. CommentsComplete indicates whether the comments connection was fully paginated.

type BulkPR

type BulkPR struct {
	PR               *gh.PullRequest
	Comments         []*gh.IssueComment
	Reviews          []*gh.PullRequestReview
	Commits          []*gh.RepositoryCommit
	CheckRuns        []*gh.CheckRun
	Statuses         []*gh.RepoStatus
	CommentsComplete bool
	ReviewsComplete  bool
	CommitsComplete  bool
	CIComplete       bool
}

BulkPR holds a PR and its nested data from a single GraphQL query. The *Complete flags indicate whether each nested connection was fully paginated. When false, the data is partial and the detail drain should fill in via REST.

type Client

type Client interface {
	ListOpenPullRequests(ctx context.Context, owner, repo string) ([]*gh.PullRequest, error)
	GetPullRequest(ctx context.Context, owner, repo string, number int) (*gh.PullRequest, error)
	GetUser(ctx context.Context, login string) (*gh.User, error)
	ListOpenIssues(ctx context.Context, owner, repo string) ([]*gh.Issue, error)
	GetIssue(ctx context.Context, owner, repo string, number int) (*gh.Issue, error)
	ListIssueComments(ctx context.Context, owner, repo string, number int) ([]*gh.IssueComment, error)
	ListReviews(ctx context.Context, owner, repo string, number int) ([]*gh.PullRequestReview, error)
	ListCommits(ctx context.Context, owner, repo string, number int) ([]*gh.RepositoryCommit, error)
	ListForcePushEvents(ctx context.Context, owner, repo string, number int) ([]ForcePushEvent, error)
	GetCombinedStatus(ctx context.Context, owner, repo, ref string) (*gh.CombinedStatus, error)
	ListCheckRunsForRef(ctx context.Context, owner, repo, ref string) ([]*gh.CheckRun, error)
	ListWorkflowRunsForHeadSHA(ctx context.Context, owner, repo, headSHA string) ([]*gh.WorkflowRun, error)
	ApproveWorkflowRun(ctx context.Context, owner, repo string, runID int64) error
	CreateIssueComment(ctx context.Context, owner, repo string, number int, body string) (*gh.IssueComment, error)
	GetRepository(ctx context.Context, owner, repo string) (*gh.Repository, error)
	CreateReview(ctx context.Context, owner, repo string, number int, event string, body string) (*gh.PullRequestReview, error)
	MarkPullRequestReadyForReview(ctx context.Context, owner, repo string, number int) (*gh.PullRequest, error)
	MergePullRequest(ctx context.Context, owner, repo string, number int, commitTitle, commitMessage, method string) (*gh.PullRequestMergeResult, error)
	EditPullRequest(ctx context.Context, owner, repo string, number int, state string) (*gh.PullRequest, error)
	EditIssue(ctx context.Context, owner, repo string, number int, state string) (*gh.Issue, error)
	ListPullRequestsPage(ctx context.Context, owner, repo, state string, page int) ([]*gh.PullRequest, bool, error)
	ListIssuesPage(ctx context.Context, owner, repo, state string, page int) ([]*gh.Issue, bool, error)
	// InvalidateListETagsForRepo drops cached conditional-GET
	// validators for the given repo's list endpoints so the next
	// list call issues an unconditional fetch. The endpoints
	// parameter selects which caches to clear ("pulls", "issues").
	// If empty, both are cleared. Used to recover from a
	// partial-failure sync.
	InvalidateListETagsForRepo(owner, repo string, endpoints ...string)
}

Client is the interface for interacting with the GitHub API.

func NewClient

func NewClient(
	token string,
	platformHost string,
	rateTracker *RateTracker,
	budget *SyncBudget,
) (Client, error)

NewClient creates a GitHub Client authenticated with the given token. platformHost selects the API endpoint: "" or "github.com" uses the public API; any other value creates an Enterprise client. rateTracker and budget may be nil.

type DiffSyncError

type DiffSyncError struct {
	Code DiffSyncErrorCode
	Err  error
}

DiffSyncError reports a non-fatal failure to compute or update the diff SHAs for a PR. SyncMR returns this when only the diff portion of the sync failed: the PR row, timeline, and CI status were updated successfully, so callers should still treat the PR data as fresh, but the diff view will be stale or missing until the underlying problem is fixed.

Code categorizes the failure for client-facing messaging via UserMessage. Err preserves the underlying detail for server-side logging only — never expose Err.Error() to API clients, since it can contain clone paths, refs, SHAs, and git stderr.

func (*DiffSyncError) Error

func (e *DiffSyncError) Error() string

func (*DiffSyncError) Unwrap

func (e *DiffSyncError) Unwrap() error

func (*DiffSyncError) UserMessage

func (e *DiffSyncError) UserMessage() string

UserMessage returns a sanitized message safe to surface to API clients. It never includes clone paths, refs, SHAs, or other internal details from the underlying error.

type DiffSyncErrorCode

type DiffSyncErrorCode string

DiffSyncErrorCode categorizes the reason a diff sync failed. The frontend uses this category to render a user-facing message that does not leak local clone paths, refs, SHAs, or git stderr.

const (
	// DiffSyncCodeCloneUnavailable means the local bare clone could not be
	// created or updated (network failure, disk full, permission denied).
	DiffSyncCodeCloneUnavailable DiffSyncErrorCode = "clone_unavailable"
	// DiffSyncCodeCommitUnreachable means a commit needed to compute the diff
	// (PR head, merge commit, or its first parent) is not present in the local
	// clone and could not be fetched.
	DiffSyncCodeCommitUnreachable DiffSyncErrorCode = "commit_unreachable"
	// DiffSyncCodeMergeBaseFailed means git merge-base could not compute the
	// fork point between the PR head and the base.
	DiffSyncCodeMergeBaseFailed DiffSyncErrorCode = "merge_base_failed"
	// DiffSyncCodeInternal covers database failures and other unexpected
	// internal errors during diff computation.
	DiffSyncCodeInternal DiffSyncErrorCode = "internal"
)

type ForcePushEvent

type ForcePushEvent struct {
	Actor     string
	BeforeSHA string
	AfterSHA  string
	Ref       string
	CreatedAt time.Time
}

type GraphQLFetcher

type GraphQLFetcher struct {
	// contains filtered or unexported fields
}

GraphQLFetcher fetches PR data via GitHub's GraphQL API (v4).

func NewGraphQLFetcher

func NewGraphQLFetcher(
	token string,
	platformHost string,
	rateTracker *RateTracker,
	budget *SyncBudget,
) *GraphQLFetcher

NewGraphQLFetcher creates a fetcher for the given host. budget may be nil.

func NewGraphQLFetcherWithClient

func NewGraphQLFetcherWithClient(
	client *githubv4.Client, rateTracker *RateTracker,
) *GraphQLFetcher

NewGraphQLFetcherWithClient wraps a pre-built githubv4.Client as a GraphQLFetcher. Used by tests that need to point the fetcher at a mock HTTP backend.

func (*GraphQLFetcher) FetchRepoIssues

func (g *GraphQLFetcher) FetchRepoIssues(
	ctx context.Context, owner, name string,
) (*RepoBulkResult, error)

func (*GraphQLFetcher) FetchRepoPRs

func (g *GraphQLFetcher) FetchRepoPRs(
	ctx context.Context, owner, name string,
) (*RepoBulkResult, error)

func (*GraphQLFetcher) ShouldBackoff

func (g *GraphQLFetcher) ShouldBackoff() (bool, time.Duration)

type QueueItem

type QueueItem struct {
	Type         QueueItemType
	RepoOwner    string
	RepoName     string
	Number       int
	PlatformHost string
	Score        float64

	// Scoring inputs
	UpdatedAt       time.Time
	DetailFetchedAt *time.Time
	CIHadPending    bool
	Starred         bool
	Watched         bool
	IsOpen          bool
}

QueueItem holds scoring inputs and result for a single item that may need a detail fetch.

func BuildQueue

func BuildQueue(
	items []QueueItem, now time.Time,
) []QueueItem

BuildQueue filters items by staleness, scores eligible ones, and returns them sorted by score descending.

func (*QueueItem) WorstCaseCost

func (qi *QueueItem) WorstCaseCost() int

WorstCaseCost returns the maximum API calls this item's detail fetch could require.

type QueueItemType

type QueueItemType int

QueueItemType distinguishes PRs from issues for cost estimation.

const (
	QueueItemPR QueueItemType = iota
	QueueItemIssue
)

type RateTracker

type RateTracker struct {
	// contains filtered or unexported fields
}

RateTracker records per-host API request counts and rate limit state, persisting to SQLite for cross-restart visibility.

func NewRateTracker

func NewRateTracker(
	database *db.DB, platformHost string, apiType string,
) *RateTracker

NewRateTracker creates a tracker for the given platform host and API type. It hydrates from DB if a row exists for the current hour.

func (*RateTracker) HourStart

func (rt *RateTracker) HourStart() time.Time

HourStart returns the start of the current tracking hour.

func (*RateTracker) IsPaused

func (rt *RateTracker) IsPaused() bool

IsPaused returns true when remaining quota is at or below the reserve buffer and quota info is fresh.

func (*RateTracker) Known

func (rt *RateTracker) Known() bool

Known returns true if we have received at least one rate limit response with a positive limit value.

func (*RateTracker) RateLimit

func (rt *RateTracker) RateLimit() int

RateLimit returns the last known rate limit.

func (*RateTracker) RecordRequest

func (rt *RateTracker) RecordRequest()

RecordRequest increments the hourly request counter and persists to DB.

func (*RateTracker) Remaining

func (rt *RateTracker) Remaining() int

Remaining returns the last known remaining request count.

func (*RateTracker) RequestsThisHour

func (rt *RateTracker) RequestsThisHour() int

RequestsThisHour returns the number of requests recorded in the current hour window.

func (*RateTracker) ResetAt

func (rt *RateTracker) ResetAt() *time.Time

ResetAt returns a copy of the reset time, or nil if unknown.

func (*RateTracker) SetOnWindowReset

func (rt *RateTracker) SetOnWindowReset(fn func())

SetOnWindowReset registers a callback invoked when a GitHub rate limit window reset is detected. The callback runs with the tracker's mutex released.

func (*RateTracker) ShouldBackoff

func (rt *RateTracker) ShouldBackoff() (bool, time.Duration)

ShouldBackoff returns true and the wait duration if the rate limit is exhausted (remaining==0). If resetAt is nil, defaults to 60s. Returns false if remaining is >0 or unknown (-1).

func (*RateTracker) ThrottleFactor

func (rt *RateTracker) ThrottleFactor() int

ThrottleFactor returns a multiplier (1, 2, 4, or 8) based on how much remaining quota is left relative to the limit.

func (*RateTracker) UpdateFromRate

func (rt *RateTracker) UpdateFromRate(rate gh.Rate)

UpdateFromRate updates remaining/limit/reset from a go-github Rate. If the reset time moved forward, GitHub started a new rate window — the request counter resets to stay aligned with GitHub's window.

type RepoBulkResult

type RepoBulkResult struct {
	PullRequests []BulkPR
	Issues       []BulkIssue
}

RepoBulkResult holds all open PRs and issues fetched via GraphQL for a repo.

type RepoRef

type RepoRef struct {
	Owner        string
	Name         string
	PlatformHost string // "github.com" or GHE hostname
}

RepoRef identifies a GitHub repository.

type RepoSyncResult

type RepoSyncResult struct {
	Owner        string
	Name         string
	PlatformHost string
	Error        string // empty on success
}

RepoSyncResult holds the outcome of syncing a single repo.

type SyncBudget

type SyncBudget struct {
	// contains filtered or unexported fields
}

SyncBudget tracks hourly API call spend for background detail fetches on a single host.

func NewSyncBudget

func NewSyncBudget(limit int) *SyncBudget

func (*SyncBudget) CanSpend

func (b *SyncBudget) CanSpend(n int) bool

func (*SyncBudget) Limit

func (b *SyncBudget) Limit() int

func (*SyncBudget) Refund

func (b *SyncBudget) Refund(n int)

Refund returns n calls back to the budget.

func (*SyncBudget) Remaining

func (b *SyncBudget) Remaining() int

func (*SyncBudget) Reset

func (b *SyncBudget) Reset()

func (*SyncBudget) Spend

func (b *SyncBudget) Spend(n int)

func (*SyncBudget) Spent

func (b *SyncBudget) Spent() int

func (*SyncBudget) TrySpend

func (b *SyncBudget) TrySpend(n int) bool

TrySpend atomically checks and increments the budget. Returns true if the spend was successful.

type SyncStatus

type SyncStatus struct {
	Running     bool      `json:"running"`
	CurrentRepo string    `json:"current_repo,omitempty"`
	Progress    string    `json:"progress,omitempty"`
	LastRunAt   time.Time `json:"last_run_at,omitzero"`
	LastError   string    `json:"last_error,omitempty"`
}

SyncStatus holds the current state of the sync engine.

type Syncer

type Syncer struct {
	// contains filtered or unexported fields
}

Syncer periodically pulls PR data from GitHub into SQLite.

func NewSyncer

func NewSyncer(
	clients map[string]Client,
	database *db.DB,
	clones *gitclone.Manager,
	repos []RepoRef,
	interval time.Duration,
	rateTrackers map[string]*RateTracker,
	budgets map[string]*SyncBudget,
) *Syncer

NewSyncer creates a Syncer that polls the given repos on the given interval. clients maps host -> Client; rateTrackers maps host -> RateTracker. Both may contain nil values. clones may be nil. budgets maps host -> SyncBudget; nil or empty disables detail drain and backfill. Budgets are created by the caller (typically main.go) and wired into each Client's HTTP transport at construction time so every sync-context RoundTrip is automatically counted.

func (*Syncer) Budgets

func (s *Syncer) Budgets() map[string]*SyncBudget

Budgets returns the per-host sync budgets map.

func (*Syncer) ClientForHost

func (s *Syncer) ClientForHost(
	host string,
) (Client, error)

ClientForHost returns the Client for a specific host, or an error if no client is configured for that host.

func (*Syncer) ClientForRepo

func (s *Syncer) ClientForRepo(
	owner, name string,
) (Client, error)

ClientForRepo returns the Client for a tracked repo by owner/name, or an error if the repo is not tracked.

func (*Syncer) HasDiffSync

func (s *Syncer) HasDiffSync() bool

HasDiffSync reports whether the syncer has a clone manager configured and is therefore expected to populate diff SHAs for tracked PRs. The HTTP layer uses this to decide whether a missing diff is a sync issue worth warning about, or simply a deployment that opted out of diffs.

func (*Syncer) HostForRepo

func (s *Syncer) HostForRepo(owner, name string) string

HostForRepo returns the platform host for a tracked repo. Thread-safe.

func (*Syncer) IsTrackedRepo

func (s *Syncer) IsTrackedRepo(owner, name string) bool

IsTrackedRepo checks whether the given repo is in the configured list.

func (*Syncer) RateTrackers

func (s *Syncer) RateTrackers() map[string]*RateTracker

RateTrackers returns the per-host rate trackers map.

func (*Syncer) RunOnce

func (s *Syncer) RunOnce(ctx context.Context)

RunOnce performs a single sync pass across all configured repos. If a sync is already in progress it returns immediately (single-flight).

Repos are synced in parallel using a bounded worker pool sized by SetParallelism (default defaultParallelism). The bound keeps the per-host GitHub rate limit and abuse-detection thresholds happy while still capturing most of the wall-clock win on network I/O.

func (*Syncer) SetFetchers

func (s *Syncer) SetFetchers(fetchers map[string]*GraphQLFetcher)

SetFetchers registers GraphQL fetchers keyed by platform host.

func (*Syncer) SetOnMRSynced

func (s *Syncer) SetOnMRSynced(
	fn func(owner, name string, mr *db.MergeRequest),
)

SetOnMRSynced registers a callback invoked after each MR is upserted during a sync pass.

Concurrency: RunOnce processes repos in parallel (see SetParallelism), so the callback may be invoked from up to `parallelism` goroutines concurrently. Implementations must be safe for concurrent use. The callback also runs on the goroutine that is mid-sync for a repo, so it must not block indefinitely or it will stall sync progress.

Call SetOnMRSynced before Start/RunOnce. Mutating the hook while a sync is in flight is not safe.

func (*Syncer) SetOnStatusChange

func (s *Syncer) SetOnStatusChange(fn func(status *SyncStatus))

SetOnStatusChange registers a callback invoked whenever the sync status transitions (start, per-repo progress, rate-limit wait, completion). Used by the server to broadcast live sync state over SSE.

func (*Syncer) SetOnSyncCompleted

func (s *Syncer) SetOnSyncCompleted(
	fn func(results []RepoSyncResult),
)

SetOnSyncCompleted registers a callback invoked at the end of each RunOnce pass with per-repo sync results.

Concurrency: this hook fires once per RunOnce pass on the goroutine that drives RunOnce, so it is not invoked concurrently with itself. Call SetOnSyncCompleted before Start/RunOnce; mutating the hook while a sync is in flight is not safe.

func (*Syncer) SetParallelism

func (s *Syncer) SetParallelism(n int)

SetParallelism sets the maximum number of repos synced concurrently in RunOnce. Values <= 0 are clamped to 1 (sequential).

func (*Syncer) SetRepos

func (s *Syncer) SetRepos(repos []RepoRef)

SetRepos atomically replaces the list of repositories to sync.

func (*Syncer) SetWatchInterval

func (s *Syncer) SetWatchInterval(d time.Duration)

SetWatchInterval sets the fast-sync interval for watched MRs. Must be called before Start.

func (*Syncer) SetWatchedMRs

func (s *Syncer) SetWatchedMRs(mrs []WatchedMR)

SetWatchedMRs replaces the fast-sync watch list. Each watched MR is synced on the watch interval via SyncMR, independent of the bulk sync cycle.

func (*Syncer) Start

func (s *Syncer) Start(ctx context.Context)

Start runs an immediate sync then launches a background ticker. It returns as soon as the goroutine is started; call Stop to shut it down. A second goroutine runs watched-MR fast-syncs on a shorter interval.

The caller's ctx and the syncer's internal lifetime ctx (canceled by Stop) are both honored: either one unblocks any in-flight work.

func (*Syncer) Status

func (s *Syncer) Status() *SyncStatus

Status returns a snapshot of the current sync state.

func (*Syncer) Stop

func (s *Syncer) Stop()

Stop signals the background goroutine to exit. Safe to call multiple times. Cancels the syncer's lifetime context first so blocked RunOnce and TriggerRun goroutines can observe the cancellation and unwind their GitHub calls, then waits for the wait group up to stopGracePeriod. The bounded wait prevents Stop from hanging the process in pathological cases where a client ignores ctx.

func (*Syncer) SyncIssue

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

SyncIssue fetches fresh data for a single issue from GitHub and updates the DB. Returns an error if the repo is not in the configured repo list.

func (*Syncer) SyncItemByNumber

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

SyncItemByNumber fetches an item by number from GitHub, determines whether it is a PR or issue, syncs it into the DB, and returns the item type ("pr" or "issue"). Returns an error if the repo is not in the configured repo list.

func (*Syncer) SyncMR

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

SyncMR fetches fresh data for a single MR from GitHub and updates the DB. Unlike the periodic sync, this always does a full fetch (details, timeline, CI). Returns an error if the repo is not in the configured repo list.

func (*Syncer) TriggerRun

func (s *Syncer) TriggerRun(ctx context.Context)

TriggerRun kicks off a non-blocking RunOnce on the Syncer's wait group so callers can request an ad-hoc sync without blocking the caller. The run participates in the Syncer's lifecycle: Stop cancels the merged context so any in-flight GitHub call unblocks, then waits for the goroutine to exit. The caller's ctx is honored too, so per-request deadlines still apply.

type WatchedMR

type WatchedMR struct {
	Owner        string
	Name         string
	Number       int
	PlatformHost string // "github.com" or GHE hostname
}

WatchedMR identifies a merge request to sync on a fast interval.

type WorkflowApprovalState

type WorkflowApprovalState struct {
	Checked  bool
	Required bool
	Count    int
	RunIDs   []int64
}

WorkflowApprovalState describes whether workflow approval is needed for a PR.

func WorkflowApprovalStateFromRuns

func WorkflowApprovalStateFromRuns(runs []*gh.WorkflowRun) WorkflowApprovalState

WorkflowApprovalStateFromRuns converts matched workflow runs into state.

Jump to

Keyboard shortcuts

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