Universal semantic versioning for monorepos and classic repos.
FerrFlow reads your commit history, determines the right version bump, updates your version files, generates a changelog, and creates a tagged release — for any language, any repo layout.
Most versioning tools are tied to a specific ecosystem (semantic-release for JS, cargo-release for Rust) or require a Node.js runtime. FerrFlow is a single compiled binary with no runtime dependencies.
| Tool | Monorepo | Multi-language | Runtime |
|---|---|---|---|
| semantic-release | plugins | JS only | Node.js |
| changesets | manual | JS only | Node.js |
| knope | limited | partial | none |
| FerrFlow | native | yes | none |
| Format | File | Ecosystem |
|---|---|---|
| TOML | Cargo.toml |
Rust |
| TOML | pyproject.toml |
Python |
| JSON | package.json |
Node.js |
| XML | pom.xml |
Java / Maven |
| CSProj | *.csproj |
.NET (C#, F#) |
| Gradle | build.gradle |
Java / Kotlin |
| Helm | Chart.yaml |
Kubernetes / Helm |
| Go | go.mod |
Go (tag-only, no file write) |
| Text | VERSION, VERSION.txt |
Any |
Cargo
cargo install ferrflownpm
npm install -D ferrflowDocker
docker run ghcr.io/ferrflow/ferrflow:latest checkPre-built binaries
Download from Releases.
# Preview what would be bumped
ferrflow check
# Run a release
ferrflow release
# Dry run
ferrflow release --dry-run
# Force a specific version (skips commit analysis)
ferrflow release --force-version 2.0.0 # single repo
ferrflow release --force-version api@3.0.0 # monorepo
# Pre-release
ferrflow release --channel beta
# Scaffold a config file
ferrflow init
# Scaffold a config file in a specific format
ferrflow init --format json5
# Use a specific config file
ferrflow check --config path/to/ferrflow.toml
# Or set via environment variable
FERRFLOW_CONFIG=path/to/ferrflow.toml ferrflow check
# Print current version
ferrflow version # single repo
ferrflow version api # monorepo, specific package
# Print last release tag
ferrflow tag
ferrflow tag api
# JSON output (for scripting)
ferrflow version --json
ferrflow tag --json
# Shell completions
ferrflow completions bash >> ~/.bash_completion
ferrflow completions zsh > ~/.zfunc/_ferrflow
ferrflow completions fish > ~/.config/fish/completions/ferrflow.fishPre-generated completion scripts are also available as ferrflow-completions.tar.gz in each GitHub release.
FerrFlow looks for a config file at the root of your repository, in this order:
ferrflow.jsonferrflow.json5ferrflow.toml.ferrflow(dotfile, JSON format)
If multiple config files exist, FerrFlow exits with an error listing the conflicting files. Use --config <path> (or FERRFLOW_CONFIG env var) to specify which one to use. If no config file is found, FerrFlow auto-detects common version files in the current directory.
Run ferrflow init to scaffold a config file interactively. Use --format to skip the format prompt:
ferrflow init # asks which format (default: json)
ferrflow init --format json5
ferrflow init --format toml
ferrflow init --format dotfile # generates .ferrflowAdd $schema to get autocompletion and validation in VS Code, WebStorm, and any JSON-aware editor:
{
"$schema": "https://ferrflow.com/schema/ferrflow.json"
}{
"$schema": "https://ferrflow.com/schema/ferrflow.json",
"workspace": {
"remote": "origin",
"branch": "main"
},
"package": [
{
"name": "my-app",
"path": ".",
"changelog": "CHANGELOG.md",
"versioned_files": [
{ "path": "package.json", "format": "json" }
]
}
]
}{
workspace: {
remote: "origin",
branch: "main",
},
package: [
{
name: "my-app",
path: ".",
changelog: "CHANGELOG.md",
versioned_files: [
{ path: "package.json", format: "json" },
],
},
],
}[workspace]
remote = "origin"
branch = "main"
[[package]]
name = "my-app"
path = "."
changelog = "CHANGELOG.md"
[[package.versioned_files]]
path = "Cargo.toml"
format = "toml"JSON
{
"package": [
{
"name": "api",
"path": "services/api",
"changelog": "services/api/CHANGELOG.md",
"shared_paths": ["services/shared/"],
"versioned_files": [
{ "path": "services/api/Cargo.toml", "format": "toml" }
]
},
{
"name": "frontend",
"path": "frontend",
"changelog": "frontend/CHANGELOG.md",
"versioned_files": [
{ "path": "frontend/package.json", "format": "json" }
]
}
]
}TOML
[[package]]
name = "api"
path = "services/api"
changelog = "services/api/CHANGELOG.md"
shared_paths = ["services/shared/"]
[[package.versioned_files]]
path = "services/api/Cargo.toml"
format = "toml"
[[package]]
name = "frontend"
path = "frontend"
changelog = "frontend/CHANGELOG.md"
[[package.versioned_files]]
path = "frontend/package.json"
format = "json"Each package can use its own versioning strategy. Set a default at the workspace level and override per package:
[workspace]
versioning = "semver" # default for all packages
[[package]]
name = "api"
path = "packages/api"
# inherits semver from workspace
[[package]]
name = "site"
path = "packages/site"
versioning = "calver" # override: date-based| Strategy | Format | Example | Description |
|---|---|---|---|
semver |
MAJOR.MINOR.PATCH |
1.4.2 |
Default, driven by conventional commits |
calver |
YYYY.M.D |
2025.3.28 |
Date-based, ignores commit types |
calver-short |
YY.M.D |
25.3.28 |
Compact date-based |
calver-seq |
YYYY.M.SEQ |
2025.3.3 |
Date + daily sequence counter |
sequential |
N |
42 |
Simple incrementing build number |
zerover |
0.MINOR.PATCH |
0.15.2 |
Permanently unstable, never hits 1.0 |
By default, FerrFlow tags single-repo releases as v1.2.3 and monorepo releases as api@v1.2.3. Customize with tag_template at the workspace or package level using {name} and {version} placeholders.
[workspace]
tag_template = "v{version}" # all packages: v1.2.3
[[package]]
name = "api"
path = "packages/api"
tag_template = "{name}/v{version}" # override: api/v1.2.3| Layout | Default template | Example tag |
|---|---|---|
| Single repo | v{version} |
v1.2.3 |
| Monorepo | {name}@v{version} |
api@v1.2.3 |
| Custom | release-{version} |
release-1.2.3 |
Publish pre-release versions (alpha, beta, rc, dev) using the --channel flag or branch-based configuration. Pre-release versions follow the format MAJOR.MINOR.PATCH-CHANNEL.IDENTIFIER.
ferrflow release --channel beta # 2.0.0-beta.1
ferrflow check --channel rc # preview pre-release versionMap branches to channels automatically:
{
"workspace": {
"branches": [
{ "name": "main", "channel": false },
{ "name": "develop", "channel": "dev", "prereleaseIdentifier": "timestamp" },
{ "name": "release/*", "channel": "rc" }
]
}
}Branch names support glob patterns. The first match wins. Wildcards match across
/ separators, so * matches branches like fix/global and feature/* matches
feature/auth/oauth.
| Strategy | Example | Description |
|---|---|---|
increment |
-beta.3 |
Auto-incrementing counter (default) |
timestamp |
-dev.20250402T1430 |
UTC timestamp |
short-hash |
-dev.a1b2c3d |
Git short hash |
timestamp-hash |
-dev.20250402T1430-a1b2c3d |
Timestamp + hash |
- Floating tags (e.g.
v1,v1.2) are never moved by pre-release versions - GitHub Releases are marked as pre-release
- Stable releases include all commits since the last stable tag (skipping pre-release tags)
- Hook environment includes
FERRFLOW_CHANNELandFERRFLOW_IS_PRERELEASE
Controls how FerrFlow commits version bumps and changelog updates after a release.
[workspace]
release_commit_mode = "commit" # default| Mode | Description |
|---|---|
commit |
Push a release commit directly to the branch |
pr |
Create a pull request with the release changes |
none |
Skip committing entirely (useful when another tool handles it) |
When using pr mode, auto_merge_releases controls whether the PR is automatically merged:
[workspace]
release_commit_mode = "pr"
auto_merge_releases = true # defaultIn monorepo mode, controls whether all package bumps go into a single commit or one commit per package:
[workspace]
release_commit_scope = "grouped" # default| Scope | Description |
|---|---|
grouped |
Single commit for all packages (e.g. chore(release): api v1.0.0, site v2.1.0) |
per-package |
One commit per package (e.g. chore(release): api v1.0.0, then chore(release): site v2.1.0) |
Per-package commits make it easier to revert a single package bump without affecting others. This works with both commit and pr release modes.
By default, release commits in commit mode include [skip ci] in the message to avoid triggering a CI loop. Override with skip_ci:
[workspace]
skip_ci = false # force CI to run on release commitsIn pr mode, skip_ci defaults to false since the PR merge triggers CI naturally.
Move abbreviated tags (e.g. v1, v1.2) to always point at the latest matching release:
[workspace]
floating_tags = ["major"] # creates/moves v1 when releasing v1.2.3| Level | Tag | Points to |
|---|---|---|
major |
v1 |
Latest v1.x.x |
minor |
v1.2 |
Latest v1.2.x |
Floating tags are never moved by pre-release versions. Override per package:
[[package]]
name = "api"
path = "packages/api"
floating_tags = ["major", "minor"]After a rebase + force-push, existing tags may point to commits that no longer exist on the branch. orphaned_tag_strategy controls how FerrFlow handles this:
[workspace]
orphaned_tag_strategy = "warn" # default| Strategy | Description |
|---|---|
warn |
Log a warning and skip the orphaned tag |
treeHash |
Attempt recovery by matching the commit's tree hash |
message |
Attempt recovery by matching the commit message |
In monorepos, a package can miss a release if its files changed but FerrFlow wasn't run. Enable recover_missed_releases to compare files against the last tag instead of just the last commit:
[workspace]
recover_missed_releases = true # default: falseIn a monorepo, use depends_on to automatically patch-bump a package when one of its dependencies is released:
{
"package": [
{ "name": "core", "path": "packages/core" },
{
"name": "cli",
"path": "packages/cli",
"depends_on": ["core"]
}
]
}When core is bumped, cli gets a patch bump even if it had no direct commits.
Run shell commands at lifecycle points during a release. Hooks can be set at the workspace level (applies to all packages) or per package:
[workspace.hooks]
pre_bump = "echo 'about to bump'"
post_bump = "cargo check"
pre_commit = "npm run build"
pre_publish = "npm pack --dry-run"
post_publish = "notify-slack.sh"
on_failure = "abort" # or "continue"| Hook | When |
|---|---|
pre_bump |
After bump calculation, before writing version files |
post_bump |
After writing version files, before changelog generation |
pre_commit |
After changelog generation, before git commit |
pre_publish |
After commit and tag, before push |
post_publish |
After push and release creation |
If a hook exits non-zero and on_failure is abort (default), the release is cancelled. Set on_failure to continue to ignore hook failures.
Hook commands receive environment variables: FERRFLOW_PACKAGE, FERRFLOW_VERSION, FERRFLOW_PREV_VERSION, FERRFLOW_CHANNEL, FERRFLOW_IS_PRERELEASE.
FerrFlow follows the Conventional Commits spec.
| Prefix | Bump |
|---|---|
fix:, perf:, refactor: |
patch |
feat: |
minor |
feat!:, BREAKING CHANGE |
major |
chore:, docs:, ci: |
none |
GitLab CI
release:
image: ghcr.io/ferrflow/ferrflow:latest
script:
- ferrflow release
rules:
- if: '$CI_COMMIT_BRANCH == "main"'GitHub Actions
- name: Release
run: ferrflow release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}MIT