github

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: May 6, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package github's adapter.go bridges the inner Plugin to the generic source.Plugin contract that PR-70 introduces. Same shape as the markdown adapter: a thin pointer-wrapper that opts into source.Sincer and source.Completer at compile time so the registry's ValidateAgainstManifest cross-check can prove "the manifest declares since/complete and the adapter implements them" before the daemon dispatches its first call.

Package github implements the GitHub Discovery source plugin promised in docs/pr_split_plan.md PR-83. The plugin surfaces issues and pull requests assigned to the current user (`is:open assignee:@me`) through the generic source.Plugin contract, using the user's `gh` CLI for both authentication and IO so PR-83 inherits gh's auth handling rather than re-implementing OAuth.

Why shell out to `gh` rather than talk to the REST/GraphQL API directly:

  • gh already has a robust credential flow (`gh auth login`, OAuth refresh, keyring integration) that the daemon would otherwise have to duplicate.
  • Users already have gh installed for marunage doctor's GitHub source check, so there is no extra dependency.
  • The Runner abstraction (runner.go) keeps the dependency injection story identical to internal/cmux/runner.go, so a future PR that wants to swap in the API client can do so without touching every call site.

Concurrency: the Plugin holds no mutable state — every method receives a context and forwards it into the injected Runner — so callers may share a single instance across goroutines without external locking.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidResponse is returned when `gh` produces output that does not
	// match the expected JSON shape. The package wraps the underlying parser
	// error so a debugging session can still see what gh sent back.
	ErrInvalidResponse = errors.New("github: invalid gh response")

	// ErrInvalidExternalID is returned by Complete when the supplied id does
	// not match the documented `<owner>/<repo>#<number>` shape. We surface
	// this before invoking the runner so a malformed id cannot silently
	// trigger a `gh issue close` against an unrelated repo.
	ErrInvalidExternalID = errors.New("github: invalid external id")

	// ErrInteractiveSetupRequired is returned by Setup when authentication
	// is missing. The Runner abstraction cannot drive `gh auth login` (which
	// requires a TTY), so the right answer is to defer to the user.
	ErrInteractiveSetupRequired = errors.New("github: interactive setup required (run `gh auth login`)")

	// ErrInvalidCheckpoint is returned by Since when the supplied checkpoint
	// is not a strict RFC3339 timestamp. Rejecting before concatenation into
	// the gh search query closes the door on injection of additional
	// qualifiers (e.g. `... author:victim`) by a daemon that mishandles
	// stored state.
	ErrInvalidCheckpoint = errors.New("github: invalid checkpoint (expected RFC3339)")
)

Typed sentinel errors. Callers branch on errors.Is rather than parsing strings; the CLI / daemon layer maps these to documented exit codes.

Functions

func Manifest

func Manifest() (*source.Manifest, error)

Manifest returns the parsed view of the bundled plugin.toml. The bytes flow through source.LoadManifestFromBytes so the embedded payload is validated by the same pipeline as on-disk manifests, without the I/O failure modes a tempfile detour would add. Validation runs on every call rather than at package init so a malformed manifest surfaces to the caller (typically tests or RegisterBuiltin) rather than crashing every binary that links the package.

func RegisterBuiltin

func RegisterBuiltin(r *source.Registry, opts ...Option) error

RegisterBuiltin constructs an Adapter wrapping a fresh Plugin (configured with opts) and registers it in r. It also runs the capability/interface cross-check against the bundled manifest so any drift between the embedded TOML and the adapter's actual interfaces is caught at startup rather than at first dispatch.

opts forward to New, so callers pass WithRunner / WithCompletionComment here exactly as they would for a directly-constructed Plugin. Returns source.ErrPluginAlreadyRegistered if r already has a "github" entry; the registry's duplicate guard is the single source of truth.

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter wraps a *Plugin and exposes it as a source.Plugin (plus Sincer / Completer). The struct holds a pointer so the adapter and any direct caller of the inner Plugin share state — though today Plugin has no mutable state, the pointer keeps the door open for a future cache or rate-limiter without a churny call-site change.

func NewAdapter

func NewAdapter(p *Plugin) *Adapter

NewAdapter wraps p. p MUST be a fully-configured *Plugin (typically from New(WithRunner(...))); we deliberately do not accept Option values here so the configuration knobs stay on the inner type and the adapter cannot drift out of sync with them.

func (*Adapter) AuthStatus

func (a *Adapter) AuthStatus(ctx context.Context) (source.AuthStatus, error)

AuthStatus forwards to the inner Plugin.

func (*Adapter) Complete

func (a *Adapter) Complete(ctx context.Context, externalID string) error

Complete forwards by externalID. The "owner/repo#number" parse and gh invocation live on the inner Plugin so a future re-implementation that uses GitHub's API directly does not need to touch this file.

func (*Adapter) List

func (a *Adapter) List(ctx context.Context) ([]source.Task, error)

List forwards to the inner Plugin. The conversion to source.Task already happens inside Plugin.List, so the adapter is a pure passthrough.

func (*Adapter) Name

func (a *Adapter) Name() string

Name reports the canonical plugin identifier. The constant is returned directly (rather than forwarded to a.inner.Name()) to mirror the markdown adapter's style and to keep the Adapter usable in places that hold a nil inner pointer for compile-time interface checks.

func (*Adapter) Setup

func (a *Adapter) Setup(ctx context.Context, opts source.SetupOptions) error

Setup forwards opts as-is. The inner Plugin honours NonInteractive; passing the struct through keeps a future opt addition compile-checked.

