gh

package
v0.23.0 Latest Latest
Warning

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

Go to latest
Published: Nov 9, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package gh talks to the GitHub REST API on behalf of relctl.

Auth deliberately prefers an existing `gh` login over a provisioned token. Every maintainer who can cut a release already has `gh` working, and asking them to mint a PAT to run a read-only `relctl status` would be the kind of friction that stops a tool being used. In a workflow, GITHUB_TOKEN is set and `gh` may not be installed at all, so the environment is checked first.

Index

Constants

View Source
const (
	// SourceDispatch means a person is named because they dispatched a
	// workflow: either this run directly, or the release-prepare run that
	// pushed the tag this run built.
	SourceDispatch = "dispatch"
	// SourcePush means the tag was pushed by hand rather than by
	// release-prepare, so the run's own actor is the person who pushed it.
	SourcePush = "push"
	// SourceUnknown means we could not tell, and say so rather than print a
	// name that would be read as an answer.
	SourceUnknown = "unknown"
)

Where an attribution came from.

View Source
const (
	WorkflowPrepare = "release-prepare.yaml"
	WorkflowRelease = "release.yaml"
	WorkflowUpgrade = "release-upgrade.yaml"
	WorkflowBranch  = "create-release-branch.yaml"
	WorkflowNightly = "nightly.yaml"
	WorkflowCI      = "ci.yaml"
)

Workflow file names relctl reads and dispatches.

View Source
const DefaultRepo = "Azure/unbounded"

DefaultRepo is the repository relctl operates on unless told otherwise.

Variables

View Source
var ErrNoToken = errors.New(
	"no GitHub credential found: set GITHUB_TOKEN, or run 'gh auth login'")

ErrNoToken is returned when no credential could be found by any route.

Functions

func EnvOrGH

func EnvOrGH(ctx context.Context) (string, error)

EnvOrGH takes GITHUB_TOKEN or GH_TOKEN when set, and otherwise borrows the token `gh` already holds.

The environment wins because that is the workflow case, where `gh` may be absent and a token is always present. Shelling out is the interactive case.

func SplitRepo

func SplitRepo(slug string) (owner, name string, err error)

SplitRepo parses an owner/name slug.

func Transient added in v0.9.0

func Transient(err error) bool

Transient reports whether an error is worth waiting out rather than reporting.

For a command that polls, the distinction is the whole difference between riding out a bad minute and spinning for the full timeout on a credential that will never work. It is drawn on go-github's typed errors rather than on status codes, because GitHub answers a rate limit with 403 - the same code it uses for "you may not do that" - and only the type tells them apart.

Every error this package returns wraps the original with %w, so errors.As reaches the cause through the annotation.

Types

type Attribution added in v0.9.0

type Attribution struct {
	// By is the login, empty when Source is SourceUnknown.
	By string `json:"by,omitempty"`
	// Source says how By was arrived at, so a caller can render "cut by" and
	// "pushed by" differently rather than presenting an inference as a fact.
	Source string `json:"source"`
	// RunURL links the release-prepare run By dispatched, when there is one.
	RunURL string `json:"runUrl,omitempty"`
}

Attribution is who is actually responsible for a run.

This exists because a release.yaml run's `actor` is not a person who did anything. release-prepare pushes the tag over SSH with a deploy key rather than GITHUB_TOKEN, because GitHub suppresses workflow triggers for tags pushed with the default token. GitHub then attributes a deploy-key push to whoever REGISTERED THE KEY, so every release build reports the same login no matter who cut it, and has done since the key was introduced.

triggering_actor is no help: on a push it equals actor, and on a re-run it names whoever pressed re-run. Neither field can name the cutter.

func Attribute added in v0.9.0

func Attribute(build Run, prepares []Run) Attribution

Attribute names the person behind a run.

Pure, and separated from the fetching, because the correlation rule is the part worth testing and it is a heuristic rather than a lookup: nothing in the API links a tag push back to the workflow run that made it.

Only workflow_dispatch carries a trustworthy actor. A push may carry the deploy key's owner, and everything else inherits an actor from the run that triggered it. For a push the rule is containment in time: a prepare run's window is open from when it was created until shortly after it finished, and the build it caused was created inside that window.

What makes containment unambiguous rather than merely plausible is release-prepare's `concurrency: release-prepare` group with cancel-in-progress false: prepares are serialized, so at most one window is open at any moment. Without that, two overlapping prepares would both contain the same push and there would be no way to choose.

Times come from CreatedAt on both sides, never RunStartedAt. A re-run moves run_started_at and leaves created_at alone, so created_at remains the moment of the push however many times the build is retried. Using run_started_at would break attribution for exactly the runs people investigate.

