Skip to content
open tomato/ rafa
GitHub
rafa pages
  1. rafa
  2. The release flow

The release flow

How merged branches become versions, from the loop's fragment through settle, tag and publish, and the ladder from manual tags to CI publishing.

A branch never owns a version number. It carries a release fragment instead, and the version is decided on the base branch after the merge. That is what lets several branches, from several loops or machines, merge in any order without two of them claiming the same version.

The release flow, at its default level: each step is typed by you.
The release flow, at its default level: each step is typed by you.100%

The subject actions it runs, in order:

  1. rafa loop start writes and commits the fragment in its wrap-up; rafa release fragment writes one by hand for a branch with no wrap-up, such as a dependency bump.
  2. rafa pr merge lands each branch.
  3. rafa release settle folds what waits into one version, one changelog section and one commit.
  4. rafa release tag tags the commit that set each version, and prints the push and publish lines.
  5. rafa release publish runs the publish command for each unit tagged at HEAD, or CI does it from level 2 of the ladder.

rafa release status and rafa release list read where a release stands at any point, and rafa doctor warns while fragments wait.

NOTE

This shows the default sequence, the most common case. The ladder below already moves some steps to settle or CI. Workflows become configurable before 1.0, once workflow config lands (#71, #119). The config shape is [TBD: the workflow config shape].

Fragments

At the end of a run, the loop writes a fragment and commits it: a small markdown file in .changes/ (the release.fragments key moves it), one per plan. It names the plan, a title and a level, then one line per change. This one is from rafa's own repository, trimmed:

---
plan: rafa-1018-bug-sweep-21
title: Bug sweep 21 — loop start reads its branch and its base one way
level: patch
---

- loop: `rafa loop start` reads its branch and its base one way. …

The level is patch, minor, major or none. none is written on purpose and ships nothing, so "no release" is visible too.

The pull request shows a forecast: the version the branch would get if it merged now.

Settle

rafa release settle

settle folds every fragment waiting on the base branch into one version and one changelog section, deletes the fragments it folded, and pushes. When several fragments wait, the highest level wins and the version moves once: two minor fragments make one minor bump, not two. --dry-run shows the result and writes nothing.

Below 1.0.0, the levels follow npm's rules: from 0.4.0, a patch gives 0.4.1, a minor gives 0.5.0, and a major gives 1.0.0. So below 1.0.0 a minor is the step that may break.

Two situations show why settle runs on the base branch:

  • One person, two machines. Each machine finishes a plan. Both pull requests merge in any order, and whichever machine settles first releases both; the other finds nothing to do.
  • A reviewer merges through GitHub's button. Nothing runs then; the fragments wait, and the next settle, from any machine, picks them up.

rafa doctor warns when fragments are waiting, and rafa next runs settle as one of its steps.

Several loops, one release

Fragments are what let several loops end in one release. Each loop's branch carries its own fragment, and nothing about versions is decided until settle runs on the base:

  • Two loops on one machine, in two worktrees: both pull requests merge, and one settle releases both.
  • Two machines: whichever settles first releases every fragment that has reached the base; the other finds nothing to do.
  • A stretch: every item's pull request merges into the stretch branch, the stretch branch merges into the default branch, and one settle after that releases the whole batch as one version (Stretches).

When several fragments wait, the highest level wins: two minor fragments and a patch make one minor bump.

The ladder

The release keys move a project up one level at a time, from nothing running on its own to a release on every merge:

Levelrelease.settlerelease.tagrelease.publishRuns on its own
0 Manualanymanualmanualnothing
1 Settle tagspushsettlemanualversion, changelog and tag in one settle
2 CI publishespushmanualcia pushed tag runs the checks and publishes
3 Release PRprsettlecievery merge updates one release pull request; merging it tags and publishes
4 Continuouspushsettlecievery push to the default branch settles, then publishes

Every project starts at level 0. In a project with a version file and a changelog, rafa doctor prints the level your keys read as, and names what that level still lacks. It never refuses a setting. With release.tag: settle alone:

Release ladder: level 1, Settle tags.

The Configuration page has the block for level 1, and the protected-branch block (release.settle: pr), which reaches level 3 only once release.publish is ci as well; until then rafa doctor reads it as level 0 and says why. Level 2 needs the key release.publish: ci and three steps outside the config: rafa release workflows --write writes the publish workflow, the package gets a trusted publisher on npmjs.com naming that workflow (trusted publishing needs npm 11.5.1 or newer), and the repository variable RAFA_RELEASE_PUBLISH is set to ci:

gh variable set RAFA_RELEASE_PUBLISH --body ci

Until the variable is ci, the publish job is skipped, and a skipped job is not a failed run. [verify: the level 2 steps, run against a repository with a trusted publisher; not run here]

Several packages in one repository

A repository with workspace packages releases each publishable one on its own. A fragment names the package with a fourth front-matter line, package: <name>; a fragment without it is the root's. Settle then moves that package's version, changelog and tag beside the root's, in one commit. A package with no fragment keeps its version through a release, and a plan that changes two packages writes two fragments.

rafa's own repository works this way; Repository layout says which of its packages publish.

Decisions not taken

A release strategy rafa enforces on every project was rejected, because no single plan covers every project. One version for every package was rejected too: every fix to one package would release all of them.