Help us shape the future of Go! Take the 2026 Go Developer Survey

github

package
v1.0.0-RC3 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package github wraps everything Zoomies needs from the GitHub API: App authentication, minting runner credentials, listing runners and queued jobs, and validating inbound webhooks.

Zoomies never asks an operator for a personal access token. It authenticates as a GitHub App and mints short-lived registration credentials itself, so there is no long-lived token sitting in a dotfile next to a runner.

Index

Constants

View Source
const (
	// SignatureHeader carries the HMAC-SHA256 of the body.
	SignatureHeader = gh.SHA256SignatureHeader
	// EventTypeHeader carries the event name, e.g. "workflow_job".
	EventTypeHeader = gh.EventTypeHeader
	// DeliveryIDHeader carries the delivery UUID, which is what an operator
	// quotes when comparing Zoomies' log with GitHub's redelivery page.
	DeliveryIDHeader = gh.DeliveryIDHeader
)

Headers GitHub sets on every delivery. They are re-exported here so the API package does not need its own opinion about their spelling.

View Source
const MigrationPermissionHint = `the migration wizard needs three repository permissions: ` +
	`"Contents" (contents) read and write, "Pull requests" (pull_requests) read and write, and ` +
	`"Workflows" (workflows) write, which GitHub requires specifically to change files under .github/workflows. ` +
	`An App created by Zoomies asks for all three at creation, so this one either predates that or had them ` +
	`removed: add them under the App's Permissions & events, then accept the change on the installation`

MigrationPermissionHint names what an App needs before it can read a repository's workflows and open a migration pull request, in the words GitHub's own settings page uses.

Variables

View Source
var ErrForbidden = errors.New("github: forbidden")

ErrForbidden is returned for a 403 that is not a rate limit, usually meaning the App installation is missing a permission.

View Source
var ErrInvalid = errors.New("github: refused as invalid")

ErrInvalid is GitHub refusing a request as invalid rather than unauthorised: a runner name already taken, a label it will not accept. The detail carries the field-by-field reason, which is the part worth reading -- the message on a 422 is always "Validation Failed".

View Source
var ErrInvalidSignature = errors.New("github: webhook signature invalid")

ErrInvalidSignature is returned when a delivery is unsigned or its signature does not match. The API answers these with 401 and does not process the body.

View Source
var ErrNoSecret = errors.New("github: no webhook secret configured")

ErrNoSecret is returned when Zoomies holds no webhook secret to verify a delivery against. It is a configuration fault, not an attack, and the API answers it differently from a bad signature.

View Source
var ErrNoWorkflows = errors.New("github: the repository has no .github/workflows directory")

ErrNoWorkflows is returned when a repository has no .github/workflows at all, which is not a failure: most repositories in a large organisation do not.

View Source
var ErrNotFound = errors.New("github: not found")

ErrNotFound is returned when GitHub reports a 404 for something Zoomies expected to exist.

View Source
var ErrNotWorkflowJob = errors.New("github: not a workflow_job payload")

ErrNotWorkflowJob is returned when a payload is not a workflow_job event.

View Source
var ErrRateLimited = errors.New("github: rate limited")

ErrRateLimited is returned when the installation has exhausted its quota. The caller backs off rather than hammering.

View Source
var MigrationPermissions = map[string]string{
	"contents":      "write",
	"pull_requests": "write",
	"workflows":     "write",
}

MigrationPermissions is the same requirement as data: permission name to the level needed. The API compares it against what an installation was granted so the wizard can say what is missing before it tries.

Functions

func BranchName

func BranchName(now time.Time) string

BranchName builds the branch a migration commits to.

The timestamp is what keeps two runs of the wizard from colliding: creating a ref that already exists fails, and the alternative -- reusing the branch -- would rewrite a pull request somebody might be reviewing.

func InstallURL

func InstallURL(htmlURL string) string

InstallURL returns the page that installs an App on an account, given the html_url the manifest conversion returned.

func IsEnterprise

func IsEnterprise(apiBaseURL string) bool

IsEnterprise reports whether an API base URL points at GitHub Enterprise Server rather than github.com.

func IsPing

func IsPing(event string) bool

IsPing reports whether an event is GitHub's "does this endpoint exist" probe, which is sent once when a webhook is configured and carries no job.

func Manifest

func Manifest(o ManifestOptions) ([]byte, error)

Manifest renders the JSON an operator's browser POSTs to GitHub to create the App.

