Releasing Flow
Flow releases as openclaw/openclaw does. A release is a release train: it
ships what main holds at the cut, and nothing waits for a named feature.
A release agent picks the cut, a main commit that the 3-hourly Flow Full
Release Validation proved, and cuts a release branch release/YYYY.M.N
from it. After the cut, only blocker fixes enter the branch, as -x
cherry-picks of merged main commits. Each release is one annotated tag on its
release branch (signed once the maintainer sets a key), built once and published three ways: an npm package, a
container image, and a GitHub Release that records both. After a stable
release, a closeout pull request brings what shipped back to main. An
installation runs a release it chose, by version and fingerprint, never a
branch.
What a release produces
| Output | Where | How you verify it |
|---|---|---|
npm package @arcforgelabs/flow@<version> |
npmjs.com | npm audit signatures after install: the registry signature and SLSA v1 provenance from trusted publishing. No npm token is stored anywhere. |
Image ghcr.io/arcforgelabs/flow@sha256:… |
GHCR | gh attestation verify oci://ghcr.io/arcforgelabs/flow@sha256:… --repo arcforgelabs/flow: GitHub's build provenance and SBOM attestations for that digest |
GitHub Release v<version> |
Releases | sha256sum -c <file>.sha256 for each asset |
Each GitHub Release has these assets, each with a .sha256 file:
release-manifest.json: the version, channel, commit, the result of every check lane, the npm integrity and shasum, the tarball's SHA-256, the image digest and tags, and the schema migrations the release contains.postpublish-evidence.json: what the publish run verified from outside, after publishing:npm audit signatures, the npm provenance type, and the image's verified attestations.arcforgelabs-flow-<version>.tgz: the npm tarball, byte for byte.flow-image-<version>.spdx.json: the image's SBOM.flow-server-<version>.cdx.json: an SBOM of the server's production dependencies.
The notes are the release's changelog file, CHANGELOG/<version>.md
(CONTRIBUTING.md, .agents/skills/flow-changelog-update/SKILL.md). Every
release needs one: Flow Release Prepare refuses to tag a release unless the
file's record lists every pull request in its range with GitHub's credit,
and its Highlights list has 1 to 8 bullets. A stable release aims for 5 to 8
highlights, as OpenClaw's do, but that is a guideline for the writing, not a
gate on the release: like OpenClaw's patch releases (2026.10.6, 2026.10.7), a
small stable release ships with the user stories it has, and verify only
warns. Never invent highlights. After the GitHub Release is published, Publish
checks that its body is the file. A release tagged before the changelog
existed keeps GitHub's generated notes.
Each stable release and correction also gets a docs page,
docs/releases/<version>.md, which changelog.mjs write writes from the
file. The closeout brings it to main, and the docs site shows it at
https://arcforgelabs.github.io/docs/flow/releases/. After the docs site
serves it, the release's GitHub Release body becomes the roster body
(changelog.mjs thanks): the numbers, the docs and changelog links, and
every person GitHub credits.
Versions and channels
Versions are YYYY.M.N with no leading zeros: the year, the month, and a
count of releases in that month. The count is not the day, and it can pass
31: 2026.10.31 comes before 2026.10.32. A new month can start again at 1.
A stable release is 2026.10.18. A beta is a pre-release of the upcoming
stable version, so 2026.10.18-beta.1 and 2026.10.18-beta.2 come before
2026.10.18. A correction is 2026.10.18-1: a fix of a published stable
release from its release branch, when that version cannot be published
again (npm never replaces a version). Corrections count from 1 and come
after their stable release. Tags add a v, are annotated (signed when a key is set), and
each GitHub Release is titled flow <version>. On a release branch,
package.json holds the branch's version; a beta and a correction take
their own version only in the packed tarball. main holds the newest
shipped version, set by each closeout. scripts/release/release.mjs holds
the rules, and test/release.test.js tests them.
| Channel | npm dist-tag | Image tags | GitHub Release |
|---|---|---|---|
| beta | beta |
<version>, beta |
pre-release |
| stable | latest |
<version>, stable, latest |
release, marked latest |
| correction | latest |
<version>, stable, latest |
release, marked latest |
A release reaches its channel when it is published; there is no separate step. The GitHub Release stays a draft until npm serves the version, then Flow NPM Release makes it public, as openclaw/openclaw does, so an installation that follows GitHub's latest release never sees a version npm does not have.
The workflows
| Workflow | Starts | Does |
|---|---|---|
flow-full-release-validation.yml (Flow Full Release Validation) |
Every 3 hours on main; on a release branch after each change there (Flow Release Branch); by hand |
Proves one exact commit with every lane a release needs: all CI lanes (test, docker-build, package) and the secret scan over the whole history. On a release branch it also requires package.json to hold the branch's version and each changelog file of that version to match its history. A scheduled run reuses an earlier pass of the same commit. Publishes nothing. |
flow-release-branch.yml (Flow Release Branch) |
By hand or from release:stable, from main |
The only writer of release/YYYY.M.N. cut creates it at a picked main commit with one release commit on top: package.json's version and the version's changelog files from notes_ref. cherry-pick copies merged main commits with git cherry-pick -x (blocker fixes only), with refreshed notes if given. notes replaces the version's changelog files. Each push starts the validation, CI and the secret scan on the branch. |
flow-release-prepare.yml (Flow Release Prepare) |
By hand or from release:stable, from main |
Takes a commit on a release branch (default: its head) that Flow Full Release Validation proved. Plans the next tag for the channel (beta, stable or correction), verifies the changelog, creates the tag annotated and signed with the release tag key, and starts Flow Release Publish. |
flow-release-checks.yml (Flow Release Checks) |
From Flow Release Publish | Requires the tag to be annotated before it checks anything out, and its signature verified by GitHub when RELEASE_REQUIRE_SIGNED_TAGS is true; to be a calendar version that matches package.json; and to sit on its release branch. Runs every CI lane on its commit, packs the npm tarball with a shrinkwrap of the tested dependencies, then installs it in an empty directory and starts it against PostgreSQL. Publishes nothing. |
flow-release-publish.yml (Flow Release Publish) |
From Flow Release Prepare, or by hand for an existing tag | approve job at the start, alongside the checks: environment release for a stable release or correction, release-beta for a beta. Then the image job pushes the image by digest, attests it and its SBOM, verifies the attestation, starts the pushed digest against PostgreSQL, then adds the channel's tags. Then it creates the GitHub Release as a draft with the manifest, checks its body is the changelog file, and starts Flow NPM Release for the tarball it proved. |
flow-npm-release.yml (Flow NPM Release) |
From Flow Release Publish | Requires the tarball from that run to match the GitHub Release's manifest. Dispatched by Flow Release Publish, it publishes on that run's approval: it checks that GitHub Actions dispatched it, that the run is for this tag, and, for a stable release, that GitHub's approvals record shows release approved. Dispatched by hand, it waits for npm-release first. publish runs in environment npm-publish: npm trusted publishing with provenance, to the channel's dist-tag. Then it verifies what npm serves, attaches the post-publish evidence and makes the GitHub Release public. For a stable release or correction it starts Flow Release Closeout. |
flow-release-closeout.yml (Flow Release Closeout) |
From Flow NPM Release, or by hand | openclaw/openclaw's stable main closeout. Requires every commit of the release branch that main lacks to be the release commit, a changelog commit or a -x cherry-pick of a main commit. On branch release/<version>-main-closeout from main, it copies the version's changelog files and sets package.json to the shipped version (unless main is newer), then opens chore(release): close out <version> on main and starts its checks. It does not merge. |
flow-release-promote.yml (Flow Release Promote) |
By hand or from release:stable, from main |
Points a channel at a published release, to roll it back or put it back, and moves beta up after a stable release. It verifies the release (the manifest's checksum, the npm integrity, signatures and provenance, and the image attestation), then in environment npm-release moves the channel. A stable tag moves npm latest, GHCR stable and latest, and GitHub's latest release, and moves beta up when the beta channel is behind. A beta tag moves npm and GHCR beta. |
supply-chain.yml (Supply Chain) |
A pull request that changes package.json, package-lock.json or the check itself; every push to main; weekly; by hand |
Checks the lock file (every dependency has an integrity hash and a resolved URL on https://registry.npmjs.org/), installs with npm ci --ignore-scripts, then runs npm audit signatures. On a pull request, dependency review fails on a high or critical advisory and on a new dependency with no licence. Not a required check on main. |
| Action | Approvals |
|---|---|
Beta (vYYYY.M.N-beta.B) |
0 |
Stable release (vYYYY.M.N) or correction (vYYYY.M.N-C) |
1: release (switched off for now, by the maintainer's decision; the one difference from openclaw/openclaw) |
| Promote or roll back a channel | 1: npm-release (switched off for now) |
| Flow NPM Release dispatched by hand | 1: npm-release (switched off for now) |
Every release workflow that changes anything runs only from main, so the
release tooling is main's even when the release is an older branch; every
environment accepts only runs from main, and npm-publish, the
environment npm's trusted publisher names, has no reviewer of its own.
Release tags (v20*) cannot be moved or deleted
(.github/rulesets/release-tags.json), and release branches cannot be
deleted or force-pushed (.github/rulesets/release-branches.json). Each job
that builds or publishes checks out the tag itself, never a commit handed on
from an earlier job, and stops if the tag no longer points at the commit the
checks proved (test/release-workflows.test.js holds this rule). A lightweight tag
(every tag from before release branches) is never published again: fix forward with a new version.
main's CI health never gates a release: validation and publication run on
the release branch. A red main is fixed on main, in parallel.
npm can take minutes to serve a new version. Flow NPM Release reads it back with backoff for about 15 minutes, and a rerun skips a version npm already serves with the manifest's integrity, so rerunning its failed jobs is safe.
Running Flow Release Publish again for the same tag is safe. It checks again, updates the GitHub Release's assets, and starts Flow NPM Release, which skips an npm version that is already published with the same bytes and refuses one published with different bytes. A published version is never replaced: fix forward with a correction or a new version.
Release a stable version
One command runs the whole stable release, as openclaw/openclaw's
pnpm release:stable does. A release agent runs it when the maintainer asks
for a release, and follows .agents/skills/flow-release/SKILL.md:
git fetch --unshallow --tags origin # or: git fetch --tags origin
npm run release:stable -- 2026.10.34
It is a resumable state machine with the phases cut → validate → publish → flip-github → sync-beta → closeout → docs → thanks. State lives in
.artifacts/release-<version>/state.json; running the command again
continues from the first phase not completed, --from <phase> starts again
from a phase, --status prints the table, and --dry-run prints every
command that would change anything without running it. Every refusal prints
Next: with the exact commands to run before resuming. It waits for no
person: the maintainer's one step is the installation's own deploy. For now
that is on purpose, so releases stay fast. --hold adds openclaw's two
stops for when they are wanted: confirm the cut (--confirm-cut-sha <sha>)
and approve publication (--approve-publication, which also merges the
closeout).
-
cut. The command picks the newest
maincommit that Flow Full Release Validation proved (or--cut-sha), and refuses until the version's notes exist (and, with--hold, until that commit is confirmed). Write them at the cut (.agents/skills/flow-changelog-update/SKILL.md), with up to 8 highlights:node scripts/release/changelog.mjs draft --version 2026.10.34 --target <cut> --output /tmp/section.md --manifest /tmp/manifest.json # edit the Highlights, Changes and Fixes node scripts/release/changelog.mjs write --version 2026.10.34 --file /tmp/section.md node scripts/release/changelog.mjs verify --version 2026.10.34 --target <cut>Commit
CHANGELOG/on any branch, push it, and run the command again with--notes-ref <branch>. Flow Release Branch cutsrelease/2026.10.34with the release commit: the version and the notes, in one commit. The notes never go through a pull request tomain; the closeout brings them there. -
validate. Flow Full Release Validation runs on the branch's head.
-
publish. Flow Release Prepare tags the head (signed when a key is set), Flow Release Publish and Flow NPM Release run, and the phase ends when npm serves the version.
-
flip-github. The GitHub Release is public and GitHub's latest.
-
sync-beta. npm
betamoves up to the release when it is behind (Flow Release Promote), so beta users are never behind stable. -
closeout. Flow NPM Release started Flow Release Closeout; the command opens its pull request if GitHub Actions could not, waits for
testandgitleaks, and merges it.mainthen carries the shipped changelog file, its docs page and the version. -
docs. Docs Sync Publish mirrors
docs/toarcforgelabs/docs, and its Pages workflow deploys the site. The phase ends whenhttps://arcforgelabs.github.io/docs/flow/releases/<version>.mdis the pagemainhas. -
thanks. openclaw/openclaw's step after the docs deploy: the command reads the GitHub Release again, replaces only its body with
node scripts/release/changelog.mjs thanks --version <version>, and reads it back (verify --check-github --post-docs). The release is done here.
A beta needs no command of its own: after the cut, start one from the
branch. Its notes are its own file, CHANGELOG/<version>-beta.<n>.md, from
the release npm's beta tag selects, put on the branch with Flow Release
Branch notes:
gh workflow run flow-release-branch.yml --repo arcforgelabs/flow --ref main -f operation=notes -f version=2026.10.34 -f notes_ref=<branch>
gh workflow run flow-release-prepare.yml --repo arcforgelabs/flow --ref main -f channel=beta -f branch=release/2026.10.34
Blockers, flakes and the cut
The release branch is the work queue. main moves on; the branch takes
only fixes for confirmed release blockers: a required lane that fails
deterministically on the branch, or an install, update or publish defect.
A flake is not a blocker, and neither is a dependency advisory (only known
malware stops a release). Every failed check gets a written decision,
blocker or flake. Rerun a flake on the same commit at most twice, and fix it
on main in parallel. Never cut again on newer main to pick up a fix, and
never cut again without the maintainer's word.
A blocker fix merges to main first, then reaches the branch:
gh workflow run flow-release-branch.yml --repo arcforgelabs/flow --ref main \
-f operation=cherry-pick -f version=2026.10.34 -f commits="<main sha> <main sha>" -f notes_ref=<branch>
The pick adds its pull request to the release's range, so the stable notes
need it in their record: give notes_ref the refreshed notes (drafted
again at the branch's head) in the same run, or run notes after it. When a
pick does not apply cleanly, open a pull request into release/<version>
with the resolved cherry-pick, keeping its (cherry picked from commit …)
line, and merge it. Release-process changes (workflows, release scripts and
tests, this file) land on main first too, and reach an open branch the
same way.
Correct a stable release
When a published stable release needs a fix and its version cannot be
published again, cherry-pick the fix onto its branch, write
CHANGELOG/<version>-<n>.md (the same range as the stable release's notes,
plus the fix, with up to 8 highlights), put it on the branch, and prepare a
correction:
gh workflow run flow-release-prepare.yml --repo arcforgelabs/flow --ref main -f channel=correction -f branch=release/2026.10.34
Only the newest stable release takes a correction. It publishes to npm
latest as 2026.10.34-1, and Flow Release Closeout brings its notes to
main.
Roll back
To roll back, point the channel at the previous release (a stable tag for
latest, a beta tag for beta). Nothing is unpublished:
gh workflow run flow-release-promote.yml --repo arcforgelabs/flow --ref main -f tag=v2026.10.33
Prepare has no dry run: the tag it creates is protected and cannot be
deleted, and it starts the publish at once. npm run release:stable -- <version> --dry-run prints what a release would run. To test the release
steps without publishing, run Flow Full Release Validation on any branch, or
look at CI on any pull request: its package and docker-build lanes run
the same smokes as a release.
Run a release
docs/SELF_HOSTING.md describes how to pin, verify and run a release with
Docker, with Node, or on Replit. docs/REPLIT.md describes a Replit
installation that follows a channel (FLOW_DEPLOY_REF=stable).
One-time setup
Done on 18 Oct 2026, with gh. Each item can be undone in the repository
settings:
- Environments
releaseandnpm-release: required reviewer Samuel.release-betaandnpm-publish: no reviewer. All four deploy frommainonly. - The rulesets on
mainand on release tags (CONTRIBUTING.md, "Merging to main").
Ready for when releases go more secure, by Samuel; not needed now, and no release waits for them (decided 20 Oct 2026: releases stay fast, with the installation's deploy as the one step):
-
The release branch ruleset. Settings → Rules → Rulesets → New ruleset → New branch ruleset. Name
Protect Flow release branches, enforcement Active, no bypass list. Target branches → Add target → Include by patternrelease/20*; Exclude by patternrelease/*-main-closeout. Rules: tick Restrict deletions and Block force pushes, nothing else. Create. (Or New ruleset → Import a ruleset, with.github/rulesets/release-branches.json.) -
The release tag signing key. Prepare signs each tag with an SSH key. Make one on your own computer:
ssh-keygen -t ed25519 -C "flow release tags" -N "" -f flow-release-tags. Then:- Repository Settings → Secrets and variables → Actions → New repository
secret: name
RELEASE_TAG_SIGNING_KEY, value the whole private key fileflow-release-tags. - Your GitHub account Settings → SSH and GPG keys → New SSH key: title
Flow release tags, key type Signing Key, key the line inflow-release-tags.pub. GitHub then shows each release tag as verified, signed by you, and Flow Release Checks refuses any tag it does not show so. - Delete both files from your computer.
- Then set the repository variable
RELEASE_REQUIRE_SIGNED_TAGStotrue, so a missing or bad signature stops a release.
Prepare tags as
IAMSamuelRodda <7961255+IAMSamuelRodda@users.noreply.github.com>; a repository variableRELEASE_TAGGER(Name <email>) names another account that holds the signing key. - Repository Settings → Secrets and variables → Actions → New repository
secret: name
-
The closeout pull request. Arc Forge's enterprise policy keeps Allow GitHub Actions to create and approve pull requests off, so Flow Release Closeout opens its pull request with the Barnacle App token (
BARNACLE_APP_IDvariable,BARNACLE_APP_PRIVATE_KEYsecret), as openclaw/openclaw's workflows use an App token. The App needs Pull requests: Read and write on this repository. Without the token,release:stableopens the pull request itself.
Later, when Flow has many paying clients and becomes a target: sign each tag with a key that a person holds, as openclaw/openclaw's maintainers do. This is not for now (decided 20 Oct 2026); the CI key stays until then. A touch on a hardware security key (FIDO2, for example a YubiKey) is the control: no tag without a touch.
- Make the key on the hardware key:
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "flow release tags", and add its.publine to your GitHub account as a Signing Key. release:stablestops before the tag and prints one command. The maintainer runs it on their own computer, touches the key, and pushes the signed tag:git tag --sign v<version> <commit> && git push origin v<version>.- Flow Release Prepare takes the existing tag in place of making one. Flow Release Checks already refuses a tag that GitHub does not verify.
- Delete the
RELEASE_TAG_SIGNING_KEYsecret.
Until hardware keys are in use, a key held in the computer's secure chip, with a fingerprint check on each use, gives the same control (on macOS, for example, the Secretive app keeps it in the Secure Enclave). Browser and phone passkeys cannot sign git tags: OpenSSH cannot use them.
Done before the first beta (18 Oct 2026):
npm (an npm owner of the @arcforgelabs scope does this on npmjs.com, with
two-factor authentication; never with a token in a file or a command):
-
Trusted publishing cannot create a package. Publish a placeholder once: a directory with only a
package.jsonthat has"name": "@arcforgelabs/flow","version": "0.0.0-placeholder","license": "MIT"and this repository's URL (git+https://github.com/arcforgelabs/flow.git). Runnpm login, thennpm publish --access public --tag placeholder, thennpm logout. -
On the package's Settings, under Trusted publishing, add two GitHub Actions publishers for organization
arcforgelabs, repositoryflow:- workflow
flow-npm-release.yml, environmentnpm-publish, allowed to publish; - workflow
flow-release-promote.yml, environmentnpm-release, with Allow npm dist-tag on and publishing off.
npm checks the workflow file of the run's top-level workflow, so these names must match the files exactly. Renaming a file breaks publishing until its trusted publisher is changed here.
- workflow
-
Under Publishing access, choose Require two-factor authentication and disallow tokens.
GHCR: the first publish creates the flow container package as private,
linked to this repository. Before that, check that the organisation has no
flow container package already
(https://github.com/orgs/arcforgelabs/packages?repo_name=flow). If one
exists that this repository did not create, empty or remove it first: it
becomes public in the next step. After the first beta, on the package's
page, open Package settings, then Change visibility, and choose
Public. Under Manage Actions access, make sure arcforgelabs/flow
has the Write role. Until the package is public, Replit and Docker
installations cannot pull the image anonymously; npm and the Replit channel
deploy do not need it.