github

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 16 Imported by: 0

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

Constants

This section is empty.

Variables

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

func LoadGhToken(host string) (token string, err error)

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 New

func New(cfg Config) (*Provider, error)

New constructs a Provider. Fails fast if neither Token nor TokenSource is set.

func (*Provider) AuthenticatedUser

func (p *Provider) AuthenticatedUser(ctx context.Context) (provider.User, error)

AuthenticatedUser → GET /user. GitHub returns type=User|Bot|Organization and a login field which we treat as the canonical handle.

func (*Provider) CloseMR

func (p *Provider) CloseMR(ctx context.Context, projectID string, mrIID int) error

CloseMR → PATCH /repos/{owner}/{repo}/pulls/{number} with state=closed.

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

func (p *Provider) DeregisterWebhook(ctx context.Context, projectID, webhookID string) error

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

func (p *Provider) IsBot(u provider.User) bool

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) Name

func (p *Provider) Name() string

func (*Provider) NormaliseEvent

func (p *Provider) NormaliseEvent(headers http.Header, body []byte) (provider.Event, error)

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

func (p *Provider) PostComment(ctx context.Context, projectID string, mrIID int, body string) error

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

func (p *Provider) RetryPipelineJob(ctx context.Context, projectID string, jobID int64) error

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

func (p *Provider) VerifySignature(headers http.Header, body []byte, secret string) bool

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.

Jump to

Keyboard shortcuts

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