ghapi

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package ghapi wraps go-github behind a narrow domain interface. No other package may import go-github: a major-version bump changes the import path, and confining it here keeps that from rippling through every file that names a pull request.

Index

Constants

View Source
const DefaultRequestTimeout = 15 * time.Second

DefaultRequestTimeout bounds every individual API call. The CI wait passes a much longer parent context but each request is still capped.

Variables

View Source
var ErrNotFound = errors.New("not found on GitHub")

ErrNotFound reports a 404 from GitHub. Callers map it to exit code 3.

View Source
var ErrUnauthorized = errors.New("not authorized by GitHub")

ErrUnauthorized reports a 401 or 403 that is not a rate limit. Its message names the token-resolution order, because that is almost always the fix.

Functions

func Describe

func Describe(err error) string

Describe renders any error as a short reason suitable for a prompt.

Types

type API

type API interface {
	FindPR(ctx context.Context, owner, repo, branch string) (PRInfo, error)
	ListWorkflows(ctx context.Context, owner, repo string) ([]Workflow, error)
	ListWorkflowRuns(ctx context.Context, owner, repo, workflowFile, branch string, limit int) ([]Run, error)
	ListRunJobs(ctx context.Context, owner, repo string, runID int64) ([]Job, error)
	SuccessfulRunDurations(ctx context.Context, owner, repo, workflowFile, branch string, limit int) ([]time.Duration, error)
	AuthenticatedLogin(ctx context.Context) (string, error)
	HasToken() bool

	// LastRate reports the rate-limit budget GitHub returned with the most
	// recent call, so a long polling loop can widen its interval before it
	// runs the quota out. remaining is -1 when nothing has been observed yet.
	LastRate() (remaining int, reset time.Time)
}

API is the narrow interface the rest of flow depends on. Everything above this package mocks API, never go-github itself.

type CachingTransport

type CachingTransport struct {
	Base http.RoundTripper
	// contains filtered or unexported fields
}

CachingTransport implements conditional requests with ETags. A 45-minute CI wait polling every 5s is roughly 540 requests; without this, each one costs a rate-limit unit even when nothing changed.

It is deliberately a small in-memory map rather than a dependency on an unmaintained HTTP cache library: flow's polling loop is the only consumer, the responses are small, and the cache lives no longer than one command.

func NewCachingTransport

func NewCachingTransport(base http.RoundTripper) *CachingTransport

NewCachingTransport wraps base with an ETag cache.

func (*CachingTransport) RoundTrip