func (*Adapter) Since

func (a *Adapter) Since(ctx context.Context, checkpoint string) ([]source.Task, error)

Since forwards the checkpoint string verbatim. The inner Plugin interprets an empty checkpoint as "first run, return everything" and a non-empty one as the RFC3339 high-water mark for the gh `updated:>=` qualifier.

type ExecRunner

type ExecRunner struct{}

ExecRunner is the production Runner backing every gh call. It defers PATH lookup to os/exec so $PATH overrides (Homebrew gh, a custom build under ~/.local/bin) are honoured without any extra configuration.

func (ExecRunner) Run

func (ExecRunner) Run(ctx context.Context, name string, args ...string) ([]byte, []byte, error)

Run shells out to name with args, capturing stdout and stderr into independent buffers. Errors from cmd.Run are returned verbatim; callers can `errors.Is(err, exec.ErrNotFound)` to detect a missing binary, but AuthStatus deliberately treats every error path identically (see the godoc on Plugin.AuthStatus) so PR-83 itself never branches on the type.

type Option

type Option func(*Plugin)

Option is the functional-option shape New accepts. Mirrors the pattern in internal/source/markdown so callers see a consistent style across the codebase.

func WithCompletionComment

func WithCompletionComment(comment string) Option

WithCompletionComment configures Complete to post `comment` to the issue before closing it. Empty string (the default) skips the comment step.

func WithRunner

func WithRunner(r Runner) Option

WithRunner injects a custom Runner. Tests pass a fake; production callers leave this unset and get the real ExecRunner.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the GitHub Discovery source. Construct one with New and reuse across goroutines: the struct holds only its dependencies.

func New

func New(opts ...Option) *Plugin

New constructs a Plugin with the given options. Defaults (Runner = ExecRunner) are filled in here so options can override them.

func (*Plugin) AuthStatus

func (p *Plugin) AuthStatus(ctx context.Context) (source.AuthStatus, error)

AuthStatus probes `gh auth status`. A zero exit means the user is logged in; any other outcome (non-zero exit, gh missing) is downgraded to AuthNotConfigured rather than a hard error. The CLI layer renders this into a user-facing "run gh auth login" hint without ever surfacing a Go error to the caller — same mapping internal/doctor uses for missing optional binaries.

PR-83 deliberately collapses gh's "expired" / "revoked" / "never logged in" outcomes into a single AuthNotConfigured: gh's stderr wording is not a stable contract and a misclassification would silently downgrade a real revocation into a "not configured" hint, which is the same user action anyway (`gh auth login`). A future PR that wants to plumb AuthExpired through can parse the captured stderr and branch here without changing the call sites.

func (*Plugin) Complete

func (p *Plugin) Complete(ctx context.Context, externalID string) error

Complete closes the GitHub issue identified by externalID. When a completion comment is configured, it is posted BEFORE the close so the audit trail records "marunage closed this — here is why" rather than a silent state change.

PRs are out of scope for Phase 1: `gh issue close` will return an error if externalID points at a PR, and the wrapped error surfaces unchanged to the caller. A future PR that wants to close PRs would extend this to dispatch on RawMetadata["type"] — but that requires plumbing the type through the call site, which is not in PR-83's scope.

func (*Plugin) List

func (p *Plugin) List(ctx context.Context) ([]source.Task, error)

List returns every open item (issue or PR) currently assigned to the authenticated user. Internally List runs `gh search issues` and `gh search prs` back-to-back; we deliberately do not parallelise the two because the daemon caller already runs sources concurrently and a stable ordering keeps Task slices diffable across runs.

func (*Plugin) Name

func (p *Plugin) Name() string

Name reports the canonical plugin identifier.

func (*Plugin) Setup

func (p *Plugin) Setup(ctx context.Context, _ source.SetupOptions) error

Setup verifies authentication. If gh is already authenticated, Setup is a no-op (idempotent); otherwise it returns ErrInteractiveSetupRequired because the Runner abstraction cannot drive `gh auth login`'s TTY-bound flow.

func (*Plugin) Since

func (p *Plugin) Since(ctx context.Context, checkpoint string) ([]source.Task, error)

Since returns items updated at or after checkpoint. An empty checkpoint degrades to List behaviour so first-run callers do not need a special case. checkpoint MUST be a strict RFC3339 timestamp — anything else is rejected with ErrInvalidCheckpoint before the value reaches the gh search query, so a daemon storage tier that has been tampered with cannot smuggle additional `author:` / `label:` qualifiers into the upstream call. The format matches gh's own `updatedAt` output, so the daemon can store the max(updatedAt) it observed last iteration verbatim.

type Runner

type Runner interface {
	// Run executes name with args and returns stdout and stderr separately.
	// Implementations must honour ctx (deadline + cancel) so a wedged gh
	// cannot block discovery indefinitely.
	//
	// When name is not on PATH the returned error must wrap exec.ErrNotFound
	// so callers can map it via errors.Is — AuthStatus uses this signal to
	// downgrade "gh missing" into AuthNotConfigured rather than a hard error.
	Run(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error)
}

Runner abstracts the single operation this package needs against the outside world: invoke `gh <args...>` and capture its stdout / stderr. Splitting it out lets tests inject canned stdout for `gh search issues` without ever spawning a real gh process. The shape mirrors internal/cmux/runner.go so cross-package readers see the same vocabulary for "shell out to a tool with deadline support".

Jump to

Keyboard shortcuts

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