Releases

Releases group a body of work into a shippable unit. They are Trinity's primary user-facing unit — dashboard, stories list, story detail, and Architect are all scoped by active release. Every PRD belongs to exactly one release (the assignment is set when Architect saves the PRD; you can move a PRD to a different unshipped release later). A release can also carry work with no PRD in between: a loose epic grouping stories directly under the release, or a single story attached to it on its own. When all stories under a release are merged, the release executes directly as its own job to run the release pipeline (audit, release notes, tagging).

Creating a Release

Trinity auto-creates your first release when you create your first PRD in Architect, giving it a readable two-word name like "Brave Otter". You rarely need to create releases by hand.

When you do want a new one (for example to start planning your next release while the current one is still in progress):

  1. Click Releases in the sidebar
  2. Click Create Release
  3. Fill in:
    • Name — a label for this release. Trinity suggests a readable two-word name like "Brave Otter"; type your own if you prefer. It's a label, so you can rename the release later.
    • Description — optional summary of what's included
  4. Click Create

Empty releases are fine — you can assign PRDs to them later from Architect or by moving existing PRDs.

Name vs branch

Creating the release fixes its git branch from the name you created it with — "Brave Otter" gets release/brave-otter. That branch is the release's identity from then on: it's where its stories merge and what it promotes from.

The two are edited separately, and that's deliberate. Renaming the release changes the label only — the branch keeps the name it was cut with, so nothing in git moves, no work is orphaned, and in-flight stories carry on merging where they always were. Reordering releases on the board doesn't move a branch either. Two releases in one project can't share a name or a branch — Trinity's suggested name is always free when you open the dialog, but if you type your own and it collides with a release already there, Trinity tells you rather than silently swapping in a different one; pick another name and try again.

Renaming the branch

Moving the branch is a separate, explicit action, because unlike the label it's something git has to actually do. What it costs depends on whether the branch has been created on your remote yet:

  • Nothing pushed yet. Trinity creates a release's branch lazily — the first time a story actually needs it. Until that happens there's nothing on your remote to rename, so the rename is instant and there's nothing to clean up.
  • The branch is live. Trinity moves it in every repository: it creates the new branch from the old one first, then deletes the old one, one repository at a time. The replacement branch always exists before the old one goes, so a rename that's interrupted mid-way leaves the branch reachable — never missing. Any worktrees you have checked out on that branch are re-pointed too.

A live branch is only renamed when the release is quiet. A branch name is what open pull requests target, what staging placements were placed from, and what queued merges are waiting to merge into — so if any of that is still in flight, Trinity tells you exactly what it's waiting on rather than moving the name out from under it. You'll see the specific items: open pull requests into the branch, unsettled merges into your dev branch, live staging placements, and any run still going.

If you want the rename to happen anyway, Trinity can queue it. A queued rename:

  1. Holds new work for that release — no new stories start, and nothing new is queued up behind the rename.
  2. Waits for the work already in flight to finish on its own. It never cancels or interrupts a run — it stands back until the release goes quiet, however long that takes. While it's waiting, the release shows the Waiting for a Quiet Release gate, and you can see what it's still waiting on.
  3. Moves the branch across every repository.
  4. Lifts the hold and re-queues the merges it set aside, now aimed at the new branch name.

You can leave it and let it finish on its own. If the rename fails partway through — a repository you can't push to, say — the hold is lifted immediately so the release keeps working, and Trinity picks up where it left off next time rather than starting over.

Versioning

A release ships the version its packages declare. The number lives where it always has — the version in each package's package.json, Cargo.toml or pyproject.toml — and a story writes it: when Architect proposes a version for a release (see Architect → Release assignment), the number you settle on becomes an acceptance criterion of the story that edits the manifest, so the version that ships is a change like any other rather than a decision made at ship time. When you ship, Trinity reads each manifest, checks the version before anything is tagged, and tags exactly what the manifests say. Check Changes reads what actually changed and proposes the right bump — patch, minor, or major — so you know what to declare (see Version bumps below).

The release policy

How those numbers relate, how their git tags are named, and the pointer a candidate publishes under are rules your repository carries, in a release policy file — .trinity/release.json at the root of your project's policy repository. A new project names its first managed repository; you can name another from the same settings card, and Trinity never moves it on its own, so adding or reordering repositories leaves it where it is. A release stops with a message saying so if the project names none, and the policy repository can't be removed, disabled or made a read-only dependency until you name another. You edit it from Release Defaults, and saving there commits the file onto your integration branch. On a new project, Architect stages the policy with the rest of the plan and the plan commit writes it into your repository, so a change to how you version is a commit you can review, revert and trace like any other.

Each branch reads the policy its own tree holds: a release on your main line reads your integration branch's copy, and a patch on a maintenance line reads the line's own copy. So a maintenance line keeps the rules of the version family it maintains even after your main line changes its own — a 3.x line whose packages each version on their own keeps doing so after 4.x moves them onto one shared version. When Architect proposes the version for a patch on a maintenance line, it groups the packages the way that line versions them. Trinity reads each repository's branch fresh from your git host before planning a release; if a repository can't be reached, the release stops and says which, rather than planning without that repository's packages.

