Skip to content

Add GitHub Issues as a Software Factory tracker - #692

Merged
czpython merged 6 commits into
czpython:mainfrom
RCheesley:feat/github-issues-tracker
Oct 14, 2026
Merged

czpython merged 6 commits into
czpython:mainfrom
RCheesley:feat/github-issues-tracker

Conversation

@RCheesley

@RCheesley RCheesley commented Sep 22, 2026 •

Copy link
Copy Markdown

What this adds

Software Factory took work from Linear or Jira only. GitHub was already wired in on the code side - branches, pull requests, reviews, pushes - but never as a source of work items, so a project whose backlog lives in GitHub Issues had no intake. This adds github as a tracker choice.

An issue that gains the trigger label opens a build, and the build drives the issue through the same funnel a Linear ticket goes through.

How it works

GitHub has no status field, so the tracker reads the five existing status settings as label names. They behave as one exclusive group: setting any of them removes the others, including the trigger label, so an issue never reads as two states at once and re-applying the trigger label opens a fresh build. Reaching the done label also closes the issue as completed.

Intake reuses the existing /_external/github/events/ webhook. Core publishes a neutral issue.labeled fact when a person (or the App itself, via the start route) adds a label, and a Software Factory subscriber turns it into the same normalized ticket.transitioned fact Linear and Jira publish, so the funnel subscriber is untouched. Labels added by any other App are ignored. The operator App is the tracker's identity - there is no second credential to connect - and the App manifest now subscribes to the issues event, without which an App would connect cleanly, report healthy, and never open a build.

Two GitHub-specific details are worth review attention:

  • Ticket keys are owner/repo#number. Issue numbers are only unique within a repository, so the repo has to travel in the key. split_key refuses a bare number rather than guess, and the agent start route takes the key as a path (with the # encoded as %23). The ticket routes by the issue's full repo name only.
  • No account resolution from an assignee. A GitHub login is not an identity any grant issuer vouches for, so the subscriber publishes the login for display only and leaves assignee_id and assignee_email unset rather than invent an address. Every GitHub build uses the default account.

A note on the settings shape

This reuses the five generic status fields rather than adding GitHub-specific ones, so the funnel subscriber and get_tracker stay as they are. The trade-off is that the defaults read as Linear status names, so an operator has to rename them (ready-for-agent, agent:in-progress, …) before selecting this tracker. The docs say so. If you would rather see dedicated github_* label settings with GitHub-shaped defaults, that is a small change and I am happy to make it.

Testing

  • ruff check backend and ruff format --check backend clean.
  • Full pytest backend/ passes locally after the review changes (2190 passed, 73 skipped).

Docs

docs/configuration.md gains a GitHub Issues as the tracker section and three corrections to claims this change makes stale: the manual App registration event list, the "Linear and Jira share five status settings" sentence, and the druks doctor paragraph.

@RCheesley
RCheesley force-pushed the feat/github-issues-tracker branch from 6bdb498 to 374062c Compare September 22, 2026 20:17
@RCheesley
RCheesley marked this pull request as ready for review September 22, 2026 20:18
@RCheesley
RCheesley requested a review from czpython as a code owner September 22, 2026 20:18

@czpython czpython left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this! GitHub Issues as a tracker is a good addition. The PR description is very clear, and the label-group idea fits the funnel well. I have a few changes to ask for before we merge.

Must fix

  1. GitHub errors are not caught. known_exceptions lists httpx.HTTPError, but the GitHub client raises githubkit's RequestFailed and GitHubAppNotInstalledError. Neither one is an httpx error. If a label write fails, the error goes up to the webhook. On a merge, this can make the pr.closed webhook fail, and Druks can lose the merge result. Please add both to known_exceptions.

  2. Routing uses the bare repo name. The webhook sends repository["name"] as project_name. ProjectRepo.get_for_name matches a bare name with LIKE '%/name', so an issue on acme/api can route to other/api. Please send repository["full_name"]. get_for_name already matches a full name exactly.

  3. software_factory_start cannot take a GitHub key. The route is /{ticket}/start, and owner/repo#12 contains a /. FastAPI returns 404 for it. Please make the route accept the key, for example with {ticket:path}, and add a test.

  4. The intake belongs in Software Factory. core/webhooks/github.py knows nothing about work items, and Linear and Jira publish ticket.transitioned from contrib/software_factory/webhooks.py. Please make core publish a neutral fact, for example issue.labeled, and add a Software Factory subscriber that turns it into ticket.transitioned.

  5. Please check the sender. The label event has a sender, the user who added the label. Please publish only when sender["type"] == "User", as the comment handlers in the same file already do. Only people with triage access or higher can add labels, which is a good start. We don't need to link the sender to a Druks account in this PR, so the default account is fine for now.

Smaller things

  • The docs say GitHub rejects a label that does not exist. I think the add-labels API creates the label instead. Could you check and correct the sentence?
  • get_account_id never runs, because assignee_id is always None. The base class covers it, so you can remove the override.
  • aclose does nothing, and its comment says "only close our own". Please make it close the client the tracker created, or remove the comment.
  • The client= argument exists only for tests. Please patch Github.get_client in the tests instead.

These things work well, and I checked them: the funnel only acts when the source is the selected tracker, a repeated delivery finds the same work item, and the label echo does nothing.

