rafa pages
- rafa
- 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 subject actions it runs, in order:
rafa loop startwrites and commits the fragment in its wrap-up;rafa release fragmentwrites one by hand for a branch with no wrap-up, such as a dependency bump.rafa pr mergelands each branch.rafa release settlefolds what waits into one version, one changelog section and one commit.rafa release tagtags the commit that set each version, and prints the push and publish lines.rafa release publishruns 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.
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:
| Level | release.settle | release.tag | release.publish | Runs on its own |
|---|---|---|---|---|
| 0 Manual | any | manual | manual | nothing |
| 1 Settle tags | push | settle | manual | version, changelog and tag in one settle |
| 2 CI publishes | push | manual | ci | a pushed tag runs the checks and publishes |
| 3 Release PR | pr | settle | ci | every merge updates one release pull request; merging it tags and publishes |
| 4 Continuous | push | settle | ci | every 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.