The permission set is deliberately minimal, because an App that manages a fleet's runners is a high-value credential: it asks for the runner administration permission for the kind of target it will manage, read access to Actions so it can see queued jobs, the three the migration wizard needs to rewrite a runs-on line and open a pull request for it, and nothing else. Metadata read is mandatory for every App.

Every key here is one GitHub's manifest schema permits; it rejects anything else outright, so nothing speculative belongs in this struct.

func ManifestURL

func ManifestURL(apiBaseURL, org string) string

ManifestURL returns the page the manifest form posts to. GitHub has a separate endpoint for creating an App under an organisation, and posting the org manifest to the user endpoint silently creates a personal App instead.

func NormalizeAPIBaseURL

func NormalizeAPIBaseURL(raw string) (string, error)

NormalizeAPIBaseURL turns the forms operators actually type into the form go-github expects: an absolute URL ending in a slash, with /api/v3 appended for a bare GHES hostname.

func ParseEventType

func ParseEventType(header string) string

ParseEventType normalises the X-GitHub-Event header. GitHub sends it lowercase, but proxies have been known to rewrite header values.

func RetryAfterRateLimit

func RetryAfterRateLimit(err error, now time.Time) (time.Time, bool)

RetryAfterRateLimit reports when err says an installation may be used again.

It answers false for anything that is not a rate limit and for a rate limit GitHub said nothing useful about, which is the same thing to a caller: fall back to your own wait.

func RunnerName

func RunnerName(pool *store.Pool) string

RunnerName mints the name one runner of this pool registers under: "zoomies-4vcpu-ubuntu-2404-biscuit-a3f9qz2m".

This is the one name Zoomies puts in somebody else's account, so GitHub's constraints on it are worth stating where the registration is made. The name must be unique within the target -- two runners answering to one name is one registration being taken over, not two runners -- which is what the random token at the end is for, and why nothing may truncate it away. GitHub also shows it in three places a reader arrives at knowing nothing: the runner list, the job header, and the "Set up job" step of every log. That is what the shape in the middle answers, and store.NewRunnerName explains why it is worth the characters.

The 64-character limit GitHub enforces is internal/naming's MaxNameLength, so a name is trimmed to fit here rather than refused by the API at registration.

func SettingsURL

func SettingsURL(apiBaseURL, slug, org string) string

SettingsURL returns the App's own settings page, which is the only place its logo can be set.

The App manifest schema has no field for an avatar -- GitHub takes it as an upload and nothing else -- so an App created by Zoomies arrives with the grey default, and every "Set up job" line in the organisation's logs is signed by an anonymous App. Pointing the operator at the page and handing them the mark is as close to automatic as GitHub allows.

An App owned by an organisation has its settings under that organisation; one owned by a user has them under the user's own settings, and GitHub answers the wrong URL with a 404 rather than a redirect.

func SplitTarget

func SplitTarget(target string) (owner, repo string, kind store.TargetType)

SplitTarget parses "owner" or "owner/repo" into its parts.

func ValidateSignature

func ValidateSignature(payload []byte, header, secret string) error

ValidateSignature checks the HMAC GitHub attached to a delivery.

It refuses to pass an unsigned delivery under any circumstances: an endpoint that accepts unsigned webhooks lets anyone on the internet make Zoomies start runners.

func WebURLForAPI

func WebURLForAPI(apiBaseURL string) string

WebURLForAPI derives the browser-facing base URL from an API base URL, which is what the UI links to.

Types

type AppFactory

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

AppFactory builds GitHub clients that authenticate as a GitHub App installation.

One factory is shared by the whole process so that every installation reuses the same TCP connection pool.

func NewAppFactory

func NewAppFactory(httpClient *http.Client) *AppFactory

NewAppFactory returns a factory that dials GitHub with httpClient. Passing nil uses a client with a request timeout, which is what production wants; tests pass their own to point at a fake.

func (*AppFactory) For

func (f *AppFactory) For(ctx context.Context, inst *store.Installation, privateKeyPEM []byte) (Client, error)

For returns a Client scoped to one installation, authenticated with the App's private key.

privateKeyPEM is passed in rather than read from the installation because the store keeps it sealed; unsealing is the caller's job and the plaintext key never lives longer than this call needs it to.

type AppInfo