The policy lists release groups, each with its packages, how they relate, and its own tag pattern (which supports any shape like release-{major}.{minor}.{patch}, with -label.number added for a candidate):

  • Independent — each package owns its version, its bump grade, its changelog, its tag namespace, and, where it publishes, its own registry pointer. A bump on one never moves another, even when two packages share a repository, and a package a release didn't touch is left untagged. Each package's tags carry its own name ahead of the number ([email protected]), from the pattern's {package} slot — its manifest name when it has one no other package in its repository shares, else its folder — and Check Changes proposes a bump for each package that changed, graded off that package's own history (see Version bumps below). A package the release didn't change, still declaring the version it last shipped, isn't tagged; one the release did change must declare a new version. A package that carries its own version also keeps its own CHANGELOG.md in its folder, written from that package's release notes; a repository versioned as one keeps a single CHANGELOG.md at its root. A package a registry target ships moves its own dist-tag pointer independently of every other package's. Either way, each release candidate's notes collect under an Unreleased heading, and the final release renames that heading to its version and the day it shipped, so a run of candidates ends as one section for the version they led to.
  • Fixed — the group's packages move together on one shared version, one bump grade, and one changelog entry. Every member's manifest declares the same version, and Trinity tags every repository the group spans — touched or not — with it, one tag per repository named through the group's pattern, so consumers can pin "everything at vX". A breaking change anywhere in the group raises the whole group's grade to major (see Version bumps below). Each repository keeps one CHANGELOG.md at its root, with one section for that version, holding the notes of any candidates that came before it. When the group spans several repositories, tagging is all-or-nothing: if one repository lands on production ahead of its siblings, the release waits at the Waiting to Tag gate and tags the whole set once the last one lands.

A package no group names versions on its own: tagged v{major}.{minor}.{patch} when it is the only package in its repository, and {package}@{major}.{minor}.{patch} when it shares its repository. With no policy file at all, that's how every package versions, and candidates publish under the next pointer.

Changing the policy

Changing how packages relate — bringing packages that version on their own into one fixed group, splitting a fixed group so each package versions on its own, or renaming a group's tags — is a change your users feel, so it ships only at a major version. The first release on a branch after such a change is a conversion release: Trinity compares the policy at the branch's last release with the policy it carries now, and names the change on the release's Check Changes tile with a Policy change badge.

  • Bringing packages together — the group's version is a new major above every member's current one. With effect at 3.5.0 and @effect/schema at 0.9.0, the group ships 4.0.0, and every member is tagged under the group's pattern (v4.0.0), changed or not. From then on the group keeps one changelog, and each member's own CHANGELOG.md gets one last entry pointing to it.
  • Splitting a group — each package continues from the group's last version under its own tag: a group at 4.2.0 splits into packages that each ship 5.0.0 ([email protected]) the next time they change.

Check Changes proposes that major, and a version that doesn't cross it — declaring 3.6.0 for a group whose members are at 3.5.0 — pauses the release at Version Check Failed with the version to declare instead.

That major opens a new version family, so Trinity offers to keep the one it leaves as a maintenance line. The line starts from that family's last release, whose code still holds the old policy, so fixes to 3.x keep shipping each package under its own tag ([email protected]) while 4.x versions as one.

Which packages a release covers

A release covers the packages its own branch holds. A patch on a maintenance line plans, tags and publishes the packages in that line's code — so a package you added in a later version isn't part of it, and a package you have since removed from your main line still ships its fixes on the lines that still have it. A package you move to another folder or rename stays the same package on every line once your project records its new place or name, so its version history, its registry target and its tokens follow it, and each older line finds it in the folder and under the name that line holds it.

A release only covers packages your project already knows about. A folder with its own package.json, Cargo.toml or pyproject.toml that isn't one of your project's packages is left out: it isn't graded, tagged or published, and Check Changes counts it as a manifest without a package so you can see what the release leaves out. Add it to the project through Architect to have releases cover it.

Repository Topology

Not every project wants a dev branch. Every project has a repository topology, and it decides which branch releases integrate on — the one branch that collects finished work and that everything else is measured against. Trinity settles it once, up front, rather than leaving new projects on a default that doesn't match the repo:

  • Importing an existing repo detects it from what's actually there: no dev/develop/development branch means Single trunk, and carrying one means Dev line.
  • Creating a project from scratch has no repo yet to detect from, so Trinity asks — defaulting to the recommended Dev line.

You can change it later under Release Defaults in settings. Picking a topology whose integration branch isn't on your repo's remote yet — choosing Dev line on a repo with no dev branch, say — saves your pick and tells you the branch is missing, rather than leaving you to find out the first time a release has nowhere to land.

  • Dev line (the default) — your dev branch is the integration line. A release forks from dev, merges back into dev, and can rest there at On Dev while you exercise it on staging. Shipping fast-forwards dev onto your base branch and tags it. Dev is what staging servers track and what in-flight sibling releases absorb from, and it gives you a halfway house: integrated but not live.
  • Single trunk — your base branch is the integration line. A release forks from the base branch and merges straight back into it, once. There is no second hop, so shipping is the version tag and nothing else: the code is already on the base branch by the time you tag it.