Thanks again. If anything here is unclear, ask me here in the PR.

@RCheesley
RCheesley force-pushed the feat/github-issues-tracker branch from 374062c to bbb0cce Compare October 2, 2026 10:08
@RCheesley

Copy link
Copy Markdown
Author

Thanks so much for such a careful review, super helpful! I've rebased onto main and pushed a second commit (bbb0cce) that works through everything.

Must fix

  1. known_exceptions now includes githubkit's RequestFailed and GitHubAppNotInstalledError, with a test.
  2. Routing now uses repository["full_name"]. I also stopped GitHub tickets passing their labels into ProjectRepo.lookup. It falls back to the labels when the name doesn't match, and a label like api would hit the same LIKE '%/api' problem. The issue's own repo is always the build's repo, so nothing else should route it.
  3. The route is now /{ticket:path}/start, with a test that posts acme/widget%2312 (the # has to be encoded). I updated the parameter description too, so the MCP tool tells agents a GitHub key is fine, which meant updating the pin in test_mcp_endpoint.py.
  4. Core now publishes a neutral issue.labeled (repo and number, with the label, title, url and assignee login in the payload), and a new issue_label_transitions_the_ticket subscriber in software_factory/subscribers.py turns it into ticket.transitioned.
  5. The sender check is in, with one exception I'd love your eyes on. A strict sender["type"] == "User" breaks the start route for GitHub, because start sets the trigger label through the App and that webhook comes back with the App's bot as the sender (type Bot). So the handler publishes when the sender is a User or our own App ({slug}[bot], matched against the connected identity), and ignores every other bot or App. There are tests for both. If you'd rather keep it to Users only and have start work differently for GitHub, happy to change it!

Smaller things

  • The REST docs don't actually say either way, but you're right that adding a missing label creates it (with a default colour) rather than failing. I've corrected the sentence and suggested creating the labels first if folks want to pick colours and descriptions.
  • Removed the get_account_id override.
  • aclose now closes the client the tracker created.
  • Removed client=, and the tests patch Github.get_client instead.

The docs also gained a short paragraph on who can trigger a build and how the repo routing works, plus a note about encoding the #.

ruff check and ruff format --check are clean, and this time the full pytest backend/ run passes locally (2190 passed, 73 skipped).

Thanks again!

RCheesley and others added 3 commits October 7, 2026 18:46
Software Factory took work from Linear or Jira only. GitHub was wired in on the
code side - branches, pull requests, reviews, pushes - but never as a source of
work items, so a project whose backlog lives in GitHub Issues had no intake.

GitHub has no status field, so the tracker reads the five status settings as
label names and drives them as an exclusive group: setting one removes the
others, including the trigger label, so a re-label opens a fresh build. Reaching
the done label also closes the issue as completed, which is what a GitHub reader
expects finished work to look like. Issue numbers repeat across repositories, so
a ticket key is `owner/repo#number`.

Intake reuses the existing GitHub webhook. `issues_labeled` publishes the same
normalized `ticket.transitioned` fact Linear and Jira publish, so the funnel
subscriber is untouched. The operator App is the tracker's identity; there is no
second credential to connect, and no new permission either: Issues write was
already on the App's list.

A GitHub login is not an identity any grant issuer vouches for, so the tracker
resolves no account from an assignee and every GitHub build uses the default
account. The webhook publishes the login for display and leaves the id and
address unset rather than guess at them.

The App manifest now subscribes to the issues event. Without it an App created
through Settings > Services would connect cleanly, report healthy, and never
open a build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Core now publishes a neutral issue.labeled fact, and a Software Factory
subscriber turns it into ticket.transitioned, the way Linear and Jira intake
already lives in the app. Core publishes a label only when a person adds it or
the App adds it itself: the start route asks for a build by setting the trigger
label through the App, so its own label has to reach the funnel. Any other
App's label is ignored.

The ticket routes by the issue's full repo name, which matches one repo
exactly. A bare name, or a label, could match the same name under another
owner, so neither routes a GitHub ticket.

The start route takes a GitHub key: owner/repo#12 has a slash, so the ticket
segment is a path.

The tracker catches githubkit's RequestFailed and GitHubAppNotInstalledError,
so a failed label write no longer fails the webhook that caused it. It closes
the client it creates, takes no client argument, and leaves account lookup to
the base class.

The docs now say GitHub creates a missing label rather than rejecting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now resolves a build's account from the person who moved the ticket, and
every ticket.transitioned payload carries actor_id. A GitHub label names a
login, which no grant issuer vouches for, so the GitHub transition carries no
actor and the build keeps the default account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RCheesley
RCheesley force-pushed the feat/github-issues-tracker branch from bbb0cce to 9410218 Compare October 7, 2026 17:50
@RCheesley

Copy link
Copy Markdown
Author

@czpython just resolved conflicts, only a minor markdown conflict this time.

@RCheesley
RCheesley requested a review from czpython October 11, 2026 12:07
A GitHub sign-in links a GitHub user to a Druks account, so the label's
sender now picks the build's account as on Linear and Jira, with the
assignee next. The tracker takes its client from get_tracker, catches
every githubkit error, and closes a done issue with close_issue. Tests
and docs drop what restated the code.
@czpython
czpython merged commit 80494cc into czpython:main Oct 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants