Inside Trinity's release pipeline: from merge to tag

Cutting a release by hand is a small ceremony of dread. Which of your six repos actually changed? Is this a patch or a minor — and who decides? Somebody has to write the changelog nobody wants to write. Then you tag each repo, one at a time, hope the staging deploy holds, and pray you didn't fat-finger a version number on a Friday afternoon.
None of that is writing software. It's coordination — the bookkeeping between "the code is merged" and "the code is shipped." And it's exactly the kind of work a machine should own.
Trinity's release pipeline owns it. A release is a first-class thing in Trinity: it groups merged work, grades each repository's version bump from what actually changed, walks a staging chain, waits for your approval at one gate, then tags every repo and publishes a changelog. This post is about how that pipeline works — and the work that went into making it safe to trust.
A release is a unit, not a moment
In most tools a "release" is an event — a button you press, a tag you cut. In Trinity it's an object with a lifecycle.
A release groups one or more PRDs into a single shippable unit. It's the primary user-facing unit, too: the dashboard, the stories list, and the Architect are all scoped by the active release. Every PRD belongs to exactly one release, assigned when Architect saves it.
Trinity auto-creates your first release when you write your first PRD, and gives it a readable two-word name like "Brave Otter" — which also names its git branch, release/brave-otter-2. You rarely create one by hand. As stories merge, they merge into that release branch. When every story under every PRD is merged, the release is ready to ship.
That framing matters. Because a release is an object that accumulates work, Trinity always knows precisely what's in it — which stories, which repos, which diffs. That's what makes everything downstream possible.
The lifecycle: from created to released
A release moves through a status machine. The forward path is linear; the interesting parts are the edges that let you back out.
Resets each repo's staging-chain tips to the snapshot taken when the release first reached staged.
Halts the release pipeline mid-flight when something needs adjusting.
Re-runs the staging walk only for the repos that didn't make it through.
The happy path runs created → in_progress → ready → staging_in_progress → staged → releasing → released. A release sits in created until the first story merges, climbs to ready once everything is in, gets walked onto staging, and finally ships.
But shipping is never just forward. A bad staging deploy needs an undo. A release that's mid-flight sometimes needs to stop. A transient git error shouldn't sink the whole thing. So the lifecycle has reverse and side paths — Yank Back, Stop, Retry Failed Repos — each wired to a specific button in the release detail panel. The state machine is the contract: every transition is explicit, and the buttons that appear depend on exactly where the release is.
A released release is terminal. There's no "un-release" — once repos are tagged on the
production branch and the changelog is published, that history stands. The recovery paths all live
before the tag, where backing out is still clean.
Version bumps, graded from the code
Here's the part that replaces the most tedious release argument: you don't pick the version bump. Trinity reads what changed.
When a release is ready to ship, Trinity looks at what landed in each repository since its last tag and grades the change — patch, minor, or major — with a short reason. If a change removes a public export, route, or field, or changes a function's signature, Trinity flags it and raises that repo to a major automatically. A breaking change can't slip out under a patch.
Crucially, every repo is graded on its own. There is no global version. Each repo's new number is computed from that repository's latest tag at the moment it's tagged — so a polyrepo project versions each repo independently, and a repo the release didn't touch is simply left untagged.
Check Changes — preview a release before you commit to it
The release dashboard's Check Changes tile runs the exact same grading at any time and shows the per-repo bumps and the precise tag each would produce right now. It's read-only and safe to run whenever you want to see where a release is heading.
You stay in control of the final call. At the release gate, the Version bumps tab shows each repo's current → proposed version, the graded bump with a dropdown to override, the reason, and any breaking change found. A bump auto-raised to major must be confirmed explicitly — and dialing it back below major makes you acknowledge the break. The grade is the recommendation; the confirmation is yours.
The staging walk and the undo button
Before a release ships, it gets staged. Trinity walks each repo's deploy chain — by default dev → staging — from the release branch, then verifies the result on the preview deploy. That's the staging_in_progress → staged transition.
The reason staging is its own step, with its own status, is the undo. The instant a release reaches staged, Trinity snapshots the commit tip of every repo's staging chain.
Yank Back resets to a snapshot, not a guess
If something breaks on the staging deploy, Yank Back force-resets each repo's
staging-chain tips to the snapshot taken at staged and reopens the release. It's a
recorded point to return to — not a manual scramble to figure out what the branches looked like
before.
The two recovery actions answer two different failures. Use Yank Back when the deploy itself is bad — a broken build hit staging, smoke tests fail, a regression slipped through. Use Retry Failed Repos when the walk errored — a network timeout, a transient permission issue — and the staging tips are still sane. Different problems, different buttons.
One gate, and five agents that do the legwork
Trinity is autonomous, but a release is exactly the kind of decision a human should sign. So the pipeline parks at a single release-approval gate and waits.
By the time you reach the gate, the work is already done. The release dashboard runs five standalone agents — each against the current release worktree, none of them advancing the lifecycle:
Smoke-test the release locally so you know it boots.
A preflight quality sweep over the merged work.
Audit and fix SEO regressions for web targets.
Write release notes from the merged stories, per target.
Grade per-repo version bumps and preview the tags.
Each agent writes to an audit log, so the gate shows cumulative state across tabs — Overview, Preflight & SEO, Notes, Version bumps, Consolidation. You approve, edit the notes or bumps, request changes, or skip. One decision, fully informed. (Trinity treats this as a quality checkpoint: the agents find and fix; you sign.)
Finalize: changelog, then independent tags
Approve the gate and the release enters releasing. Finalize is the part that touches the outside world, so it's built to be resumable.
For each repo that's being tagged, Trinity prepends the generated release notes to that repo's CHANGELOG.md, commits it to the production branch, then creates an annotated git tag — its version resolved from that repo's own latest tag plus the confirmed bump — and pushes it. Every tag is persisted the moment it's created.
That persistence is the point. If tagging fails for some repos — a flaky push, an expired credential — the release lands in partially_released rather than collapsing. The coordinator auto-retries up to three times, and a retry skips the repos that already succeeded. Tagging four repos and losing one to a network blip doesn't mean re-tagging all four.
| Step | Cutting a release by hand | Trinity |
|---|---|---|
| Scope | Eyeball which repos changed | Knows exactly — the release owns the merged work |
| Version bump | Argue patch vs minor in a thread | Graded from the diff, major on any break |
| Changelog | Someone writes it from memory | Generated from the merged stories |
| Tagging | Tag each repo one at a time | Tagged per repo, each from its own latest tag |
| Failure | Half-tagged, untangle by hand | Partial-released, auto-retries, skips done repos |
| Undo | Hope you remember the old tips | Yank Back to a recorded snapshot |
It runs in parallel, and across hosts
A release isn't a global lock on your project. Each one gets its own coordinator, worker pool, and job queue, so you can run several at once — a Multi-Release Status bar shows a pill per running release with live progress.
Ordering, when it matters, is explicit. Releases form a dependency DAG: a release can't enter releasing until everything it depends on is released, dependencies are auto-inferred from cross-PRD story dependencies, and circular ones are blocked. So "the API release ships before the client release" is a constraint Trinity enforces, not a thing you remember.
And because every repo resolves its own git host, transport, and identity, a single release can tag one repo on GitHub and another on a self-hosted Forgejo in the same run — the machinery behind that is its own story in parallel multi-host git.
The work behind it
Most of this isn't one feature — it's a year of sanding down the sharp edges of shipping. A few of the bigger passes:
- Semantic bumps graded from code arrived in 0.3.1, along with the Check Changes preview and the readable two-word release names. Before that, you picked the number. Now Trinity reads the diff and you confirm.
- A real staging stage — the
stagedstatus, the tip snapshot, and Yank Back — turned staging from a hopeful push into a step you can back out of cleanly, with backup refs and a multi-pass yank so an interrupted undo stays recoverable. - Per-repo host and identity (0.3.2) threaded the right host, credential, and author into the release path, plus an ownership lease so a worker that loses its claim aborts finalize instead of double-tagging.
- Gates that wait (0.3.4) made approval a first-class pause: the release parks, surfaces as needs you, and resumes the instant you answer — rather than guessing or failing.
None of these are glamorous. All of them are the difference between a release pipeline you watch nervously and one you trust to run while you do something else.
The bookkeeping, owned
Go back to the Friday-afternoon dread. Every piece of it — which repos changed, patch or minor, the changelog, the tagging, the staging deploy you hoped would hold — is coordination, not creation. It's the bookkeeping between merged and shipped.
Trinity owns that bookkeeping. The release knows what's in it, grades each repo's bump from the code, walks staging with an undo button, writes the changelog, and tags every repo on its own version — pausing exactly once, for the one decision that's yours to make.
If you remember one thing: the hard part of releasing was never the version number. It was everything around it. See how it fits into the rest of execution in the releases docs, or read the 0.3.1 changelog for where graded bumps came in.