Everything downstream follows from that one choice. On a single-trunk project:

  • The On Dev column means "merged into your base branch, not yet tagged" — unless the release is a release candidate, which is tagged there. An Integrate promote rests there deliberately — the merge has landed, the version hasn't.
  • Staging targets and in-flight sibling releases are fed from the base branch, because that's the branch that actually receives releases.
  • There is no hotfix kind. A hotfix exists to fork from a branch dev can't reach; here an ordinary release already forks from your base branch and merges straight back, so it is the hotfix path. Trinity refuses Hotfix on a single-trunk project and tells you to create a standard release instead.
  • The forward-port and the "production isn't fast-forwardable" halt below don't apply — both are failure modes of having two lines that can drift apart.

A release stamps its route when it's created, from whichever topology is set at that moment. Switching topology therefore affects new releases only; anything already in flight finishes on the route it started.

Hotfix Releases

On a dev line project most releases are standard: they fork from your dev branch and promote up the release branch → dev → main spine. A hotfix is the exception — for when production is broken and the fix cannot wait behind whatever is currently sitting unshipped on dev. (On a single trunk project there is no exception to make: see Repository topology.) A patch is the third kind, for fixing a version you shipped long ago, on either topology — see Maintenance lines below.

A hotfix release differs in three ways, all of them consequences of one choice:

  • It forks from production, not dev. The release branch is cut from your production branch, so it contains exactly what's live plus your fix — no unshipped dev work rides along with it.
  • It lands on production directly. There is no dev hop and no staging step: the release branch merges straight into production, carrying the fix and its changelog entry in one merge, and the version tag goes on the production line.
  • It owes your dev branch a merge back. Because production has now moved without dev, your dev branch is missing the fix. Trinity merges production back onto dev automatically — the forward-port — and will not mark the hotfix Shipped until that lands.

To create one, pick Hotfix as the kind when creating the release. Trinity fixes the rest of the routing for you: a hotfix always promotes with Ship Now (there is no integration-line resting point for it to stop at), and the Promote panel names the walk it will actually take.

Why the forward-port is enforced

Trinity promotes dev to production as a fast-forward only — production is never allowed to be ahead of dev. A hotfix deliberately breaks that, and until dev absorbs the fix, the next standard release's promote will refuse to run.

Rather than leave that as a trap for whoever ships next, Trinity makes the merge-back a precondition: a hotfix whose forward-port hasn't landed waits at the Waiting to Forward-Port gate. Nothing to answer — it clears itself the moment your dev branch contains production, whether Trinity's own merge landed it or you merged a back-merge PR yourself.

Once the forward-port lands, the fix also propagates to every other in-flight release, the same way any dev advance does.

When a promote refuses because production moved

If a standard release's promote to production halts saying production isn't fast-forwardable, something put commits on production that dev doesn't have. Trinity tells the two cases apart:

  • A hotfix Trinity already forward-ported — nothing is wrong; the local view was just stale. Trinity re-reads the branches and promotes.
  • Somebody pushed to production outside Trinity — the halt names the offending commits and who authored them. Merge them back onto dev (a back-merge PR), then retry the promote.

Maintenance Lines

A hotfix fixes what is live right now. A patch fixes a version you shipped a while ago and still support — a customer on 1.1 needs the fix, and they cannot take everything that has landed since.

A patch lands on a maintenance line: the branch that carries one version series, such as 1.1.x, for as long as you support it. You start a line from the release that shipped that version, and a patch — Patch as the kind, and the line it targets — forks from where the line stands, so it contains exactly what those users are running plus your fix — nothing from the trunk rides along.

Starting a line

Open a Shipped release on the Releases page and choose Start a maintenance line from this release. Trinity suggests the version series from what the release shipped (1.1.x for a release that shipped 1.1.31); change it if you name your lines differently. Leave the branch empty to name it after the series, behind your project's own maintenance branch prefix (maint/1-1-x with a maint/ prefix, 1-1-x with none) — see Project Settings → Branching Strategy for where that prefix comes from and how it stays in sync with your workspace's — or type the exact branch you want, such as REL_1_1_STABLE.

Trinity cuts the line's branch in every repository from the commit that release shipped there. If any repository cannot take it — the release never shipped anything in that repository, or the push is refused — nothing is created, and the message names that repository.

A release whose version is already covered by a line you still support is refused — the message names that line. Retire it first if you genuinely want a second line for the same version.

The new-family offer

When your next major version ships, the version it replaces stops taking new work — and if anyone is still running it, that's the moment to decide whether it keeps getting fixes. The Releases page offers to keep it: a banner names the family (3.x, say) and asks whether to start a maintenance line for it.