The candidate list is trusted to have been taken after the build. Nothing here can check that - a list is just a slice, and one fetched an hour too early looks exactly like one fetched a second too late - so the callers carry the obligation. See Prepares, and TestAttributeTrustsItsCandidateList for what breaks when they do not.

func (Attribution) Known added in v0.9.0

func (a Attribution) Known() bool

Known reports whether an actual person was identified.

type Client

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

Client is a GitHub API client scoped to one repository.

func New

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

New builds a Client.

Constructed lazily by the commands that need it: version resolution is pure git and must keep working with no credential at all, including inside a workflow that has not been granted one.

func (*Client) API

func (c *Client) API() *github.Client

API exposes the underlying client for calls this package does not wrap.

func (*Client) Dispatch

func (c *Client) Dispatch(ctx context.Context, workflow, ref string, inputs map[string]any) error

Dispatch starts a workflow_dispatch run.

GitHub's dispatch endpoint returns 204 with no body: it does not say which run it created, or whether the inputs were understood. A rejected input is a 422 here, but a workflow that ignores an input it does not declare is a silent success, which is why the callers print exactly what they are sending.

func (*Client) Drafts

func (c *Client) Drafts(ctx context.Context) ([]Release, error)

Drafts lists unpublished releases, newest first.

Worth surfacing because a draft is a release that built but never shipped, and the usual cause is a soak that failed. They accumulate silently otherwise: this repository has drafts going back to v0.1.17.

Paginated, and that matters rather than being tidiness. A single page filtered client-side reports a count that reads as a total but is really "drafts among the most recent N releases". At the time of writing 24 of the most recent 30 releases are drafts, so the two answers were already about to diverge.

func (*Client) FindDispatched

func (c *Client) FindDispatched(
	ctx context.Context,
	workflow string,
	after time.Time,
	timeout time.Duration,
) (*Run, error)

FindDispatched waits for the run a dispatch created.

The dispatch endpoint returns no run ID, so the run has to be found afterwards. Matching is on workflow, event and creation time: anything created after the dispatch was sent, on that workflow, by workflow_dispatch.

Not exact, and deliberately not pretending to be. Two dispatches of one workflow within the polling window are indistinguishable, so this reports the newest and the caller shows its URL rather than asserting ownership.

func (*Client) LatestRelease

func (c *Client) LatestRelease(ctx context.Context) (*Release, error)

LatestRelease fetches whatever /releases/latest resolves to.

This is the release the install command in README.md and every guide points at, which is why relctl reports it separately from "the highest version": the two can disagree, and when they do that is the thing worth knowing.

func (*Client) Name

func (c *Client) Name() string

Name returns the repository name.

func (*Client) Owner

func (c *Client) Owner() string

Owner returns the repository owner.

func (*Client) Prepares added in v0.9.0

func (c *Client) Prepares(ctx context.Context) ([]Run, error)

Prepares lists recent release-prepare runs, newest first, for correlation.

MUST be called AFTER fetching the runs it will be used to attribute, never before. A list is a window in time, and Attribute reads the absence of a match in it as evidence that a tag was pushed by hand. Taken too early the list cannot contain the prepare that is about to push, the absence means nothing, and the deploy key's owner gets reported as the pusher: the one answer this whole file exists to avoid.

func (*Client) Progress

func (c *Client) Progress(ctx context.Context, tag string) (*ReleaseProgress, error)

Progress correlates the runs belonging to one tag.

The build run is exact: a tag push sets head_branch to the TAG, so release.yaml runs are identifiable by name.

The soak is not. release-upgrade fires on workflow_run, so its head_branch is the default branch and the API exposes no link back to the run that triggered it. Matching on head_sha alone is ambiguous, because a promoted final and its last candidate share a commit: v0.3.0 and v0.3.0-rc.1 are both 3c9621da, as are v0.2.4 and v0.2.4-rc.1. Both tags therefore produce build runs with the same head_sha, and so do their soaks.

So the sha narrows server-side and the TIME WINDOW disambiguates: a soak belongs to this build if it started after it and before the next build of the same commit. Manual workflow_dispatch retries land in the same window, which is what you want, since they are attempts at the same soak.

func (*Client) Release

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

Release fetches one release by tag. A missing release is not an error: it reports nil, because "built but not yet drafted" is an ordinary state to be in while watching a release through.

func (*Client) ReleaseBranches

func (c *Client) ReleaseBranches(ctx context.Context) ([]string, error)

ReleaseBranches lists the release-X.Y branches that exist.