type AppInfo struct {
	ID    int64
	Slug  string
	Name  string
	Owner string
	// Permissions is what the App was granted, so setup can tell the operator
	// exactly which permission is missing rather than "403".
	Permissions map[string]string
	Events      []string
	// RepositorySelection is GitHub's own word for how much of the target this
	// installation covers: "all" or "selected". It is the difference between a
	// pool that will see every repository somebody pushes to and one that
	// silently sees none of them -- the second commonest setup mistake after a
	// missing permission, and the one nothing in Zoomies could previously say
	// anything about.
	RepositorySelection string
}

AppInfo describes the authenticated GitHub App.

func (*AppInfo) CanReadContents

func (a *AppInfo) CanReadContents() bool

CanReadContents reports whether this installation may read a repository's files at all.

It is a lower bar than MissingForMigration, and deliberately so: opening a pull request needs Contents at write, but merely reading a repository's workflows needs only read. The difference matters because GitHub answers 404 -- not 403 -- for contents an App may not see, which is the same answer it gives for a repository that genuinely has no .github/workflows. Without this, an App that was never granted Contents makes every repository in the organisation look empty.

func (*AppInfo) MissingForMigration

func (a *AppInfo) MissingForMigration() []string

MissingForMigration lists, in words, the permissions this App still lacks before it can open a pull request. It is empty when the App can.

A permission GitHub does not report at all is treated as missing: an older GHES that omits a key from the installation's permissions is not a reason to try the write and fail halfway through, having already created a branch.

func (*AppInfo) MissingRequirements

func (a *AppInfo) MissingRequirements(kind store.TargetType) []string

MissingRequirements lists, in words an operator can act on, what this App is still missing for the given target type. It is empty when the App is correctly configured.

func (*AppInfo) SubscribedToJobs

func (a *AppInfo) SubscribedToJobs() bool

SubscribedToJobs reports whether the App will be told about queued jobs.

It is deliberately not one of MissingRequirements: every one of those is a permission the fleet cannot work without, and this is a subscription the fleet works without -- more slowly, on the fallback poller, which is a supported way to run. Bundling the two made a probe return an error for an App with every permission it needs, so an installation that was merely slower was recorded as unhealthy and the verify dialog's own "will run on the fallback poller" sentence could never be reached.

type Client

type Client interface {
	// Target returns the org or repo this client acts on.
	Target() (name string, kind store.TargetType)
	// Probe verifies the credentials and permissions, returning an error whose
	// message names the missing permission where GitHub tells us.
	Probe(ctx context.Context) (*AppInfo, error)
	// CreateJITConfig mints an ephemeral runner registration.
	CreateJITConfig(ctx context.Context, req JITRequest) (*JITConfig, error)
	// CreateRegistrationToken mints a one-hour token for config.sh.
	CreateRegistrationToken(ctx context.Context) (*RegistrationToken, error)
	// CreateRemoveToken mints a token for deregistering a runner from the host.
	CreateRemoveToken(ctx context.Context) (*RegistrationToken, error)
	// ListRunners returns every self-hosted runner registered on the target.
	ListRunners(ctx context.Context) ([]Runner, error)
	// DeleteRunner removes a registration. Deleting one that is already gone
	// returns nil, because the desired end state has been reached.
	DeleteRunner(ctx context.Context, id int64) error
	// ListRunnerGroups returns the target's runner groups.
	ListRunnerGroups(ctx context.Context) ([]RunnerGroup, error)
	// CreateRunnerGroup creates an organisation runner group. Repository
	// targets reject it because GitHub only supports groups at organisation
	// scope.
	CreateRunnerGroup(ctx context.Context, req RunnerGroupCreate) (*RunnerGroup, error)
	// ListQueuedJobs is the webhook fallback: it walks recent workflow runs and
	// returns the jobs still waiting for a runner.
	ListQueuedJobs(ctx context.Context) ([]QueuedJob, error)
	// GetWorkflowJob reconciles a known job even after its run has completed.
	GetWorkflowJob(ctx context.Context, repo string, id int64) (*WorkflowJobEvent, error)
	// CancelWorkflowRun asks GitHub to cancel the entire run containing a job.
	// Force bypasses conditions that may otherwise keep a run cancelling.
	CancelWorkflowRun(ctx context.Context, repo string, runID int64, force bool) error
	// RateLimit reports remaining quota.
	RateLimit(ctx context.Context) (*RateLimit, error)
	// WebURL returns the browser URL for the target.
	WebURL() string

	// ListRepositories returns the repositories this installation can see.
	ListRepositories(ctx context.Context, limit int) ([]Repository, error)
	// ListWorkflows returns the workflow files at the top of a repository's
	// .github/workflows, with their contents.
	ListWorkflows(ctx context.Context, repo string) ([]WorkflowFile, error)
	// OpenPullRequest commits a set of files to a new branch and opens a pull
	// request for it.
	OpenPullRequest(ctx context.Context, req PullRequestRequest) (*PullRequest, error)
}