Accepting opens the same Start a maintenance line dialog, prefilled from that family's last shipped version, so accepting is exactly starting a line by hand from that release. There's no way to dismiss the offer, and none is needed: it stops appearing the moment a line covers that family, whether you started it from the offer or by hand.

Supported versions

Once a project has a line, the Releases page lists every one under Supported versions, with its series and branch. A line you still support shows Supported; a retired one shows the day it was retired.

Retiring a line

When you stop supporting a version, choose Retire on its row and confirm. A retired line takes no new patches: creating one is refused, and the refusal names the day the line was retired. Its branch and everything it shipped stay where they are.

Everything else follows the line, not the trunk:

  • The version comes from the line's own tags. A patch on 1.1 goes out as v1.1.32 even while your trunk sits at v1.2.0. Trinity only ever considers tags that are actually on the line, so a higher number somewhere else can't be mistaken for this line's latest.
  • A patch never changes what your next release proposes. The version a new release starts from is the highest final you have shipped, so v1.1.32 going out on a line after v1.2.0 leaves the proposal at v1.2.0.
  • The changelog and the tag land on the line. The entry rides the same merge as the fix, and the tag goes on maint/1.1.
  • The line receives nothing else, ever. When a standard release integrates, Trinity propagates it to every other in-flight release — except any release on a maintenance line. A maintenance line only receives what you explicitly send it.

Patches ship immediately

A hotfix waits for its forward-port before it can be marked Shipped, because production moving without dev breaks the next release's promote. A maintenance line doesn't merge into anything, so nothing breaks — and a broken stable release should not wait while someone untangles a year-old cherry-pick against the trunk.

So a patch ships as soon as it lands on its line. Trunk still owes you the fix, and Trinity remembers: you cannot create another release on that line until the fix reaches trunk. The refusal names the release that owes it. Get the fix onto trunk (usually as a normal release, or by backporting the other direction), and the line is free to release again.

Backports

Most fixes go the other way: land on trunk first, then send them back to each supported line. That's a backport — an explicit list of commits, cherry-picked onto one or more maintenance lines.

Trinity opens one pull request per repository per line, so maint/1.1 and maint/1.0 each get their own reviewable PR carrying only the commits you named, with a note recording where each one came from. Nothing else from your trunk goes with them.

A few things worth knowing:

  • Nothing is guessed. You name the commits and you name the lines. Trinity never decides on its own that a line should receive something.
  • A conflict is per-repository. If the fix replays cleanly on three repos and conflicts on the fourth, the three land and only the fourth needs attention — Trinity resolves it the same way it resolves any merge conflict, and re-running the backport picks up exactly where it stopped.
  • Two backports to the same line queue up. If you send two different fixes to maint/1.1 at once, the second waits for the first instead of racing it.

Release Lifecycle

A release promotes along the spine its topology defines — release branch → dev → main on a dev-line project, release branch → base branch on a single-trunk one — tracked by a single status. The statuses themselves are the same either way: Integrated means "landed on your integration branch", Shipped means "tagged". Staging lives on its own axis — a release can be integrated and hold placements on several staging targets at the same time, so staging never changes the promotion status.

Status Column label Meaning
Created Not Started Empty release — no stories merged yet
In Progress In Progress At least one story has merged into the release branch; more work still pending
Ready Ready to Release Every story the release carries — under its PRDs, in a loose epic, or attached directly — is merged or already-done, apart from quality checkpoints (a failed story blocks this until resolved)
Promoting Promoting A promote is in flight — the release branch is merging up to your integration branch, or (dev line) dev up to your base branch
Integrated On Dev Landed on your integration branch and not yet tagged — where an Integrate promote stops and a Ship promote pauses. On a single-trunk project that means merged into your base branch, untagged. A release candidate rests here tagged
Promote Failed Promote Failed A promote hop errored; retry it forward or revert
Partially Shipped Partially Shipped Some repos reached production, others didn't (transient — Trinity auto-retries, or you retry the repos that need it)
Shipped Shipped Done — every repo tagged on the base branch, release notes published

The forward happy path is created → in_progress → ready → promoting → integrated → promoting → shipped. An Integrate promote lands on the integration branch in one hop and rests at integrated; a Ship Now promote walks straight through to shipped.

You drive promotion from the Promote panel in the release detail — it picks the mode (Ship, Integrate, or Ship Now) and fires the right action. Staging targets are placed and withdrawn independently on the staging axis; nothing about staging moves the release's promotion status.

Parked releases

"Promoting" covers two very different situations: a promote actively merging up the spine, and a promote that's stopped moving because one of its jobs is sitting at a gate — waiting on you to re-authenticate, approve a PR, resolve a merge conflict, and so on (see Gates for the full list). Left as one flat "Promoting" badge, a release stuck for days looks identical to one that's seconds from landing.

A release with at least one job parked at a gate shows Parked instead of Promoting, in the release detail header and the Multi-Release Status bar. The badge names the longest-waiting gate kind; the release detail panel adds a row underneath listing every gate kind currently blocking the release, each with how long it's been waiting and how many jobs are stuck there, plus a link to the Run page — the surface where you actually answer the gate. A release resumes its normal Promoting label the moment the last parked job clears.