func (*Client) Repo

func (c *Client) Repo() string

Repo returns the owner/name slug this client is scoped to.

func (*Client) Runs

func (c *Client) Runs(ctx context.Context, q ListRuns) ([]Run, error)

Runs lists workflow runs, newest first.

type ListRuns

type ListRuns struct {
	// Workflow is the workflow file name.
	Workflow string
	// Branch filters by head branch. For a tag push this is the TAG, which is
	// what makes a build run identifiable by the tag it built.
	Branch string
	// Event filters by triggering event.
	Event string
	// HeadSHA filters server-side by commit.
	HeadSHA string
	// Status filters server-side, e.g. "in_progress" or "queued".
	//
	// Server-side rather than a Go filter over one page: a workflow with ten
	// completed runs newer than a still-running one would otherwise drop it
	// entirely, which is exactly what concurrent tag pushes produce.
	Status string
	// Limit is the page size, capped by GitHub at 100.
	//
	// Named for what it is. It is not a total: nothing here paginates, because
	// every caller wants a recent window rather than the whole history.
	Limit int
}

ListRuns describes which runs to fetch.

type Options

type Options struct {
	// Repo is the owner/name slug. Empty means DefaultRepo.
	Repo string
	// Token supplies the credential. Nil means EnvOrGH.
	Token TokenSource
	// BaseURL points the client at another API root, for tests.
	BaseURL string
}

Options configure a Client.

type Release

type Release struct {
	Tag        string
	Draft      bool
	Prerelease bool
	URL        string
	// PublishedAt is the zero time for a draft. GitHub only sets published_at
	// on publication, so it is empty for exactly the releases anything about
	// draft age would want it for, and empty without an error to say so.
	// Use CreatedAt when the release may be a draft.
	PublishedAt time.Time
	// CreatedAt is set for drafts as well as published releases, and for a
	// draft it is the date of the commit the release points at.
	CreatedAt time.Time
}

Release is a GitHub release, reduced to what relctl reports on.

func (Release) State

func (r Release) State() string

State renders the release's publication state.

type ReleaseProgress

type ReleaseProgress struct {
	// Build is the release.yaml run that built the tag.
	Build *Run
	// Soaks are the release-upgrade.yaml runs for that build, oldest first.
	// More than one means the soak was retried.
	Soaks []Run
}

ReleaseProgress is where a release has got to, across three workflows.

type Run

type Run struct {
	ID         int64
	Workflow   string
	Status     string
	Conclusion string
	Event      string
	HeadBranch string
	HeadSHA    string
	// CreatedAt is when the run was CREATED, which for a push is when the push
	// happened. It does not move when a run is re-run, and that is what makes
	// it the right clock for Attribute. See RunStartedAt.
	CreatedAt time.Time
	// RunStartedAt is when the LATEST ATTEMPT began, so it moves on a re-run
	// and CreatedAt does not. Carried for display, never for correlation.
	RunStartedAt time.Time
	// UpdatedAt is when the run last changed, which for a finished run is when
	// it finished. Used as the closing edge of a run's window.
	UpdatedAt time.Time
	// Actor is who GitHub credits the run to.
	//
	// NOT who caused it, for a tag push. release-prepare pushes tags over SSH
	// with a deploy key, and GitHub attributes a deploy-key push to whoever
	// registered the key, so every release.yaml run names the same person
	// regardless of who cut the release. Attribute exists because of this.
	Actor string
	// TriggeringActor is who started the latest attempt, so it names whoever
	// pressed re-run rather than whoever caused the run to exist.
	TriggeringActor string
	URL             string
}

Run is one workflow run, reduced to what relctl reports on.

func (Run) Done

func (r Run) Done() bool

Done reports whether the run has finished, whatever the outcome.

func (Run) Failed added in v0.9.0

func (r Run) Failed() bool

Failed reports whether the run finished and GitHub said it did not succeed.

Deliberately not !Succeeded(). A completed run with no conclusion at all renders as "completed" (see State) and is an absence of evidence, not evidence of failure. Anywhere a failure EXCLUDES something, treating that absence as a failure is the unsafe direction: it discards a run that may have been fine and falls through to whatever the code does when it finds nothing.

func (Run) State

func (r Run) State() string

State renders the run's outcome, or its status while it is still going.

func (Run) Succeeded

func (r Run) Succeeded() bool

Succeeded reports whether the run finished cleanly.

type TokenSource

type TokenSource func(ctx context.Context) (string, error)

TokenSource resolves a GitHub credential. Replaced in tests.

Jump to

Keyboard shortcuts

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