func (t *CachingTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

type Client

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

Client is the go-github-backed implementation of API.

func New

func New(ctx context.Context, opts Options) *Client

New builds a client. A missing token is not an error: the client still works for public data, and every caller already handles the Unknown case.

func (*Client) AuthenticatedLogin

func (c *Client) AuthenticatedLogin(ctx context.Context) (string, error)

AuthenticatedLogin returns the login the token belongs to. The token itself is never returned or logged.

func (*Client) FindPR

func (c *Client) FindPR(ctx context.Context, owner, repo, branch string) (PRInfo, error)

FindPR returns the newest pull request whose head is branch. A branch with no pull request yields PRNone, not an error — working before opening a PR is the normal case.

The head filter is "{owner}:{branch}", so this finds same-repo pull requests only. A PR opened from a fork has a head of "{forkOwner}:{branch}" and reads as PRNone, which makes `flow delete` ask "No pull request found for branch X. Delete anyway?" rather than naming the open PR. That is the intended trade-off for flow's workflow, where branches live in the repository itself; it fails safe, because PRNone still prompts rather than deleting. Matching fork PRs would mean listing without a head filter and comparing Head.Ref, which would also match an unrelated fork that happens to reuse the name.

func (*Client) HasToken

func (c *Client) HasToken() bool

HasToken reports whether the client is authenticated.

func (*Client) LastRate

func (c *Client) LastRate() (remaining int, reset time.Time)

LastRate reports the most recently observed rate-limit budget.

func (*Client) ListRunJobs

func (c *Client) ListRunJobs(ctx context.Context, owner, repo string, runID int64) ([]Job, error)

ListRunJobs returns every job of a run, following pagination: the ScaleOps workflow has well over one page of jobs, and a truncated list would silently drop a gating job.

func (*Client) ListWorkflowRuns

func (c *Client) ListWorkflowRuns(ctx context.Context, owner, repo, workflowFile, branch string, limit int) ([]Run, error)

ListWorkflowRuns returns runs of one workflow file on one branch, newest first.

func (*Client) ListWorkflows added in v0.4.0

func (c *Client) ListWorkflows(ctx context.Context, owner, repo string) ([]Workflow, error)

ListWorkflows returns the repository's active workflows, so flow can ask which one builds the chart rather than making the user look up a filename. Disabled workflows are left out: they cannot produce the run flow would wait for.

func (*Client) SuccessfulRunDurations

func (c *Client) SuccessfulRunDurations(ctx context.Context, owner, repo, workflowFile, branch string, limit int) ([]time.Duration, error)

SuccessfulRunDurations returns how long recent successful runs took, for the ETA in the progress display.

type Conclusion

type Conclusion string

Conclusion is how a completed run or job finished.

const (
	ConclusionSuccess   Conclusion = "success"
	ConclusionFailure   Conclusion = "failure"
	ConclusionCancelled Conclusion = "cancelled"
	ConclusionSkipped   Conclusion = "skipped"
	ConclusionTimedOut  Conclusion = "timed_out"
	ConclusionNeutral   Conclusion = "neutral"
	ConclusionNone      Conclusion = ""
)

Workflow run and job conclusions.

type Job

type Job struct {
	ID          int64      `json:"id"`
	Name        string     `json:"name"`
	Status      RunStatus  `json:"status"`
	Conclusion  Conclusion `json:"conclusion,omitempty"`
	StartedAt   *time.Time `json:"started_at,omitempty"`
	CompletedAt *time.Time `json:"completed_at,omitempty"`
	HTMLURL     string     `json:"html_url,omitempty"`
	Steps       []Step     `json:"steps,omitempty"`
}

Job is one job within a run.

func (Job) Done

func (j Job) Done() bool

Done reports a job that will not change state again. A skipped job counts as satisfied: on a default dev build, the GPU and FIPS legs are always skipped, and waiting for them would hang forever.

func (Job) Duration

func (j Job) Duration() time.Duration

Duration returns how long the job ran, or zero if it has not finished.

func (Job) Failed

func (j Job) Failed() bool

Failed reports a job that finished unsuccessfully. Skipped and cancelled are deliberately not failures here; cancellation is handled by retargeting.

func (Job) Skipped

func (j Job) Skipped() bool

Skipped reports a job GitHub chose not to run.

type Options

type Options struct {
	APIBase        string
	Token          string
	UserAgent      string
	RequestTimeout time.Duration
	DisableRetries bool
	DisableCaching bool
	// Transport overrides the base RoundTripper; tests use httptest.
	Transport http.RoundTripper
}

Options configure the client.

type PRInfo

type PRInfo struct {
	State    PRState    `json:"state"`
	Number   int        `json:"number,omitempty"` // 0 when None or Unknown
	URL      string     `json:"url,omitempty"`
	MergedAt *time.Time `json:"merged_at,omitempty"`
	// Reason explains why State is Unknown, for display.
	Reason string `json:"reason,omitempty"`
}

PRInfo is everything flow needs to know about a branch's pull request.

func Unknown

func Unknown(reason string) PRInfo

Unknown builds an Unknown PRInfo with a displayable reason.

func (PRInfo) Exists

func (p PRInfo) Exists() bool

Exists reports a pull request that was found.

func (PRInfo) Known

func (p PRInfo) Known() bool

Known reports whether the state was actually determined.

type PRState

type PRState int

PRState models pull-request existence as three values, because "there is no PR" and "I could not find out" must never collapse into the same branch of a conditional — the wording a user sees differs, and so does the right action.

const (
	// PRUnknown means the state could not be determined.
	PRUnknown PRState = iota
	// PRNone means it was determined that no pull request exists.
	PRNone
	PROpen
	PRMerged
	PRClosed
)

Pull request states.

func (PRState) MarshalJSON

func (s PRState) MarshalJSON() ([]byte, error)

MarshalJSON emits the name rather than the ordinal, so golden payloads stay readable and stable.

func (PRState) String

func (s PRState) String() string

String returns the lowercase name used in output and JSON payloads.

func (*PRState) UnmarshalJSON

func (s *PRState) UnmarshalJSON(data []byte) error

UnmarshalJSON parses the name form.

type RateLimitError

type RateLimitError struct {
	ResetAt time.Time
	Message string
}

RateLimitError reports an exhausted rate limit and when it resets.

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

type RetryTransport

type RetryTransport struct {
	Base http.RoundTripper
	// Sleep overrides the wait between attempts; tests substitute a no-op.
	// Left nil, the wait is a cancellable timer, so Ctrl-C during a backoff
	// aborts instead of running it out.
	Sleep func(time.Duration)
}

RetryTransport retries transport errors, 5xx responses and 429s, preferring the server's own Retry-After guidance over exponential backoff plus jitter. Implementing it as a RoundTripper means the policy applies uniformly instead of being repeated at every call site.

func NewRetryTransport

func NewRetryTransport(base http.RoundTripper) *RetryTransport

NewRetryTransport wraps base with the retry policy.

func (*RetryTransport) RoundTrip

func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

type Run

type Run struct {
	ID         int64      `json:"id"`
	Name       string     `json:"name,omitempty"`
	Status     RunStatus  `json:"status"`
	Conclusion Conclusion `json:"conclusion,omitempty"`
	Branch     string     `json:"branch,omitempty"`
	HTMLURL    string     `json:"html_url,omitempty"`
	Attempt    int        `json:"run_attempt,omitempty"`
	CreatedAt  time.Time  `json:"created_at"`
	UpdatedAt  time.Time  `json:"updated_at,omitempty"`
	HeadSHA    string     `json:"head_sha,omitempty"`
}

Run is one workflow run, flattened to the fields flow uses.

func (Run) Succeeded

func (r Run) Succeeded() bool

Succeeded reports a run that completed successfully.

type RunStatus

type RunStatus string

RunStatus is a workflow run or job lifecycle state.

const (
	StatusQueued     RunStatus = "queued"
	StatusInProgress RunStatus = "in_progress"
	StatusCompleted  RunStatus = "completed"
	StatusWaiting    RunStatus = "waiting"
	StatusPending    RunStatus = "pending"
)

Workflow run and job statuses as GitHub reports them.

type Step

type Step struct {
	Name       string     `json:"name"`
	Number     int        `json:"number"`
	Status     RunStatus  `json:"status"`
	Conclusion Conclusion `json:"conclusion,omitempty"`
}

Step is one step within a job. The step list is what supplies the human-readable description in the progress display.

type Workflow added in v0.4.0

type Workflow struct {
	// Name is the display name, e.g. "Build And Release".
	Name string `json:"name"`
	// File is the base name of the workflow file, e.g. "go.yaml". That, not
	// the display name, is what every other call here takes.
	File string `json:"file"`
}

Workflow is one entry from a repository's Actions workflow list.

Jump to

Keyboard shortcuts

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