Merges that wait on checks

Nothing Trinity merges lands until its checks are green, whichever path the merge is on. Every pull request a release merges waits for its checks first, exactly as a story's does: placing the release on a staging target, each promote hop up the spine (including a hotfix's merge into production and its forward-port back onto dev), carrying a dev advance into the other in-flight releases, and a backport onto a maintenance line. The two paths clear that precondition differently, though — a story's own pull request never pauses over a failing check: Trinity sends the story back to its implementer with the failing checks as feedback, and the fixed story goes through them again, failing to the Story Failed gate only after three rounds still fail. A release's own merges have no implementer to send work back to, so a merge whose checks aren't green pauses the release instead — it shows as Parked — at one of the checks gates, and each pause means something different:

  • Awaiting Checks — the checks are still running. Nothing to do; the release carries on when they finish, and asks you if they're still running after two hours.
  • Awaiting Host Review — the checks passed, but your repository host wants a review or another rule satisfied first. Give it on the host; the release carries on by itself.
  • Checks Unverified — no checks ran, or they finished without passing (skipped, for example). Decide whether to merge without them, for this merge or for every later one in the project, check again, or stop.
  • Checks Failed — a check failed. Fix the branch or re-run the check, then check again, or stop.

Stopping at any of them leaves that merge undone and its pull request open. Answering Checks Unverified with Always go ahead in this project is the same as setting Pull requests without verified checks to Merge without them under Project Settings → General → Automation — the project's own standing answer to "no checks ran" and "checks finished without passing", asked once rather than on every merge; set it back to Ask me whenever you like. Gates has the full detail on every gate above.

Recovering from a bad staging placement

Staging is a set of placements — one per target branch — each recovered on its own from the Stage Target Picker (inside the Promote panel's Ship mode):

  • Unstage — withdraw a live (placed), drifted, or failed placement, resetting that target's branch in every repo to the per-repo anchor Trinity captured when it placed the release.
  • Retry — re-drive a placement whose walk failed.

Because placements are independent, a bad deploy on one target never forces you to unwind the others.

Recovering from a partial ship

If some repos reach production and others don't, the release lands in Partially Shipped and the Results tab shows which repos shipped and which failed, each with its last failure reason. The coordinator auto-retries up to three times (releases.auto_retry_count); once that's exhausted, the Results tab offers whichever of these fits each failed repo:

  • Retry ship — for a repo whose promote hop itself failed (a merge conflict, a failing check). It gives the coordinator's auto-retry a fresh budget, then re-runs the promote spine for every repo; a repo that already shipped is left untouched.
  • Retry Tagging — for a repo that reached production but never got its tag.
  • Retry publish — for a registry publish the promote left unfinished.

Whichever clears the last repo, the release transitions to Shipped once every repo has landed its tag.

Assigning, Moving, and Reparenting PRDs

Every PRD belongs to exactly one release, chosen when the PRD is first committed in Architect. You don't link PRDs manually — Architect proposes a target release (existing unshipped or brand new) and you settle it in the conversation before the plan is committed. If you commit a plan without settling that choice, the commit is refused rather than guessing a release for you — the one exception is a brand-new project with no releases yet, where the first release is created for you.

Two actions on each PRD row in the release detail panel let you change that assignment afterwards:

  • Move — reassigns the PRD (and every story under it) to a different unshipped release. Use this when you want stories to ship with a different release entirely.
  • Reparent — changes the parent release without moving the PRD's stories. Use this when the structure of the parent release changed (e.g. a different release became the right home for the work that's already in flight). Only available while the target release is created or in_progress.

To move a PRD:

  1. Open the release that currently owns the PRD
  2. Click the Move action next to the PRD row
  3. Pick the target release (another unshipped release, or create a new one inline)
  4. Confirm

When a move is blocked: the Move action is disabled when the PRD has any non-terminal work — a story that's claimed, running, waiting at a gate, failed, or completed-but-not-merged. Moving would orphan live feature/release branches. Either let the in-flight stories finish or cancel/skip them first. Also: neither the source nor the target release can be past in_progress (any staged-family or shipping status freezes the release).

When a reparent is blocked: Reparent only works from a PRD that hasn't started shipping — it's refused the moment any of its stories has merged into the parent, merged into the release, merged to dev, or released, since reparenting past that point would rewrite history that already landed. It's also refused while any of its stories holds an active worker (cancel the job and retry), and while a story elsewhere depends on one in this PRD and sits in a release that ships before the destination (move the dependent too, or drop the dependency). Also: the release the PRD is reparenting FROM has to be created or in_progress too — once it's reached its own commitment gate, reparenting away from it is refused the same way the target release's status refuses reparenting onto it.

When a PRD whose stories depend on stories in another release's PRDs is created or moved, Trinity automatically adds a release dependency.

Release Dependencies

