Skip to content

feat(tracker): Add GitHub Issues tracker adapter #1

@Fatih0234

Description

@Fatih0234

Problem

The orchestrator currently only supports a file-based Local Board tracker (internal/tracker/local.go). This is excellent for integration tests and demos, but it is a toy for real-world usage. To make Contrabass-PI a daily-driver tool, it must be able to poll, claim, and update issues from GitHub Issues.

The IssueTracker interface in internal/types/types.go is already abstracted and clean. Adding a GitHub adapter is a natural plug-in that requires zero architectural changes to the orchestrator, pipeline, or TUI.

Goals

  • Implement IssueTracker for GitHub Issues REST API v3
  • Support authentication via GITHUB_TOKEN (with $ENV_VAR interpolation in config)
  • Map GitHub issue states ↔ Contrabass issue states
  • Reuse the existing IssueTracker interface exactly — no interface changes
  • Add config validation and YAML parsing for the new tracker type

Non-Goals

  • GraphQL API (REST v3 is sufficient and simpler)
  • Webhooks / push-based updates (keep polling model)
  • Pull Request support (skip PRs when fetching issues)
  • GitHub Projects / custom fields

Interface Contract

// Existing interface in internal/types/types.go
type IssueTracker interface {
    FetchIssues() ([]Issue, error)
    ClaimIssue(id string) (Issue, error)
    ReleaseIssue(id string) (Issue, error)
    GetIssue(id string) (Issue, error)
    UpdateIssueState(id string, state IssueState) (Issue, error)
    SetRetryQueue(id string, retryAt time.Time) (Issue, error)
}

State Mapping

Contrabass State GitHub State Action
todo open + no assignee Poll as candidate
in_progress open + assigned to bot Claim via assign
retry_queued open + assigned + label contrabass:retry Same as in_progress
in_review open + label contrabass:review Post comment with handoff
done closed Close issue on approval

Configuration

Add to WORKFLOW.md:

---
tracker:
  type: github
  owner: Fatih0234
  repo: contrabass-minimal
  token: $GITHUB_TOKEN
  label_prefix: contrabass
  assignee_bot: contrabass-bot   # optional: bot username for claiming
---

Add to internal/config/config.go:

type TrackerConfig struct {
    Type         string `yaml:"type"`          // internal | github | linear
    BoardDir     string `yaml:"board_dir"`     // for internal
    IssuePrefix  string `yaml:"issue_prefix"`  // for internal
    Owner        string `yaml:"owner"`         // for github
    Repo         string `yaml:"repo"`          // for github
    Token        string `yaml:"token"`         // for github
    LabelPrefix  string `yaml:"label_prefix"`  // for github
    AssigneeBot  string `yaml:"assignee_bot"`  // for github
}

API Surface

Implement a GitHubTracker in a new file internal/tracker/github.go.

FetchIssues()

GET /repos/{owner}/{repo}/issues?state=open&assignee=none&per_page=100
  • Filter out Pull Requests (pull_request key present)
  • Convert each to types.Issue with State = StateUnclaimed
  • Use issue.Number as the display identifier, "GH-" + number as ID
  • Store the GitHub number in a private field for API calls

ClaimIssue(id)

PATCH /repos/{owner}/{repo}/issues/{number}
Body: {"assignees": ["bot-username"]}
  • Returns the updated issue with State = StateClaimed
  • If already assigned, return error (another instance claimed it)

ReleaseIssue(id)

PATCH /repos/{owner}/{repo}/issues/{number}
Body: {"assignees": [], "state": "open"}
  • Unassigns the bot and ensures the issue is open
  • Returns updated issue with State = StateUnclaimed

UpdateIssueState(id, state)

  • Maps the Contrabass state to GitHub actions:
    • StateInReview → add label contrabass:review, post a comment with handoff link
    • StateReleasedPATCH to close the issue
    • StateRetryQueued → add label contrabass:retry
  • Returns updated types.Issue

SetRetryQueue(id, retryAt)

  • Add label contrabass:retry
  • Post a comment: Retry scheduled at {retryAt}
  • Returns updated issue

GetIssue(id)

GET /repos/{owner}/{repo}/issues/{number}
  • Returns a single issue by its numeric ID

Edge Cases & Error Handling

Scenario Behavior
Rate limited (403 + X-RateLimit-Remaining: 0) Return wrapped error with retry-after header
Issue not found (404) Return clear error, do not panic
Network timeout Retry once with 1s backoff, then surface error
Token missing / invalid Fail fast at config validation time
PR returned in issues list Silently skip it
Assignee conflict (race) Treat as "already claimed", return error so orchestrator skips

Existing Reference

See docs/references/contrabass/internal/tracker/github.go for the full Contrabass reference implementation. It handles pagination, credential validation, and close-on-release behavior. Use it as a guide, but keep the minimal version simpler.

Files to Create / Modify

  • Create: internal/tracker/github.go
  • Create: internal/tracker/github_test.go (use httptest to mock GitHub API)
  • Modify: internal/config/config.go — add GitHub fields to TrackerConfig
  • Modify: internal/config/config_test.go — add validation tests
  • Modify: cmd/contrabass/main.go — wire tracker.NewGitHub(...) when tracker.type == "github"

Acceptance Criteria

  • gh issue list shows open, unassigned issues are picked up by FetchIssues()
  • Claiming an issue assigns the configured bot user
  • Releasing an issue unassigns and re-opens
  • Moving to in_review adds the review label and posts a comment
  • Moving to done closes the issue
  • Config validation fails fast if tracker.type == "github" but owner, repo, or token are missing
  • Unit tests mock the GitHub API and cover happy path + 404 + rate-limit
  • The orchestrator works end-to-end in --no-tui mode against a real repo with a test issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions