Documentation
¶
Overview ¶
Package github is the conductor's client of a GitHub App: it turns an App id and a private key into the short-lived installation tokens a turn clones, fetches and pushes with.
Why an App rather than the personal access token a playbook's `secrets:` can already carry: an installation token lasts an hour, is scoped to named repositories, belongs to no human, and is revoked centrally by uninstalling the App. A PAT is none of those — it is long-lived, it carries one person's access, and revoking it is somebody remembering to.
The cost is that a token has to be MINTED, and minting needs the private key. That key can mint a token for every repository the App is installed on, so it is strictly more valuable than the PAT it replaces and it lives in exactly one place: the conductor's own host, beside the master key. Nothing hands it to a task. See docs/security.md.
Nothing here caches a token past its usefulness and nothing here logs one.
Index ¶
Constants ¶
const DefaultBaseURL = "https://api.github.com"
DefaultBaseURL is github.com's API. GitHub Enterprise Server lives on another host and is not supported: Podium speaks to one GitHub, and pretending otherwise would be a configuration knob with no tested path behind it.
const DefaultTimeout = 15 * time.Second
DefaultTimeout bounds one call. Minting is two round trips at worst and both are small.
Variables ¶
var ErrNotFound = errors.New("github: not found")
ErrNotFound is a 404. It is internal to this package's own retry between the org and user installation endpoints; callers see ErrNotInstalled instead.
var ErrNotInstalled = errors.New("the app is not installed on that account")
ErrNotInstalled is a repository owner the App is not installed on. It means the operator created the App and never installed it, or installed it on another account — the most common way this is set up wrong, and unrecoverable without a human.
ErrUnauthorized is GitHub refusing the App's own credential: a wrong app id, a key that does not match it, or an App that has been deleted. It is separated because it is the one failure an operator fixes by editing configuration rather than by reading a log.
Functions ¶
func ParsePrivateKey ¶
func ParsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error)
ParsePrivateKey reads the PEM GitHub hands out. GitHub generates PKCS#1 ("BEGIN RSA PRIVATE KEY"); PKCS#8 is accepted too, because a key round-tripped through openssl or a secret manager often comes back in that form and refusing it would be a puzzle rather than a message.
func SplitRepoURL ¶
SplitRepoURL takes a playbook's repos[].url apart into the owner and the bare repository name the token call wants.
It accepts what an operator actually writes — with or without .git, with or without a trailing slash — and refuses anything that is not github.com, because a token minted by this App is worthless anywhere else and a silent mismatch would show up as a 404 on the clone rather than as a message about the URL.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client mints installation tokens for one GitHub App.
It is safe for concurrent use and it caches: an installation id never changes, the bot identity never changes, and a token is reused until renewBefore of its expiry. A conductor running twenty turns of one playbook mints once, not twenty times, which is what keeps this off GitHub's rate limit.
func (*Client) Identity ¶
Identity is the App's bot account, as an author. It is two calls the first time and cached for ever after: neither the slug nor the id of a bot account changes.
func (*Client) Token ¶
Token is an installation token for exactly the named repositories of one owner.
repos are bare repository names ("monorepo"), not "owner/repo": GitHub's parameter is scoped to the installation, which already fixes the owner. An empty list is refused rather than sent, because GitHub reads "no repositories" as EVERY repository the installation has, and a turn that asked for nothing getting everything is the wrong way round for a default.
type Identity ¶
Identity is the App's bot account as git and GitHub see it.
The email is the whole point of the type. GitHub links a commit to an ACCOUNT by the author's email, and a bot account's address is `<user id>+<slug>[bot]@users.noreply. github.com` — an id nothing but the API will tell you. Get it wrong and every commit is attributed to nobody, which is the failure this whole path exists to avoid.
type Options ¶
type Options struct {
// AppID is the App's numeric id, as GitHub's settings page shows it.
AppID string
// PrivateKeyPEM is the PEM the App's "generate a private key" button produced.
PrivateKeyPEM []byte
// BaseURL overrides DefaultBaseURL. It exists for tests.
BaseURL string
// HTTPClient overrides the default. It exists for tests.
HTTPClient *http.Client
// Now overrides the clock. It exists for tests.
Now func() time.Time
}
Options builds a Client.