Releases can depend on other releases. This creates a DAG (directed acyclic graph) that enforces ordering:

  • A release cannot promote until all its dependencies are Shipped — or, for a release candidate, until the final of its cycle is
  • Dependencies are auto-inferred from cross-PRD story dependencies when linking PRDs
  • You can also add or remove dependencies manually via the Dependency Editor in the release detail panel
  • Circular dependencies are detected and blocked

Dependency Editor

Open a release and click the Dependency Editor to manage dependencies:

  1. Add a dependency — select another release from the dropdown. The current release will not be allowed to promote until the selected dependency is Shipped.
  2. Remove a dependency — click the remove button next to an existing dependency to unlink it.

All dependency changes are audit-logged with the release names for traceability.

Running a Release

Execution is release-scoped. To start:

  1. Go to the Run page
  2. Select the release to execute
  3. Click Run and configure workers, mode, etc.

Each release gets its own coordinator, worker pool, and job queue. You can run multiple releases in parallel — the Multi-Release Status bar shows pills for each running release with progress counts. A release with jobs stuck at a gate shows a "gated" count on its pill (hover for the gate kinds and how many jobs are at each) and, for releases you're not currently viewing, a banner naming the release and gate kind — click either to jump straight to that release. See Parked releases.

Your active release selection persists server-side (keyed by user_id + project_id), so it syncs across every device signed into your account — not just the device where you picked it.

Version bumps

Trinity figures out each repository's version bump for you, rather than asking you to pick one.

  • Graded from what changed. When a release is ready to ship, Trinity looks at what landed in each repository since its last release and grades the change as a patch, minor, or major bump, with a short reason.
  • Breaking changes are caught. If a change removes a public export, route, or field — or changes a function's signature — Trinity flags it and raises that repository's bump to a major automatically, so a breaking change can't go out under a patch or minor version.
  • Check Changes proposes. The Check Changes tile shows each repository's current → proposed version, the graded bump, the reason, and any breaking changes found. Repositories the release didn't touch are listed as "not tagged". The current version counts a candidate already tagged on your integration branch.
  • An open prerelease cycle keeps its own target. When a repository's current version already carries a -label.number suffix (4.0.0-beta.0), Check Changes proposes that cycle's release (4.0.0) instead of grading a fresh bump — moving past a prerelease is a deliberate choice you make in the manifests, not something Check Changes proposes on its own. Once a repository ships its final release, Check Changes goes back to grading a fresh bump from what changed.
  • Packages sharing a repository bump on their own. Unless a fixed group ties them together, a repository holding several packages is graded package by package: each package that changed gets its own bump off its own latest tag ([email protected] → [email protected]), its own reason and its own breaking changes, and a package that didn't change isn't bumped.
  • A policy change proposes a major. On a conversion release, each group whose packages change how they version proposes the next major above every member's current version, whatever the grade, and the Check Changes tile names the change.
  • A fixed group shows one version. When a fixed release group spans your repositories, Check Changes shows the one shared version every repository it spans will be tagged with, and the per-repo detail (current tag, reason, breaking changes) stays available beside it. A breaking change anywhere in the group raises the whole group to a major.
  • Preview anytime with Check Changes. The Check Changes tile on the release dashboard runs the same grading and shows the per-repo bumps and the exact tag each would produce right now — so you can see where a release is heading before you commit to shipping it. It's read-only and safe to run at any point.

The version that ships is the one the manifests declare. Before anything is tagged or written to a changelog, Trinity checks it: every member of a fixed group must declare the same version, the version can't already be tagged on the release's line (a candidate tagged on your integration branch counts), it can't sort below a version already tagged for the same release, and a release that changes the policy must declare a new major. A version that fails pauses the release at Version Check Failed until you fix the manifests. Once a package ships, its version is recorded, so the next release proposes from it.

Release candidates

A version with a label — 2.0.0-rc.1, 3.1.0-beta.2 — makes the release a release candidate. The label belongs to the whole release: every package it tags carries the same label (each with its own number), or none does, and a mix pauses the release at Version Check Failed.

A candidate is integrated and tagged, and never shipped. Promote it with Integrate: it lands on your integration branch, gets its tags there — on dev on a dev-line project, on your base branch on a single-trunk one — and rests at On Dev. Promoting a candidate with Ship or Ship Now is refused, before anything is tagged — whatever your project's targets; a labelled version is release-wide, not a registry fact. Cut as many candidates as you need, each its own release; the final is the release whose manifests declare the version without a label, and that one ships.

A candidate's version isn't recorded as shipped, so the next release still proposes from the last final. A candidate stays On Dev for good, and the work it carried counts as released when the final of its cycle — the release declaring the same version without the label — ships: its stories are marked released alongside the final's, and a release that depends on the candidate may promote from then on. Its notes go into the changelog under Unreleased, and the final gathers its own candidates' notes beneath its version, leaving any other version's candidates under Unreleased.

Publish

A release that tags a package a Library or CLI target ships also publishes it to its registry (npm, Cargo). Every other target type has nothing to publish, and never sees any of this.

