Documentation
¶
Overview ¶
Package github implements provider.Provider for github.com and GitHub Enterprise Server. See ADR-0021 for the implementation choices that differ from the GitLab provider (HMAC-signed webhooks, owner/repo split, etc.).
Index ¶
- Variables
- func LoadGhToken(host string) (token string, err error)
- type Config
- type Provider
- func (p *Provider) AuthenticatedUser(ctx context.Context) (provider.User, error)
- func (p *Provider) CloseMR(ctx context.Context, projectID string, mrIID int) error
- func (p *Provider) CreateMR(ctx context.Context, projectID string, draft provider.MRDraft) (provider.MR, error)
- func (p *Provider) DeregisterWebhook(ctx context.Context, projectID, webhookID string) error
- func (p *Provider) GetMRState(ctx context.Context, projectID string, mrIID int) (provider.MRState, error)
- func (p *Provider) IsBot(u provider.User) bool
- func (p *Provider) ListNotesSince(ctx context.Context, projectID string, mrIID int, since provider.NoteCursor) ([]provider.NotePoll, error)
- func (p *Provider) Name() string
- func (p *Provider) NormaliseEvent(headers http.Header, body []byte) (provider.Event, error)
- func (p *Provider) PostComment(ctx context.Context, projectID string, mrIID int, body string) error
- func (p *Provider) ReactToNote(ctx context.Context, projectID string, _ int, noteID int64, ...) error
- func (p *Provider) RegisterWebhook(ctx context.Context, projectID, callbackURL, secret string, ...) (string, error)
- func (p *Provider) ReplyToDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string, ...) error
- func (p *Provider) ResolveDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string) error
- func (p *Provider) RetryPipelineJob(ctx context.Context, projectID string, jobID int64) error
- func (p *Provider) UpdateMRDescription(ctx context.Context, projectID string, mrIID int, description string) error
- func (p *Provider) UpdateMRTitle(ctx context.Context, projectID string, mrIID int, title string) error
- func (p *Provider) VerifySignature(headers http.Header, body []byte, secret string) bool
Constants ¶
This section is empty.
Variables ¶
var ErrGhNotConfigured = errors.New("gh: no token configured for host")
ErrGhNotConfigured signals that `gh` is unavailable or not logged into the requested host. Caller can fall through to a GITHUB_TOKEN env var.
Functions ¶
func LoadGhToken ¶
LoadGhToken returns the OAuth token the `gh` CLI is currently using for the given host — the same token `gh auth status` shows. Useful for spike / personal-laptop deployments where the user has already done `gh auth login` and doesn't want to mint a separate PAT. Production deployments should still use a service-account PAT via GITHUB_TOKEN.
Unlike glab (which stores its token in a plain YAML config we can read directly), gh on macOS stores the token in the system keychain and exposes it only via the `gh auth token` subcommand. We shell out rather than poke at the keychain ourselves — it's the supported surface and is portable to Linux/Windows where gh uses different backends. Same approach as the rest of the daemon: shell out to host tools (git, claude) rather than reimplement them.
Returns ErrGhNotConfigured if `gh` isn't on $PATH, isn't logged in for the given host, or returned an empty token. Callers can fall through to a GITHUB_TOKEN env var.
Types ¶
type Config ¶
type Config struct {
BaseURL string // defaults to https://api.github.com
// Token is a static classic PAT, fine-grained PAT, or App installation
// token. Required unless TokenSource is set.
Token string
// TokenSource, if set, takes precedence over Token and is called to
// resolve the token fresh on every request instead of caching one at
// construction time. Use this for `gh auth login`'s OAuth token: `gh
// auth token` itself always shells out live (see LoadGhToken) and
// handles gh's own refresh, but a Provider built from a one-time
// Token snapshot would still only ever see whatever was valid at
// daemon startup (see ADR-0063/0065, the GitLab-side version of this
// same caching bug).
TokenSource func() (string, error)
Timeout time.Duration // defaults to 30s per request
}
Config wires a Provider.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider is the GitHub implementation of provider.Provider.
func (*Provider) AuthenticatedUser ¶
AuthenticatedUser → GET /user. GitHub returns type=User|Bot|Organization and a login field which we treat as the canonical handle.
func (*Provider) CreateMR ¶
func (p *Provider) CreateMR(ctx context.Context, projectID string, draft provider.MRDraft) (provider.MR, error)
CreateMR → POST /repos/{owner}/{repo}/pulls. GitHub calls them PRs in the UI; the API uses both terms. Labels are applied as a follow-up call because the PR creation endpoint does not accept them.
func (*Provider) DeregisterWebhook ¶
DeregisterWebhook → DELETE /repos/{owner}/{repo}/hooks/{hook_id}. 404 is success (already gone).
func (*Provider) GetMRState ¶
func (p *Provider) GetMRState(ctx context.Context, projectID string, mrIID int) (provider.MRState, error)
GetMRState reads the PR's state ("open" | "closed"; merged is "closed" with merged_at set). Returns the GitLab-style state vocabulary mapping for callers that don't care about the merged-vs-just-closed distinction. GetMRState → GET /repos/{owner}/{repo}/pulls/{number}. Returns one of "opened" | "closed" | "merged" to match the poller's state-event vocabulary (see internal/poller/poller.go mrStateEvent), plus whether the PR has a merge conflict — from the same response, no extra request.
GitHub's REST response has a `state` field ("open"|"closed") and a separate `merged` boolean; we collapse them into the same three strings GitLab returns so the poller can stay provider-agnostic. `mergeable_state` is "dirty" when the PR conflicts with its base branch; it's also null/absent while GitHub is still computing mergeability, which we treat as "no conflict (yet)" rather than guessing.
func (*Provider) IsBot ¶
IsBot covers GitHub Apps (Type=Bot) and the trailing `[bot]` username convention used by integrations like dependabot, renovate, and codecov.
func (*Provider) ListNotesSince ¶
func (p *Provider) ListNotesSince(ctx context.Context, projectID string, mrIID int, since provider.NoteCursor) ([]provider.NotePoll, error)
ListNotesSince fetches new comments on a PR across GitHub's three comment streams and returns them merged + sorted by ID ascending:
- issue_comment → /repos/.../issues/{n}/comments (PR conversation)
- pull_request_review → /repos/.../pulls/{n}/reviews (top-level reviews)
- pull_request_review_comment → /repos/.../pulls/{n}/comments (inline line comments)
Each stream is filtered against its own watermark in `since`, falling back to since.Legacy for any stream not yet tracked individually. See provider.NoteCursor and ADR-0041 for why a single shared watermark is wrong here: mixing the three streams' ids into one scalar can cause a lower-id comment on one stream to be silently and permanently dropped after a higher-id comment arrives on a different stream.
Only inline review comments carry a `node_id` we can hand to ResolveDiscussion; the other two come back with DiscussionID="". Body-less reviews ("approved" with no comment) are filtered out to match NormaliseEvent's webhook semantics — no actionable content for the subagent.
Pagination cap: 100 per endpoint per tick. For dogfood / personal use this is fine; if a single 30s window ever sees >100 new comments across all streams we'll need to paginate.
func (*Provider) NormaliseEvent ¶
NormaliseEvent decodes a GitHub webhook POST into provider.Event. Routes by the X-GitHub-Event header. Returns provider.ErrIgnore for event kinds we did not subscribe to or sub-actions we don't care about.
GitHub's three comment events (issue_comment, pull_request_review, pull_request_review_comment) all collapse onto provider.EventNoteAdded. We only surface body-bearing reviews; "approved" reviews with no body are skipped (no actionable content for a subagent).
func (*Provider) PostComment ¶
PostComment → POST /repos/{owner}/{repo}/issues/{number}/comments. Issue comments cover non-review-line MR comments — exactly what we want for status updates and the author's /everflow control conversation.
func (*Provider) ReactToNote ¶
func (p *Provider) ReactToNote(ctx context.Context, projectID string, _ int, noteID int64, stream, emoji string) error
ReactToNote adds a reaction to a comment. GitHub's reactions API is keyed per comment type, so which endpoint we hit depends on stream:
- streamIssueComment → POST /repos/{o}/{r}/issues/comments/{id}/reactions
- streamReviewComment → POST /repos/{o}/{r}/pulls/comments/{id}/reactions
- streamReview → no reactions endpoint exists for top-level PR reviews; this is a no-op (nil), not an error — see ADR-0050.
func (*Provider) RegisterWebhook ¶
func (p *Provider) RegisterWebhook(ctx context.Context, projectID, callbackURL, secret string, kinds []provider.EventKind) (string, error)
RegisterWebhook → POST /repos/{owner}/{repo}/hooks. GitHub's `events` field takes a list of event names; we translate provider.EventKind into the union of GitHub event names that map onto each.
func (*Provider) ReplyToDiscussion ¶
func (p *Provider) ReplyToDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string, body string) error
ReplyToDiscussion → POST /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies.
Unlike GitLab's opaque discussion_id, GitHub's replies endpoint requires the numeric review-comment ID (the `id` field on a pull_request_review_comment, e.g. as returned in ListNotesSince's streamReviewComment entries) — not the GraphQL node ID used as DiscussionID by ResolveDiscussion below. Callers must pass the numeric ID as a string; a non-numeric discussionID is an error rather than a silent no-op.
func (*Provider) ResolveDiscussion ¶
func (p *Provider) ResolveDiscussion(ctx context.Context, projectID string, mrIID int, discussionID string) error
ResolveDiscussion marks a GitHub pull-request review thread as resolved via the GraphQL `resolveReviewThread` mutation.
The discussionID we receive from the inbound webhook decoder is the pull_request_review_comment's GraphQL node_id (see events.go). PullRequestReviewComment has no field linking back to its parent PullRequestReviewThread, so we can't look the thread up directly from the comment node. Instead we list the PR's review threads (each with its comments) and find the thread whose comments contain our node ID.
func (*Provider) RetryPipelineJob ¶
RetryPipelineJob → POST /repos/{owner}/{repo}/actions/jobs/{job_id}/rerun. Works only for GitHub Actions workflow jobs; external CI (Jenkins, Circle) emits check_run events but does not support rerun via this endpoint. In that case the caller will get a 404 and should fall back to asking the author for help.
We don't currently pass through projectID via the interface because jobID is globally unique within GitHub; we'd need to thread it through. Workaround for now: assume the daemon has one GH project per Run, which is true post-trigger because AgentState.ProjectID is set. Callers that want this method to work for GH must pass owner/repo via ProjectID.
func (*Provider) UpdateMRDescription ¶
func (p *Provider) UpdateMRDescription(ctx context.Context, projectID string, mrIID int, description string) error
UpdateMRDescription → PATCH /repos/{owner}/{repo}/pulls/{number}.
func (*Provider) UpdateMRTitle ¶
func (p *Provider) UpdateMRTitle(ctx context.Context, projectID string, mrIID int, title string) error
UpdateMRTitle → PATCH /repos/{owner}/{repo}/pulls/{number}.
func (*Provider) VerifySignature ¶
VerifySignature checks the X-Hub-Signature-256 header against an HMAC-SHA256 of the body, keyed by the registered secret. Constant-time comparison. Unlike GitLab, this is real cryptographic signing.