← back to the garden

budding

How We Use the GitHub Ecosystem for Our Development

planted · July 23, 2026
#github#project-management#developer-experience#workflow

We build across roughly thirty repositories and we do not run Jira, Linear, Notion, or a separate planner. The work lives in GitHub, and only in GitHub: issues for the things that get done, discussions for the things that get decided, and a single code-less repository that ties the two together across every project. That was a deliberate choice, and most of the time it holds up. This is the shape of how we use it, and the couple of places where it does not.

The reason to stay inside GitHub is not frugality. It is that the work and the record of the work should not drift apart. The moment planning lives in one tool and the code lives in another, someone has to keep them in sync by hand, and nobody ever does for long. When the plan is an issue and the pull request closes it, the board stays honest without anyone maintaining it.

One issue, one deliverable

The smallest discipline is also the one that matters most. An issue is one thing you can finish. Not a theme, not an area, not "auth." A deliverable with a done state: this guard, this endpoint, this migration. If an issue cannot be closed by a single pull request, it is usually two issues, or it is an epic wearing an issue's clothes.

This sounds obvious and it is the thing teams get wrong first. A tracker full of vague, never-quite-done issues is worse than no tracker, because it tells you nothing while looking like it does.

Phases are the spine

The build runs in numbered phases. Early foundations first, then the transactional core, then broadcasts, analytics, trust and safety, billing, go-live. Each phase is a checkpoint you can stand on: enough shipped to be real, not so much that it never closes.

The instinct is to encode phases as labels, one per phase, and filter by them. We tried the label version and moved off it. A label is a flat tag with no notion of completion. It can tell you an issue belongs to a phase; it cannot tell you the phase is sixty percent done. A milestone can. A milestone is a bucket with a real progress bar, so "where is phase five" becomes a number you read instead of a query you run and count by hand. Phases have a beginning and an end, so they want the container that understands ending.

Above all of it sits a single roadmap issue: the whole plan, phase by phase, one place to see the arc. It is the map. Everything else is a pin on it.

Epics without the word Epic

GitHub has no Epic object. Coming from Jira you look for it and it is not there. For years people faked it with a checklist in an issue body, a list of - [ ] #123 lines that renders as checkboxes and quietly drives a progress bar. It works, and we used it, and it is a hack.

Native sub-issues replaced it. You nest real issues under a parent, and the parent grows a live progress tree instead of a hand-maintained checklist. The children can live in other repositories, which matters when one phase touches the API repo, the dashboard repo, and the infra repo at once. Each phase is a parent issue carrying an epic label so it stays easy to find, with its real work hanging off it as sub-issues. The label makes it discoverable; the nesting makes it add up.

The difference from the checklist era is not cosmetic. A checklist is a copy of the truth that someone has to keep accurate. A sub-issue relationship is the truth. Close the child and the parent already knows.

Labels are for the things without an order

Once phases moved to milestones and structure moved to sub-issues, labels were left with the job they are actually good at: orderless, cross-cutting tags. Security. Infra. Backend, frontend. Deferred, for the honest admission that a thing is real but not now. These do not have a done state and do not have a sequence, and that is exactly why a flat tag suits them. The rule we landed on: if a value has an order or a finish line, it is a milestone or a field, not a label. If it is just a facet, it is a label.

The payoff is a label list that stayed small enough to mean something. Label sprawl is how trackers rot. Every tag that overlaps another tag is a decision someone has to make on every issue, and they will make it inconsistently, and then filtering lies to you.

Pull requests close the loop, literally

The habit that keeps the whole thing from needing a caretaker is one line in a pull request: Closes #NN. Merge the PR and the issue closes itself, the milestone ticks up, the parent's progress bar advances. No one moves a card. The board is a side effect of shipping code, which is the only kind of board that stays true.

This is the quiet reason the single-tool choice pays off. The auto-close only works because the plan and the code share an address space. Split them across two products and you are back to manual reconciliation, which is the tax we were trying not to pay.

Issues finish, discussions do not

An issue has a done state. That is its whole personality. But much of what a team produces has no done state: a decision and the reasoning behind it, a handover when someone inherits an unfamiliar project, a research write-up, an open question nobody has answered yet, an announcement. Force those into issues and you get a tracker full of things that can never legitimately close, which is the vague-issue rot from the other direction.

Those go in Discussions. The test is simple. If closing it would feel wrong, it was never an issue. A decision does not get closed, it gets referred back to. A handover is not finished, it is read. Discussions are threaded, they sort into categories, and the question-and-answer ones let you mark the accepted answer so the thread stops being a debate and becomes a reference.

The two are not walled off. When a discussion produces something actionable, GitHub converts it into an issue and keeps the link back, so the thread that reached the decision points at the work that carries it out. That is the whole loop: discuss until it is a decision, split off issues to execute, let the pull requests close them.

The repository with no code

There is one more piece, and it is the least obvious. The org has a repository that holds no product code at all. Its only job is the material that belongs to the whole organization rather than to any single codebase.

GitHub gives a specially named repo org-wide reach, and the private variant keeps that reach internal. Two things come with the name. First, its profile README renders on the organization page itself, so when a member lands on the org they meet a hand-written index of where everything is rather than a wall of repositories sorted by last push. Second, it is where the cross-cutting things live: announcements that are not about one product, a category per active project for its context, status, and handovers, research write-ups, and the playbooks for how we work, including our own conventions for the very issues and labels this note is about. A new project gets a discussion category before it gets a line of code, so the thinking has somewhere to accumulate from day zero.

The per-project categories matter more than they look. Without them, one project's status threads bleed into another's and the thing you wanted, everything about this project in one place, dissolves. With them, the code-less repo becomes the table of contents for everything that is not code: who decided what, where each project stands, and how the studio does things.

Where it strains

The garden rule is that you are honest about where a thing stops, so here is where this one does.

GitHub's planning layer is thin. The moment you want a real timeline, a Gantt-style view of phases against dates, or custom fields like estimate and priority that sort and group, you have to reach for Projects and add that scaffolding yourself. Issues and milestones store the work; they do not visualize a plan. For a solo-ish, engineering-led build that is fine, because the roadmap issue and the milestone bars carry it. For a large team that wants to slice capacity and forecast dates, the native tools run out and the Projects setup starts to look like the thing you were avoiding.

Cross-repo is better than it was and still not seamless. Sub-issues span repos, but milestones do not; a milestone is per-repository, so a phase that genuinely lives in three repos cannot be one milestone across all of them. You either accept the roadmap issue as the real cross-repo spine and let milestones be per-repo, or you lift phases into a Project that spans repositories and pay for that layer.

Discussions drift without a librarian. They have categories but no real hierarchy and weak search, so the code-less repo stays useful only as long as someone keeps a pinned index pointing at what matters. Left alone it turns from a table of contents into an attic, which is a people problem the tool will not solve for you.

And none of it plans for you. GitHub Issues is a faithful record of decisions you already made. It will not tell you a phase is overcommitted or that two issues secretly depend on each other. That judgment stays with the humans, which is correct, but it means the tracker rewards teams that already think clearly and punishes teams hoping the tool will do the thinking.

That is the trade we took knowingly. A little less planning power, in exchange for a plan that can never drift from the code, because they are the same thing.