Client is the GitHub surface Zoomies uses, scoped to one installation.

It is an interface so that tests can run the whole controller against a fake GitHub without a network, and so that GHES differences stay behind one seam.

type Factory

type Factory interface {
	For(ctx context.Context, inst *store.Installation, privateKeyPEM []byte) (Client, error)
}

Factory builds a Client for an installation. The controller holds one factory and caches clients per installation.

type FakeGitHub

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

FakeGitHub is an in-memory stand-in for the GitHub REST API.

It lives in the production package rather than a _test.go file on purpose: the controller, scheduler and API tests all need a GitHub to run the whole system against, and a second, subtly different fake in each of them would be worse than one honest fake here. "Honest" means it enforces the rules that actually bite -- runner names must be unique, a deleted runner is gone, a job only appears in a run's job list -- so a test that passes against it has some claim to passing against GitHub.

It serves both the github.com layout (/app) and the GitHub Enterprise Server layout (/api/v3/app), so the same fake exercises both code paths.

Pagination is not implemented: every list returns one page. Tests that need to exercise paging should drive appClient against their own handler.

func NewFake

func NewFake() *FakeGitHub

NewFake starts a fake GitHub and returns it. The caller must Close it.

func (*FakeGitHub) AddQueuedJob

func (f *FakeGitHub) AddQueuedJob(repo, workflow, jobName string, labels []string) QueuedJob

AddQueuedJob queues a job on repo ("owner/name") and returns it as the API will report it. The repository becomes visible to the installation.

func (*FakeGitHub) AddRepo

func (f *FakeGitHub) AddRepo(fullName string)

AddRepo makes "owner/name" visible to the installation. AddQueuedJob does this for you; call it directly for a repository with no jobs.

func (*FakeGitHub) AddRunner

func (f *FakeGitHub) AddRunner(name string, labels []string) Runner

AddRunner registers a runner that Zoomies did not create, which is how a test simulates a registration the controller has lost track of.

func (*FakeGitHub) AddRunnerGroup

func (f *FakeGitHub) AddRunnerGroup(name string) RunnerGroup

AddRunnerGroup registers a runner group and returns it.

func (*FakeGitHub) AddWorkflow

func (f *FakeGitHub) AddWorkflow(repo, path, content string)

AddWorkflow puts a workflow file in a repository, creating the repository if this is the first thing in it.

func (*FakeGitHub) AppID

func (f *FakeGitHub) AppID() int64

AppID is the App ID the fake claims to be.

func (*FakeGitHub) Branches

func (f *FakeGitHub) Branches(repo string) []string

Branches returns a repository's branch names, sorted.

func (*FakeGitHub) ClearErrors

func (f *FakeGitHub) ClearErrors()

ClearErrors removes every failure injected with SetError.

func (*FakeGitHub) Client

func (f *FakeGitHub) Client(target string, kind store.TargetType) Client

Client returns a Client wired to this fake for the given target. It is the quickest way for a test to get a working github.Client.

func (*FakeGitHub) Close

func (f *FakeGitHub) Close()

Close shuts the server down.

func (*FakeGitHub) CompleteJob

func (f *FakeGitHub) CompleteJob(jobID int64, conclusion string)

CompleteJob finishes a job with the given conclusion ("success", "failure", "cancelled", ...).

func (*FakeGitHub) FileContent

func (f *FakeGitHub) FileContent(repo, path string) (string, bool)

FileContent returns what a repository's file holds now, which is how a test checks what a migration actually committed.

func (*FakeGitHub) InstallationID

func (f *FakeGitHub) InstallationID() int64

InstallationID is the installation ID the fake serves.

func (*FakeGitHub) Requests

func (f *FakeGitHub) Requests() []string

Requests returns every request the fake has served, as "GET /path". Tests use it to assert that a poll stayed within its call budget.

func (*FakeGitHub) Runners

func (f *FakeGitHub) Runners() []Runner

Runners returns a snapshot of the registered runners.

func (*FakeGitHub) Server

func (f *FakeGitHub) Server() *httptest.Server

Server exposes the underlying test server, for tests that need its URL or its TLS-aware http.Client.

func (*FakeGitHub) SetArchived

func (f *FakeGitHub) SetArchived(repo string, archived bool)

SetArchived archives or unarchives a repository, creating it if this is the first thing said about it.

func (*FakeGitHub) SetDefaultBranch

func (f *FakeGitHub) SetDefaultBranch(repo, branch string)

SetDefaultBranch names a repository's default branch. It is "main" until something says otherwise.

func (*FakeGitHub) SetError

func (f *FakeGitHub) SetError(pattern string, status int, message string)

SetError makes every request whose path contains pattern fail with status and message. An empty pattern matches every request. Calls accumulate; use ClearErrors to reset.

A 403 carrying the rate-limit headers (see SetRateLimit) is how a test produces ErrRateLimited, because that is how GitHub signals it.

func (*FakeGitHub) SetEvents

func (f *FakeGitHub) SetEvents(events ...string)

SetEvents replaces the App's event subscriptions.

func (*FakeGitHub) SetMethodError

func (f *FakeGitHub) SetMethodError(method, pattern string, status int, message string)

SetMethodError is SetError narrowed to one HTTP method.

Several verbs share a path: an orphan sweep lists an installation's runners and then deletes them at the same one, so a test about a refused delete cannot afford to refuse the listing that finds them too.

func (*FakeGitHub) SetPermissions

func (f *FakeGitHub) SetPermissions(p map[string]string)

SetPermissions replaces what the App reports being granted, so tests can exercise the "missing permission" reporting.

func (*FakeGitHub) SetRateLimit

func (f *FakeGitHub) SetRateLimit(limit, remaining int, reset time.Time)

SetRateLimit sets the quota reported by /rate_limit and by the rate-limit headers on every response.

func (*FakeGitHub) SetRepositorySelection

func (f *FakeGitHub) SetRepositorySelection(selection string)

SetRepositorySelection sets GitHub's "all" or "selected", so a test can describe an App installed on a handful of repositories rather than on everything.

func (*FakeGitHub) SetRunnerGroupPublicAccess

func (f *FakeGitHub) SetRunnerGroupPublicAccess(name string, allowed bool)

SetRunnerGroupPublicAccess changes the public-repository policy GitHub reports for a group. It exists for controller tests of the otherwise easy to miss state where a runner is online but no public-repository job is eligible to use it.

func (*FakeGitHub) StartJob

func (f *FakeGitHub) StartJob(jobID int64, runnerName string)

StartJob marks a queued job as picked up by runnerName.

func (*FakeGitHub) URL

func (f *FakeGitHub) URL() string

URL is the API base URL to hand to an installation.

type FileChange

type FileChange struct {
	Path    string
	Content string
	// SHA is the blob SHA the change was computed against.
	SHA string
}

FileChange is one file a pull request writes.

type JITConfig

type JITConfig struct {
	// Encoded is the base64 configuration.
	Encoded string
	// RunnerID is the ID GitHub assigned, so Zoomies can delete the
	// registration later if the runner never comes up.
	RunnerID int64
	// Name echoes the requested name.
	Name string
}

JITConfig is the base64 blob handed to `actions-runner --jitconfig`. It registers an ephemeral runner exactly once and expires quickly, which is what makes it safe to pass through an environment variable.

type JITRequest

type JITRequest struct {
	// Name must be unique within the target. GitHub rejects reuse.
	Name string
	// Labels are the custom labels this runner advertises, without the
	// implicit self-hosted/os/arch set.
	Labels []string
	// RunnerGroupID is 1 (Default) unless the pool names a group.
	RunnerGroupID int64
	// WorkFolder defaults to "_work".
	WorkFolder string
}

JITRequest asks GitHub for a just-in-time runner configuration.

type ManifestCredentials

type ManifestCredentials struct {
	AppID int64
	Slug  string
	Name  string
	// PEM is the App's private key. It is shown once and must be sealed
	// immediately.
	PEM string
	// WebhookSecret is the secret GitHub generated for the App's webhook. It
	// is the only copy: the manifest cannot ask for a particular one, and
	// GitHub does not show it again.
	WebhookSecret string
	ClientID      string
	ClientSecret  string
	HTMLURL       string
}

