Configuration
FerrFlow supports six config file formats, searched in this order:
ferrflow.jsonferrflow.json5ferrflow.tomlferrflow.ts(requirestsx)ferrflow.js(requiresnode).ferrflow(JSON)
If no config file is found, FerrFlow auto-detects common version files in the current directory.
Config formats
Section titled “Config formats”export default { workspace: { tagTemplate: "v{version}", }, package: [ { name: "my-app", path: ".", changelog: "CHANGELOG.md", versionedFiles: [ { path: "Cargo.toml", format: "toml" }, ], }, ],};{ "$schema": "https://ferrflow.com/schema/ferrflow.json", "workspace": { "tagTemplate": "v{version}" }, "package": [ { "name": "my-app", "path": ".", "changelog": "CHANGELOG.md", "versionedFiles": [ { "path": "Cargo.toml", "format": "toml" } ] } ]}[workspace]tag_template = "v{version}"
[[package]]name = "my-app"path = "."changelog = "CHANGELOG.md"
[[package.versioned_files]]path = "Cargo.toml"format = "toml"{ $schema: "https://ferrflow.com/schema/ferrflow.json", workspace: { tagTemplate: "v{version}", }, package: [ { name: "my-app", path: ".", changelog: "CHANGELOG.md", versionedFiles: [ { path: "Cargo.toml", format: "toml" }, ], }, ],}workspace: tagTemplate: "v{version}"
package: - name: my-app path: "." changelog: CHANGELOG.md versionedFiles: - path: Cargo.toml format: tomlTypeScript and JavaScript configs
Section titled “TypeScript and JavaScript configs”TypeScript (.ts) and JavaScript (.js) config files use a default ESM export. The export can be a plain object or an async function.
The main advantage of TS/JS configs is function hooks. Instead of shell command strings, you can write hooks as native functions with full access to the hook context:
export default { workspace: { tagTemplate: "v{version}", hooks: { postPublish: async (ctx) => { await fetch("https://hooks.slack.com/services/...", { method: "POST", body: JSON.stringify({ text: `Released ${ctx.package}@${ctx.newVersion}`, }), }); }, }, }, package: [ { name: "my-app", path: ".", versionedFiles: [{ path: "package.json", format: "json" }], }, ],};Hook context object
Section titled “Hook context object”Function hooks receive a context object with these fields:
| Field | Type | Description |
|---|---|---|
package | string | Package name |
oldVersion | string | Version before bump (empty on first release) |
newVersion | string | Version after bump |
bumpType | string | major, minor, patch, or none |
tag | string | Full git tag name |
dryRun | boolean | Whether --dry-run is set |
packagePath | string | Absolute path to package root |
channel | string or null | Pre-release channel name |
isPrerelease | boolean | Whether this is a pre-release |
Shell string hooks and function hooks can be mixed in the same config. Shell strings still work in TS/JS configs.
workspace
Section titled “workspace”Global settings that apply to all packages.
| Field | Type | Default | Description |
|---|---|---|---|
remote | string | "origin" | Git remote to push to |
branch | string | auto-detected | Branch to push to (detected from remote HEAD) |
tagTemplate | string | "v{version}" or "{name}@v{version}" | Tag naming pattern. Uses {version} and {name} placeholders. Defaults to v{version} for single-package repos and {name}@v{version} for monorepos. |
versioning | string | "semver" | Default versioning strategy for all packages |
releaseCommitMode | string | "commit" | How to handle the release commit: "commit", "pr", or "none" |
skipCi | boolean | depends on mode | Add [skip ci] to release commits. Defaults to true when mode is "commit", false otherwise. |
autoMergeReleases | boolean | true | Enable auto-merge on release PRs (only applies when mode is "pr") |
recoverMissedReleases | boolean | false | When enabled, if FerrFlow finds unreleased commits spanning multiple version bumps, it creates all intermediate releases instead of jumping to the latest version |
floatingTags | array | [] | Floating tag levels to create on each release: "major", "minor". For example, ["major"] creates a v1 tag that always points to the latest v1.x.y release. |
orphanedTagStrategy | string | "warn" | How to handle tags pointing to orphaned commits after rebase + force-push: "warn", "treeHash", or "message" |
telemetry | boolean | true | Send anonymous usage telemetry (details) |
Tag template
Section titled “Tag template”The tagTemplate field controls how git tags are named. Available placeholders:
| Placeholder | Description |
|---|---|
{version} | The version number (e.g. 1.2.3) |
{name} | The package name |
{ "workspace": { "tagTemplate": "v{version}" }}[workspace]tag_template = "v{version}"{ workspace: { tagTemplate: "v{version}", },}workspace: tagTemplate: "v{version}"For monorepos, use {name} to namespace tags per package:
{ "workspace": { "tagTemplate": "{name}@v{version}" }}[workspace]tag_template = "{name}@v{version}"{ workspace: { tagTemplate: "{name}@v{version}", },}workspace: tagTemplate: "{name}@v{version}"Floating tags
Section titled “Floating tags”Floating tags are version aliases that always point to the latest release matching a given level. This is useful for GitHub Actions or Docker images where users reference v1 instead of v1.2.3.
{ "workspace": { "floatingTags": ["major"] }}[workspace]floating_tags = ["major"]{ workspace: { floatingTags: ["major"], },}workspace: floatingTags: - majorWhen releasing v1.2.3, FerrFlow creates or moves a v1 tag pointing to the same commit. With ["major", "minor"], both v1 and v1.2 tags are maintained.
If a floating tag would move backward (e.g. releasing a v1.1.0 hotfix when v1.2.0 already exists), FerrFlow blocks the release. Use --force to override this check.
Orphaned tag strategy
Section titled “Orphaned tag strategy”When a branch is rebased and force-pushed, existing tags may point to commits that are no longer part of the branch history. By default, FerrFlow warns about these orphaned tags and skips them. You can configure automatic recovery instead.
| Strategy | Behavior |
|---|---|
"warn" | Log a warning identifying the orphaned tag and skip it (default) |
"treeHash" | Attempt to find a commit on the current branch with the same file tree as the orphaned tag’s commit |
"message" | Attempt to find a commit on the current branch with the same commit message |
{ "workspace": { "orphanedTagStrategy": "treeHash" }}[workspace]orphaned_tag_strategy = "treeHash"{ workspace: { orphanedTagStrategy: "treeHash", },}workspace: orphanedTagStrategy: treeHash"treeHash" is the safest recovery option — it matches commits that have identical file contents, which is typical after a rebase that doesn’t modify files. Use "message" when rebases also change the tree (e.g. squashing commits) but preserve the original message.
If recovery fails (no matching commit found within the last 1000 commits), FerrFlow falls back to warning and skipping the tag. In that case, re-tag manually:
git tag -f api@v1.2.0 <correct-commit>Release commit mode
Section titled “Release commit mode”Controls how FerrFlow handles the commit that updates version files and changelogs.
| Mode | Behavior |
|---|---|
"commit" | Commits directly to the current branch and pushes (default) |
"pr" | Creates a release/ branch and opens a pull request |
"none" | Only creates tags and releases, does not commit file changes |
{ "workspace": { "releaseCommitMode": "pr", "autoMergeReleases": true }}[workspace]release_commit_mode = "pr"auto_merge_releases = true{ workspace: { releaseCommitMode: "pr", autoMergeReleases: true, },}workspace: releaseCommitMode: pr autoMergeReleases: trueVersioning strategies
Section titled “Versioning strategies”FerrFlow supports multiple versioning strategies, configurable at workspace or package level.
| Strategy | Format | Example progression |
|---|---|---|
semver | MAJOR.MINOR.PATCH | 1.2.3 → 1.3.0 → 2.0.0 |
calver | YYYY.MM.PATCH | 2026.03.0 → 2026.03.1 → 2026.04.0 |
calver-short | YY.MM.PATCH | 26.03.0 → 26.03.1 |
calver-seq | YYYY.MM.SEQ | 2026.03.1 → 2026.03.2 |
sequential | N | 1 → 2 → 3 |
zerover | 0.MINOR.PATCH | 0.1.0 → 0.2.0 (never reaches 1.0) |
{ "workspace": { "versioning": "calver" }}[workspace]versioning = "calver"{ workspace: { versioning: "calver", },}workspace: versioning: calverpackage
Section titled “package”Defines a package to version. You can have one or many.
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Package identifier, used in git tag prefix |
path | yes | — | Relative path to the package directory |
changelog | no | {path}/CHANGELOG.md | Path to the changelog file |
sharedPaths | no | [] | Paths that trigger this package when changed |
dependsOn | no | [] | Package names this package depends on. When a dependency is bumped, this package gets a patch bump automatically. |
versioning | no | inherited from workspace | Override versioning strategy for this package |
tagTemplate | no | inherited from workspace | Override tag template for this package |
floatingTags | no | inherited from workspace | Override floating tags for this package |
versionedFiles
Section titled “versionedFiles”Files where the version number should be updated.
{ "package": [ { "name": "my-app", "path": ".", "versionedFiles": [ { "path": "Cargo.toml", "format": "toml" }, { "path": "npm/package.json", "format": "json" } ] } ]}[[package]]name = "my-app"path = "."
[[package.versioned_files]]path = "Cargo.toml"format = "toml"
[[package.versioned_files]]path = "npm/package.json"format = "json"{ package: [ { name: "my-app", path: ".", versionedFiles: [ { path: "Cargo.toml", format: "toml" }, { path: "npm/package.json", format: "json" }, ], }, ],}package: - name: my-app path: "." versionedFiles: - path: Cargo.toml format: toml - path: npm/package.json format: jsonformat | File | Field updated |
|---|---|---|
toml | Cargo.toml, pyproject.toml | [package].version or [project].version |
json | package.json | version |
xml | pom.xml | First <version> element |
gradle | build.gradle, build.gradle.kts | version = "..." |
helm | Chart.yaml | version and appVersion (when present) |
gomod | go.mod | No file update — version comes from git tags only |
txt | VERSION, VERSION.txt | Entire file content replaced |
Run shell commands at key points in the release lifecycle. Hooks can be defined at workspace level (defaults for all packages) or per package (overrides workspace hooks for that package).
Lifecycle
Section titled “Lifecycle”calculate bump ↓pre_bump ← validate state, check prerequisites ↓write version files ↓post_bump ← modify additional files based on new version ↓generate changelog ↓pre_commit ← review staged changes, run linters ↓git commit + tag ↓pre_publish ← run tests against tagged commit, build artifacts ↓git push + create release ↓post_publish ← push Docker images, notify Slack, publish packagesConfiguration
Section titled “Configuration”{ "workspace": { "hooks": { "preBump": "cargo test", "postBump": "node scripts/sync-deps.js", "preCommit": "cargo fmt --check", "prePublish": "cargo build --release", "postPublish": "make docker-push && ./scripts/notify.sh", "onFailure": "abort" } }}[hooks]pre_bump = "cargo test"post_bump = "node scripts/sync-deps.js"pre_commit = "cargo fmt --check"pre_publish = "cargo build --release"post_publish = "make docker-push && ./scripts/notify.sh"on_failure = "abort"{ workspace: { hooks: { preBump: "cargo test", postBump: "node scripts/sync-deps.js", preCommit: "cargo fmt --check", prePublish: "cargo build --release", postPublish: "make docker-push && ./scripts/notify.sh", onFailure: "abort", }, },}workspace: hooks: preBump: cargo test postBump: node scripts/sync-deps.js preCommit: cargo fmt --check prePublish: cargo build --release postPublish: "make docker-push && ./scripts/notify.sh" onFailure: abort| Field | Type | Default | Description |
|---|---|---|---|
preBump | string | — | Run after bump calculation, before writing version files |
postBump | string | — | Run after version files are written |
preCommit | string | — | Run after changelog, before git commit |
prePublish | string | — | Run after commit+tag, before push |
postPublish | string | — | Run after push and release creation |
onFailure | string | "abort" | "abort" cancels the release on failure, "continue" prints a warning |
Environment variables
Section titled “Environment variables”Every hook receives these variables:
| Variable | Description | Example |
|---|---|---|
FERRFLOW_PACKAGE | Package name | api |
FERRFLOW_OLD_VERSION | Version before bump (empty on first release) | 1.2.3 |
FERRFLOW_NEW_VERSION | Version after bump | 1.3.0 |
FERRFLOW_BUMP_TYPE | major, minor, patch, or none | minor |
FERRFLOW_TAG | Full git tag name | api@v1.3.0 |
FERRFLOW_DRY_RUN | true if --dry-run is set | false |
FERRFLOW_PACKAGE_PATH | Absolute path to package root | /home/user/repo/packages/api |
Per-package hooks
Section titled “Per-package hooks”Package-level hooks replace workspace-level hooks for that package (they are not merged).
{ "workspace": { "hooks": { "preBump": "echo releasing $FERRFLOW_PACKAGE", "postPublish": "make notify" } }, "package": [ { "name": "api", "path": "packages/api", "hooks": { "preBump": "cargo test" } } ]}[hooks]pre_bump = "echo releasing $FERRFLOW_PACKAGE"post_publish = "make notify"
[[package]]name = "api"path = "packages/api"
[package.hooks]pre_bump = "cargo test"{ workspace: { hooks: { preBump: "echo releasing $FERRFLOW_PACKAGE", postPublish: "make notify", }, }, package: [ { name: "api", path: "packages/api", hooks: { preBump: "cargo test", }, }, ],}workspace: hooks: preBump: "echo releasing $FERRFLOW_PACKAGE" postPublish: make notify
package: - name: api path: packages/api hooks: preBump: cargo testIn this example, the api package runs cargo test for preBump (overriding the workspace echo) but inherits the workspace postPublish hook.
Behavior
Section titled “Behavior”--dry-run: hooks are printed but not executed.--verbose: hook stdout/stderr is streamed live. Otherwise output is only shown on failure.- Files modified by
postBumporpreCommithooks are automatically included in the release commit.
Complete examples
Section titled “Complete examples”Single repo
Section titled “Single repo”{ "$schema": "https://ferrflow.com/schema/ferrflow.json", "workspace": { "tagTemplate": "v{version}" }, "package": [ { "name": "ferrflow", "path": ".", "changelog": "CHANGELOG.md", "versionedFiles": [ { "path": "Cargo.toml", "format": "toml" }, { "path": "npm/package.json", "format": "json" } ] } ]}[workspace]tag_template = "v{version}"
[[package]]name = "ferrflow"path = "."changelog = "CHANGELOG.md"
[[package.versioned_files]]path = "Cargo.toml"format = "toml"
[[package.versioned_files]]path = "npm/package.json"format = "json"{ $schema: "https://ferrflow.com/schema/ferrflow.json", workspace: { tagTemplate: "v{version}", }, package: [ { name: "ferrflow", path: ".", changelog: "CHANGELOG.md", versionedFiles: [ { path: "Cargo.toml", format: "toml" }, { path: "npm/package.json", format: "json" }, ], }, ],}workspace: tagTemplate: "v{version}"
package: - name: ferrflow path: "." changelog: CHANGELOG.md versionedFiles: - path: Cargo.toml format: toml - path: npm/package.json format: jsonMonorepo
Section titled “Monorepo”{ "$schema": "https://ferrflow.com/schema/ferrflow.json", "workspace": { "tagTemplate": "{name}@v{version}" }, "package": [ { "name": "api", "path": "packages/api", "changelog": "packages/api/CHANGELOG.md", "sharedPaths": ["packages/shared/"], "versionedFiles": [ { "path": "packages/api/Cargo.toml", "format": "toml" } ] }, { "name": "site", "path": "packages/site", "changelog": "packages/site/CHANGELOG.md", "sharedPaths": ["packages/shared/"], "versionedFiles": [ { "path": "packages/site/package.json", "format": "json" } ] } ]}[workspace]tag_template = "{name}@v{version}"
[[package]]name = "api"path = "packages/api"changelog = "packages/api/CHANGELOG.md"shared_paths = ["packages/shared/"]
[[package.versioned_files]]path = "packages/api/Cargo.toml"format = "toml"
[[package]]name = "site"path = "packages/site"changelog = "packages/site/CHANGELOG.md"shared_paths = ["packages/shared/"]
[[package.versioned_files]]path = "packages/site/package.json"format = "json"{ $schema: "https://ferrflow.com/schema/ferrflow.json", workspace: { tagTemplate: "{name}@v{version}", }, package: [ { name: "api", path: "packages/api", changelog: "packages/api/CHANGELOG.md", sharedPaths: ["packages/shared/"], versionedFiles: [ { path: "packages/api/Cargo.toml", format: "toml" }, ], }, { name: "site", path: "packages/site", changelog: "packages/site/CHANGELOG.md", sharedPaths: ["packages/shared/"], versionedFiles: [ { path: "packages/site/package.json", format: "json" }, ], }, ],}workspace: tagTemplate: "{name}@v{version}"
package: - name: api path: packages/api changelog: packages/api/CHANGELOG.md sharedPaths: - packages/shared/ versionedFiles: - path: packages/api/Cargo.toml format: toml
- name: site path: packages/site changelog: packages/site/CHANGELOG.md sharedPaths: - packages/shared/ versionedFiles: - path: packages/site/package.json format: json