github

package
v0.9.1 Latest Latest
Warning

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

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

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

View Source
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.

View Source
const DefaultTimeout = 15 * time.Second

DefaultTimeout bounds one call. Minting is two round trips at worst and both are small.

View Source
const DefaultWebURL = "https://github.com"

DefaultWebURL is where GitHub's OAuth endpoints live.

Variables

View Source
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.

View Source
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.

View Source
var ErrRefreshRefused = errors.New("github refused the refresh token")

ErrRefreshRefused is GitHub refusing a refresh token: it expired, was already used, or the person revoked the App. Only reconnecting fixes it.

View Source
var ErrUnauthorized = errors.New("github refused the app credential")

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

func SplitRepoURL(raw string) (owner, repo string, err error)

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 New

func New(opts Options) (*Client, error)

New returns a Client, or an error naming what is missing.

func (*Client) Identity

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

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

func (c *Client) Token(ctx context.Context, owner string, repos []string) (Token, error)

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

type Identity struct {
	Name  string
	Email string
}

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.

type Token

type Token struct {
	Value     string
	ExpiresAt time.Time
}

Token is one installation token and the moment it stops working.

type UserToken added in v0.6.0

type UserToken struct {
	AccessToken      string    `json:"access_token"`
	RefreshToken     string    `json:"refresh_token"`
	ExpiresAt        time.Time `json:"expires_at"`
	RefreshExpiresAt time.Time `json:"refresh_token_expires_at,omitzero"`
}

UserToken is a person's connected account as Podium stores it, the value of their github.token secret. The access token lives eight hours and the refresh token six months; a refresh replaces both, and the old refresh token stops working.

func ParseUserToken added in v0.6.0

func ParseUserToken(res *http.Response, now time.Time) (UserToken, error)

ParseUserToken reads GitHub's answer from /login/oauth/access_token, for a code or a refresh. GitHub reports a refused grant as 200 with an error field.

type UserTokens added in v0.6.0

type UserTokens struct {
	ClientID     string
	ClientSecret string
	// BaseURL overrides DefaultBaseURL, and WebURL DefaultWebURL. They exist for tests.
	BaseURL string
	WebURL  string
	// HTTPClient overrides a client with DefaultTimeout. It exists for tests.
	HTTPClient *http.Client
}

UserTokens narrows and refreshes a person's connected-account token, as the App's OAuth client. A turn never holds the person's own token: it gets one scoped to its playbook's repositories, which the conductor revokes when the turn is over.

func (UserTokens) Refresh added in v0.6.0

func (u UserTokens) Refresh(ctx context.Context, refreshToken string) (UserToken, error)

Refresh trades a refresh token for a new access token and refresh token. The one it was given stops working, so the caller must store what comes back.

func (UserTokens) Revoke added in v0.6.0

func (u UserTokens) Revoke(ctx context.Context, token string) error

Revoke ends one token. A token GitHub no longer knows is already revoked.

func (UserTokens) Scope added in v0.6.0

func (u UserTokens) Scope(ctx context.Context, userToken, owner string, repos []string) (Token, error)

Scope exchanges userToken for one that reaches only repos of owner, with contents and pull requests write. ExpiresAt is zero when GitHub gave the token no expiry, which is what a non-expiring user token yields: the caller has to revoke it. A user token GitHub no longer accepts is ErrNotFound.

Jump to

Keyboard shortcuts

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