ManifestCredentials is everything GitHub hands back once the operator has created the App. Every field except HTMLURL is a secret or identifies one.

func ExchangeManifestCode

func ExchangeManifestCode(ctx context.Context, apiBaseURL, code string) (*ManifestCredentials, error)

ExchangeManifestCode trades the temporary code GitHub redirected back with for the App's real credentials.

The code is valid for one hour and exactly one exchange, so a failure here means starting the manifest flow again rather than retrying.

type ManifestOptions

type ManifestOptions struct {
	// Name is the App's display name, unique across all of GitHub.
	Name string
	// URL is the App's homepage, shown on its GitHub page.
	URL string
	// WebhookURL is where GitHub delivers workflow_job events.
	WebhookURL string
	// Organization is the org the App is created under. Empty creates it on
	// the operator's own account, which is the right choice for repo targets.
	Organization string
	// SetupURL is permanent App configuration: GitHub sends the operator here
	// after every future install or reconfiguration of the App -- adding a
	// repository to the installation, most commonly, years later. It has to be
	// an address that still answers then.
	SetupURL string
	// RedirectURL is where GitHub returns during *this* handshake, carrying
	// ?code=. Empty means "the same as SetupURL", which is right when the
	// handshake is driven from the controller's own web UI.
	//
	// They are separate because the installer is not: it catches the callback
	// on a temporary loopback listener that is gone minutes later, and writing
	// that ephemeral port into setup_url left every App `zoomies init` ever
	// created sending its operator to a refused connection.
	RedirectURL string
	// Public allows the App to be installed on accounts other than the one
	// that created it. Zoomies defaults it off: a fleet controller's App has
	// no business being installable by strangers.
	Public bool
	// AllowWorkflowCancellation raises Actions from read to write so Zoomies
	// can cancel workflow runs. It is opt-in because write is not needed to run
	// the fleet.
	AllowWorkflowCancellation bool
}

ManifestOptions describes the App the installer asks GitHub to create.

type PullRequest

type PullRequest struct {
	Number  int    `json:"number"`
	HTMLURL string `json:"html_url"`
	Branch  string `json:"branch"`
}

PullRequest is what opening one produced.

type PullRequestRequest

type PullRequestRequest struct {
	// Repo is "owner/name".
	Repo string
	// Base is the branch to open against. Empty means the default branch.
	Base string
	// Head is the branch to create. It must not already exist.
	Head          string
	Title         string
	Body          string
	CommitMessage string
	Files         []FileChange
}

PullRequestRequest describes the pull request to open.

type QueuedJob

type QueuedJob struct {
	ID           int64
	RunID        int64
	Repo         string
	WorkflowName string
	JobName      string
	Labels       []string
	QueuedAt     time.Time
	HTMLURL      string
	// RunnerName is set once GitHub has assigned the job.
	RunnerName  string
	Status      string
	Conclusion  string
	StartedAt   *time.Time
	CompletedAt *time.Time
}

QueuedJob is a job the fallback poller found waiting. It carries only what the scheduler needs to match it to a pool.

type RateLimit

type RateLimit struct {
	Limit     int
	Remaining int
	ResetAt   time.Time
}

RateLimit reports the installation's remaining API quota, which the UI shows on the Installations page.

type RateLimitedError

type RateLimitedError struct {
	// ResetAt is when the quota refills, zero when GitHub did not say.
	ResetAt time.Time
	// RetryAfter is the secondary limit's answer, which is a duration rather
	// than an instant. Zero when unset.
	RetryAfter time.Duration
	Detail     string
}

RateLimitedError is a refusal that says when to come back.

GitHub sends the answer on every rate-limited response -- a reset instant on the primary limit, a retry-after duration on the secondary one -- and it used to be formatted straight into the message and lost, leaving every caller to guess a fixed wait. Guessing long wastes quota that came back minutes ago; guessing short spends the next window on refusals. So the number is carried.

It wraps ErrRateLimited, so the callers that only ask "was this a rate limit" keep working through errors.Is and only the ones that want the time reach for errors.As.

func (*RateLimitedError) Error

func (e *RateLimitedError) Error() string

func (*RateLimitedError) RetryAt

func (e *RateLimitedError) RetryAt(now time.Time) (time.Time, bool)

RetryAt is when a caller may try this installation again, given the time it is asking at, and whether GitHub said anything at all.

A reset already in the past is no answer -- clocks drift, and a response can sit in a queue -- so it is reported as unknown and the caller keeps its own default rather than resuming into the same refusal.

