Skip to content

Cutting a release

A release promotes dev to main. That is the only time main moves, and it is a different lifecycle from everyday change work: a change is triggered by an issue and lands on dev; a release is triggered by a decision to ship what dev already holds.

/ff-release <semver> drives it. Omit the version and it reads the current one, proposes the next bump from the change set (breaking → major, feat → minor, fix/chore only → patch), and confirms before editing anything.

The command aborts unless all of these hold. It never releases from a dirty or behind tree.

Terminal window
git rev-parse --abbrev-ref HEAD # must be dev
git fetch origin
git status --porcelain # must be empty
git rev-list --left-right --count origin/dev...HEAD # must be 0 0

dev CI must also be green on origin — gh run list --branch dev --limit 5.

ComponentVersioning
backend/pyproject.tomlThe single source of truth. The only backend version literal
backend/packages/**/pyproject.tomlDeclare dynamic = ["version"] and read the root through hatchling — nothing to bump
frontend/package.jsonIts own literal, tracking the backend version
docsite/package.jsonIndependent. Not bumped in a normal app release, only when the docs themselves are released
cli/No version field — tagged with the repo

So a backend bump edits one line. The 19 member packages follow automatically, which is deliberate: they each used to carry their own literal, and they drifted — factflow-confluence sat at 0.1.0 while the rest said 0.5.1 and the tags had reached v0.7.2.

Two consequences worth knowing:

  • Regenerate the lock with uv lock after the bump. frontend/bun.lock does not record the root package version, so it will not change.
  • A local venv keeps the old version until you reinstall. Editable installs cache their metadata, so a plain uv sync after a bump leaves member packages reporting the previous number. Use uv sync --all-groups --reinstall. Fresh builds — CI and Docker — are always correct.
Terminal window
cd backend && uv run ruff check . && uv run ruff format --check . && uv run pytest && cd ..
cd frontend && bun run check && bun run build && cd ..
cd cli && go build ./cmd/factflow/ && go vet ./... && go test -race ./... && cd ..

Note that the backend step runs uv run pytest with no path. That is deliberate here and is the one place it applies. During development you always scope pytest to the package you touched, because the full suite starts Testcontainers for PostgreSQL, Artemis, RabbitMQ and Pulsar and takes a long time. A release is the opposite trade: the point is to prove the whole suite is green before anything reaches main, so the cost is the feature.

Do not release red. If the OpenAPI snapshot is part of the release surface, regenerate it and confirm it is committed.

ArtifactForm
Bump commitchore(release): vX.Y.Z
Release PRtitle Release vX.Y.Z, --base main --head dev
Tag / GitHub releasevX.Y.Z

Release notes summarize merged work grouped by type (feat / fix / chore) from git log --oneline origin/main..origin/dev. No AI attribution, no marketing URLs.

The radicalAI org applies a ruleset named GitHub Flow to every repository's default branch. Two of its rules matter here:

  • pull_request — one approving review
  • required_linear_historymerge commits are not allowed on main

A release PR merged normally produces a merge commit, so the second rule rejects it. A repository admin is a bypass actor on that ruleset, so the merge goes through with:

Terminal window
GH_HOST=dnb.ghe.com gh pr merge <PR> --repo radicalAI/factflow --merge --admin

That is a deliberate override, not a formality — it bypasses the approval requirement as well, so get the review first.

The alternative is --rebase, which satisfies linear history without any override. It costs you branch identity: rebasing replays every commit as a new object, so main and dev no longer sit at the same SHA afterwards and each will report the other as diverged. If you want the two branches identical after a release, take the admin merge.

The ruleset is org-level, so it cannot be changed from this repository. Ask an org admin to amend ruleset 6141 if this becomes a recurring obstacle — and ask only about required_linear_history, since the other rules in it are ones we want.

Tag main at the release commit with gh release create vX.Y.Z --target main, then return to dev. No back-merge is needed because dev was the source, but confirm main and dev match at the release commit.

If main has picked up a commit of its own — a hotfix applied directly to it — the two branches will not match even after the release. Bring it back with a fast-forward of dev to main once main contains everything dev had, so the next release does not carry the gap forward.