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
- Variables
- func BranchName(now time.Time) string
- func InstallURL(htmlURL string) string
- func IsEnterprise(apiBaseURL string) bool
- func IsPing(event string) bool
- func Manifest(o ManifestOptions) ([]byte, error)
- func ManifestURL(apiBaseURL, org string) string
- func NormalizeAPIBaseURL(raw string) (string, error)
- func ParseEventType(header string) string
- func RetryAfterRateLimit(err error, now time.Time) (time.Time, bool)
- func RunnerName(pool *store.Pool) string
- func SettingsURL(apiBaseURL, slug, org string) string
- func SplitTarget(target string) (owner, repo string, kind store.TargetType)
- func ValidateSignature(payload []byte, header, secret string) error
- func WebURLForAPI(apiBaseURL string) string
- type AppFactory
- type AppInfo
- type Client
- type Factory
- type FakeGitHub
- func (f *FakeGitHub) AddFile(repo, path, content string)
- func (f *FakeGitHub) AddQueuedJob(repo, workflow, jobName string, labels []string) QueuedJob
- func (f *FakeGitHub) AddRepo(fullName string)
- func (f *FakeGitHub) AddRunner(name string, labels []string) Runner
- func (f *FakeGitHub) AddRunnerGroup(name string) RunnerGroup
- func (f *FakeGitHub) AddWorkflow(repo, path, content string)
- func (f *FakeGitHub) AppID() int64
- func (f *FakeGitHub) Branches(repo string) []string
- func (f *FakeGitHub) ClearErrors()
- func (f *FakeGitHub) Client(target string, kind store.TargetType) Client
- func (f *FakeGitHub) Close()
- func (f *FakeGitHub) CompleteJob(jobID int64, conclusion string)
- func (f *FakeGitHub) FileContent(repo, path string) (string, bool)
- func (f *FakeGitHub) InstallationID() int64
- func (f *FakeGitHub) PullRequestBody(repo string, number int) string
- func (f *FakeGitHub) Requests() []string
- func (f *FakeGitHub) Runners() []Runner
- func (f *FakeGitHub) Server() *httptest.Server
- func (f *FakeGitHub) SetArchived(repo string, archived bool)
- func (f *FakeGitHub) SetDefaultBranch(repo, branch string)
- func (f *FakeGitHub) SetError(pattern string, status int, message string)
- func (f *FakeGitHub) SetEvents(events ...string)
- func (f *FakeGitHub) SetMethodError(method, pattern string, status int, message string)
- func (f *FakeGitHub) SetPermissions(p map[string]string)
- func (f *FakeGitHub) SetRateLimit(limit, remaining int, reset time.Time)
- func (f *FakeGitHub) SetRepositorySelection(selection string)
- func (f *FakeGitHub) SetRunnerGroupPublicAccess(name string, allowed bool)
- func (f *FakeGitHub) StartJob(jobID int64, runnerName string)
- func (f *FakeGitHub) URL() string
- type FileChange
- type JITConfig
- type JITRequest
- type ManifestCredentials
- type ManifestOptions
- type PullRequest
- type PullRequestRequest
- type QueuedJob
- type RateLimit
- type RateLimitedError
- type Readme
- type RegistrationToken
- type Repository
- type Runner
- type RunnerGroup
- type RunnerGroupCreate
- type WorkflowFile
- type WorkflowJobEvent
Constants ¶
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.
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 ¶
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.
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".
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.
var ErrNoReadme = errors.New("github: the repository has no README")
ErrNoReadme is returned when a repository has no README, which is not a failure either: the badge has nowhere to go, and the migration goes ahead without it.
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.
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.
var ErrNotFound = errors.New("github: not found")
ErrNotFound is returned when GitHub reports a 404 for something Zoomies expected to exist.
var ErrNotWorkflowJob = errors.New("github: not a workflow_job payload")
ErrNotWorkflowJob is returned when a payload is not a workflow_job event.
var ErrRateLimited = errors.New("github: rate limited")
ErrRateLimited is returned when the installation has exhausted its quota. The caller backs off rather than hammering.
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 ¶
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 ¶
InstallURL returns the page that installs an App on an account, given the html_url the manifest conversion returned.
func IsEnterprise ¶
IsEnterprise reports whether an API base URL points at GitHub Enterprise Server rather than github.com.
func IsPing ¶
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 ¶
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 ¶
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 ¶
ParseEventType normalises the X-GitHub-Event header. GitHub sends it lowercase, but proxies have been known to rewrite header values.
func RetryAfterRateLimit ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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)
// ReadReadme returns the README GitHub renders on the repository's front
// page, or ErrNoReadme when there is none. It is where the badge goes.
ReadReadme(ctx context.Context, repo string) (*Readme, 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) AddFile ¶
func (f *FakeGitHub) AddFile(repo, path, content string)
AddFile puts any file in a repository, creating the repository if this is the first thing in it. It is AddWorkflow under a name that does not lie about a README.
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) 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) PullRequestBody ¶
func (f *FakeGitHub) PullRequestBody(repo string, number int) string
PullRequestBody returns the body a pull request was opened with, or "" for one that was never opened.
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 ¶
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 ¶
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 Readme ¶
type Readme struct {
// Path is repository-relative -- "README.md", but also "readme.markdown"
// or "README.rst", which is why the caller checks it before writing
// Markdown into it.
Path string
// SHA is the blob SHA, for the same reason a workflow file carries one.
SHA string
// Content is the decoded file.
Content string
}
Readme is a repository's README as GitHub finds it: whichever file at the root it would render on the repository's front page, whatever its case or extension.
type RegistrationToken ¶
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 ¶
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.