Each package publishes to the registry stored with its token in Secrets: NPM_REGISTRY for an npm package, or npm's public registry when that's unset, and crates.io for a crate. A package whose package.json names another registry in publishConfig doesn't publish; the release stops before anything reaches a registry and names both.

Every publish runs in two steps, so none of your package's own code runs while its registry token is around:

  • Build — the package is built from its release tag with no token anywhere. An npm package's workspace installs first, from its committed lockfile, frozen: Trinity reads the package manager from the packageManager field of the repo's root package.json, else from the lockfile (pnpm-lock.yaml, yarn.lock, package-lock.json, bun.lock), and installs once per repo at its root. A repo that names neither has no lockfile to install from, so it installs nothing and packs with npm pack. An install that fails fails that package's publish with the install output. The package is then packed with the same tool (pnpm pack, yarn pack, bun pm pack, or npm pack), which runs your prepack, prepare and postpack scripts and turns workspace: and catalog: dependencies into real versions. A tarball that still names a dependency as workspace:, catalog:, link: or file: doesn't publish, since nobody could install it; the release stops and names the package and the dependency. Cargo runs cargo package, which builds and verifies the crate, build.rs included. A package that builds in prepack publishes what that build produced. prepublishOnly, publish and postpublish scripts don't run.
  • Upload — the file the build produced is uploaded with the token, and nothing of the package runs: npm publish with --ignore-scripts, and cargo publish with --no-verify. A built manifest that names another version, or a crate whose sources changed while it built, doesn't upload.

The publish records the checksum of the file it uploaded. A publish picked up again after a pause builds again, and records the checksum of what that upload sent.

A publish moves through a three-way pointer:

  • pending — recorded, nothing has run yet.
  • published — the bytes are live on the registry, but under a holding tag no consumer resolves. Trinity publishes every package this way first, dependencies before dependents, so nothing a consumer installs can ever resolve a dependency that hasn't landed yet.
  • pointer_moved — the real pointer your dependents resolve — latest for a release on trunk, your prerelease pointer for a labelled release, latest-<major> for a maintenance patch — now names this version.

failed is the other branch off pending: the upload never reached the registry, or the registry refused it.

Once every package that can publish has landed under its holding tag, Trinity stops and waits for you to approve moving the real pointers — see Pointer Move. The release's own status has already moved on by then; only the publish is waiting, whatever your automation settings say for the rest of the release — this gate is never skipped.

A package's very first publish is the one exception: the registry sets its latest pointer the moment the package exists, whatever tag you configured, so Trinity raises the pointer gate for it before that publish runs rather than after.

While any package's publish is unfinished — it failed, or you stopped the publish with it still waiting or still under its holding tag — the Publish section of the Results tab shows a Retry publish button, whatever the release's status. It runs the release's publish again from the tags the release already recorded: every package that has not landed publishes, dependencies first, and nothing already on the registry is published again. The release's own status never moves, and moving a real pointer still waits for your approval at the pointer gate. Once every package has finished — its pointer moved, or, for a registry with no pointer such as Cargo or Go, its version published — the button goes away.

Stopping a publish keeps it stopped: Trinity's own retries of a partly shipped release never restart a publish you stopped, so it waits for your Retry publish. When Retry Tagging tags a package that publishes to a registry, it starts that package's publish for you.

Approving the pointer gate, or setting a missing registry token, picks the publish up exactly where it waited, from the release's tags. Nothing else about the release runs again, so work that landed on your branch while the publish waited is neither published nor merged to your production branch.

Recovery here is forward-only. A publish that already recorded a version refuses to publish a different one over it — bump the version and cut another release instead of trying to replace what the registry already has. Stopping the pointer gate rather than approving it leaves an already-published package published, under its holding tag, exactly as it was: there is no un-publishing a version, so Trinity never attempts it.

Release Detail Panel