func (*RateLimitedError) Unwrap

func (e *RateLimitedError) Unwrap() error

type RegistrationToken

type RegistrationToken struct {
	Token     string
	ExpiresAt time.Time
}

RegistrationToken is the older credential, used by non-ephemeral pools that must run config.sh. It is valid for one hour.

type Repository

type Repository struct {
	FullName      string `json:"full_name"`
	DefaultBranch string `json:"default_branch"`
	Private       bool   `json:"private"`
	Archived      bool   `json:"archived"`
	HTMLURL       string `json:"html_url"`
}

Repository is one repository an installation can see.

type Runner

type Runner struct {
	ID     int64
	Name   string
	OS     string
	Status string // online | offline
	Busy   bool
	Labels []string
	// Ephemeral is reported by GitHub for JIT-configured runners.
	Ephemeral bool
}

Runner is a runner as GitHub sees it, used to reconcile Zoomies' view with reality and to clean up registrations Zoomies has lost track of.

type RunnerGroup

type RunnerGroup struct {
	ID         int64
	Name       string
	Default    bool
	Visibility string
	// PublicRepositoryAccessKnown distinguishes an older GitHub Enterprise
	// response that omitted the setting from a group that explicitly disables
	// it. Treating both as false would raise a warning nobody could act on.
	PublicRepositoryAccessKnown bool
	AllowsPublicRepositories    bool
	RestrictedToWorkflows       bool
}

RunnerGroup is a runner group in the target org.

type RunnerGroupCreate

type RunnerGroupCreate struct {
	Name                     string
	Visibility               string
	AllowsPublicRepositories bool
}

RunnerGroupCreate is the policy for a new organisation runner group.

type WorkflowFile

type WorkflowFile struct {
	// Path is repository-relative.
	Path string
	// SHA is the blob SHA. GitHub requires it to update the file, and it is
	// what makes an update fail rather than clobber somebody's change.
	SHA string
	// Content is the decoded file.
	Content string
}

WorkflowFile is one file under .github/workflows as it exists on the repository's default branch.

type WorkflowJobEvent

type WorkflowJobEvent struct {
	// Action is "queued", "in_progress", "completed" or "waiting".
	Action string
	// InstallationID routes the delivery to one of Zoomies' installations.
	InstallationID int64

	JobID        int64
	RunID        int64
	Repo         string // "acme/widgets"
	WorkflowName string
	JobName      string
	// Labels are the workflow's runs-on values, including implicit ones.
	Labels     []string
	RunnerID   int64
	RunnerName string
	Status     string
	Conclusion string

	// QueuedAt is the job's created_at, i.e. when GitHub started looking for a
	// runner.
	QueuedAt    time.Time
	StartedAt   time.Time
	CompletedAt time.Time
	HTMLURL     string

	// HeadBranch, HeadSHA and RunAttempt say what the job ran for.
	HeadBranch string
	HeadSHA    string
	RunAttempt int
	// Steps are the job's steps as this delivery reports them. On a completed
	// delivery every step carries its conclusion, which is what lets a failed
	// job say where it failed.
	Steps []store.JobStep
}

WorkflowJobEvent is the part of a workflow_job delivery Zoomies acts on.

It is a hand-written struct rather than go-github's event type because this payload is the load-bearing input of the whole scheduler: naming exactly the fields that matter keeps it obvious what a change to GitHub's payload would break.

func ParseWorkflowJob

func ParseWorkflowJob(payload []byte) (*WorkflowJobEvent, error)

ParseWorkflowJob decodes a workflow_job delivery.

func (*WorkflowJobEvent) State

func (e *WorkflowJobEvent) State() store.JobState

State maps the delivery's action onto the job state machine.

"waiting" means the job is held for a deployment review, which can take hours or days, and GitHub sends a real "queued" delivery once it is approved. It is its own state, ahead of queued in the lifecycle so the approval moves the job forward, and one the scheduler does not count: recording it as queued started a runner that idled out and was started again on the next pass, for as long as the review took.

func (*WorkflowJobEvent) ToJob

func (e *WorkflowJobEvent) ToJob() *store.Job

ToJob maps the event onto the row the store keeps, stamping the timestamps that this transition is the first evidence of.

The returned Job has no ID: Store.UpsertJob keys on GitHubJobID and mints one for a job it has not seen before.

Jump to

Keyboard shortcuts

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