Click any release card to open its detail panel. What's visible depends on the release's status; the major surfaces are:

  • Status badge — current lifecycle state. Shows Parked instead of Promoting when a job is stuck at a gate, with a reason row underneath — see Parked releases.

  • Story progress — bar showing merged / released count vs. total.

  • Release Dashboard — five tactical tiles sit at the top: Run Project, Run SEO, Run Audit, Check Changes, and Generate Notes. Each runs its standalone agent against the current release worktree without advancing the lifecycle. Check Changes previews the per-repo version bumps from what changed (and the tag each would produce) — see Version bumps. While a promote is in flight or the release holds live staging placements, every tile except Run Project and Check Changes (both read-only) shows a Locked badge — re-running a tactical agent then would diverge from what's already on dev or staging.

  • Promote panel — the primary promotion control. A three-way segmented control picks the mode, and the action beneath it fires the matching route:

    • Ship — opens the Stage Target Picker: check the staging targets you want, click Stage selected to place the release onto them, then Ship to promote all the way to production through staging. Typing a branch the list doesn't offer creates it as a new staging target on the spot — it's kept and offered again on your next release, not a one-time entry.
    • Integrate — promotes straight to your integration branch and stops at On Dev (no version tag, unless the release is a release candidate, which is tagged there). The button reads Integrate to Dev or Integrate to Base, per your topology.
    • Ship Now — walks the same route as Ship, but bypasses the staging gate. On a hotfix release or a patch release this is the only mode, since neither has an integration-line resting point to stop at.

    Each mode's description names the branches your project actually walks: release → dev → main on a dev-line project, release → base plus the tag on a single-trunk one. A hotfix release overrides both — the panel names its own route, release → production and then the forward-port back onto dev. A patch release likewise names its own route, release → maint/<line>.

    A separate manual-override row of Mark … buttons is there for corrections when you need to force a status.

  • Staging Drift / CI tiles — while staging is active, these scope to a selectable dimension (the Dev row or one staging target). Staging Drift shows whether that dimension's branch has moved since the release was placed; the CI tile surfaces its checks so you don't have to switch to GitHub.

  • Status — a two-level accordion. The outer rows are the release's staging placements (one per target); each expands to that target's per-repo promotion ledger (pending → integrating → integrated → shipping → main_landed → shipped, or failed). When the release holds no placements it still lists the per-repo states directly.

  • PRDs list — every PRD assigned to the release. Each row has a Move action (reassign to another release) and a Reparent action (change parent release without moving the PRD's stories). Both are available only while the target release is created or in_progress.

  • Dependency Editor — manage release dependencies.

  • Results tab — after a promote, shows each package's tag and release notes, and preflight results grouped by package. Every package in a repo carries that repo's tag. For a package a registry target ships, it also shows the publish ledger — ecosystem, version, pointer and state, plus the registry's own refusal and, while any package's publish is unfinished, a Retry publish button, in the order the packages publish, dependencies first — with a copyable install line once a consumer can actually resolve it: cargo add <name>@<version> the moment Cargo publishes, npm install <name>@<pointer> only once npm's pointer has actually moved onto it, since the version sits behind a holding tag until then. For Partially Shipped releases it names each repo that hasn't shipped with its last failure reason, and offers Retry ship for one whose promote hop itself failed or Retry Tagging for one that reached production untagged.

  • Delete — permanently deletes the release (disabled for shipped releases or those with active jobs).

When stories in linked PRDs have active execution jobs, editing is locked to prevent conflicts.

Automation Tab

Releases also have an Automation tab for per-release overrides. In addition to the standard automation settings (auto-merge, squash merge, etc.), releases support:

  • Delete release branch — clean up the release branch after completion

Release overrides are editable only while the release is clean (no stories have started). Once any story in the release moves past pending, the effective setting is frozen and further edits are disabled. These overrides follow the same cascade as stories: Trinity defaults → Workspace → My workspace → Project → My project → Entity → Job.

Auto-merge never reaches the pointer gate. However far your automation settings carry a release on its own — auto-merge, straight through to a registry publish — the pointer move onto a real dist-tag always waits for a person. Every package that can publish still lands under its holding tag on its own; only moving the tag your dependents resolve needs you.

Releases Page Layout

Above both views sits a Version State panel, one row per package: its current (shipped) version and the highest version anything has actually tagged for it so far. Once a package has been published at least once, its row also shows which registry dist-tag pointer serves what — latest for trunk, the prerelease pointer for open release candidates, and each maintenance line's own latest-<major> — shown only for npm, the one ecosystem with a dist-tag to move. A package with an open release candidate cycle also lists its candidates in order; every one but the newest names the candidate that overtook it. The moment a final actually lands, the whole cycle closes and drops off this list — its version is what currentVersion / the tagged version already show.

Right below it, whenever there's one to make, sits the new-family offer — an action prompt, so it reads before the board rather than after it.

The Releases page has two views, toggled at the top right — Board and Staging Targets.

The Board view lays out one column per status:

  • Not Started — created (no stories merged yet)
  • In Progress — in_progress (at least one story merged, more pending)
  • Ready to Release — ready (every story terminal, awaiting a promote)
  • Promoting — promoting (a promote hop is in flight)
  • Promote Failed — promote_failed (a hop errored)
  • On Dev — integrated (landed on your dev branch, not yet on main)
  • Partially Shipped — partially_shipped (some repos reached production, others pending)
  • Shipped — shipped (historical)

Every status has its own column, so a release is always visible somewhere on the board; columns with no releases are hidden.

The Staging Targets view flips the board around: instead of grouping by status, it lists your project's staging target branches and shows which release currently holds each one — so you can see at a glance what's deployed where and which targets are free.

A project with at least one maintenance line also lists its supported versions below the board, whichever view is showing.

Deleting a Release

You can delete a release if:

  • It is not in shipped or partially_shipped status (shipped releases are historical records)
  • It has no PRDs: move or remove them first
  • No other active releases depend on it
  • It has no active execution jobs
  • No patch targets it, when it is a maintenance line
  • No stories have been executed under it

A release that fails one of these refuses to delete and says which.

Deletion is permanent: the release and the stories parented on it directly are removed, not hidden. Trinity also closes those stories' open issues on your repository host as not planned.