Skip to content
FerrFlow

Configuration / Configuration

Configuration

FerrFlow supports six config file formats, searched in this order:

  1. ferrflow.json
  2. ferrflow.json5
  3. ferrflow.toml
  4. ferrflow.ts (requires tsx)
  5. ferrflow.js (requires node)
  6. .ferrflow (JSON)

If no config file is found, FerrFlow auto-detects common version files in the current directory.

ferrflow.ts
export default {
workspace: {
tagTemplate: "v{version}",
},
package: [
{
name: "my-app",
path: ".",
changelog: "CHANGELOG.md",
versionedFiles: [
{ path: "Cargo.toml", format: "toml" },
],
},
],
};

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:

ferrflow.ts
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" }],
},
],
};

Function hooks receive a context object with these fields:

FieldTypeDescription
packagestringPackage name
oldVersionstringVersion before bump (empty on first release)
newVersionstringVersion after bump
bumpTypestringmajor, minor, patch, or none
tagstringFull git tag name
dryRunbooleanWhether --dry-run is set
packagePathstringAbsolute path to package root
channelstring or nullPre-release channel name
isPrereleasebooleanWhether 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.

Global settings that apply to all packages.

FieldTypeDefaultDescription
remotestring"origin"Git remote to push to
branchstringauto-detectedBranch to push to (detected from remote HEAD)
tagTemplatestring"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.
versioningstring"semver"Default versioning strategy for all packages
releaseCommitModestring"commit"How to handle the release commit: "commit", "pr", or "none"
skipCibooleandepends on modeAdd [skip ci] to release commits. Defaults to true when mode is "commit", false otherwise.
autoMergeReleasesbooleantrueEnable auto-merge on release PRs (only applies when mode is "pr")
recoverMissedReleasesbooleanfalseWhen enabled, if FerrFlow finds unreleased commits spanning multiple version bumps, it creates all intermediate releases instead of jumping to the latest version
floatingTagsarray[]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.
orphanedTagStrategystring"warn"How to handle tags pointing to orphaned commits after rebase + force-push: "warn", "treeHash", or "message"
telemetrybooleantrueSend anonymous usage telemetry (details)

The tagTemplate field controls how git tags are named. Available placeholders:

PlaceholderDescription
{version}The version number (e.g. 1.2.3)
{name}The package name
ferrflow.json
{
"workspace": {
"tagTemplate": "v{version}"
}
}

For monorepos, use {name} to namespace tags per package:

ferrflow.json
{
"workspace": {
"tagTemplate": "{name}@v{version}"
}
}

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.

ferrflow.json
{
"workspace": {
"floatingTags": ["major"]
}
}

When 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.

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.

StrategyBehavior
"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
ferrflow.json
{
"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:

Terminal window
git tag -f api@v1.2.0 <correct-commit>

Controls how FerrFlow handles the commit that updates version files and changelogs.

ModeBehavior
"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
ferrflow.json
{
"workspace": {
"releaseCommitMode": "pr",
"autoMergeReleases": true
}
}

FerrFlow supports multiple versioning strategies, configurable at workspace or package level.

StrategyFormatExample progression
semverMAJOR.MINOR.PATCH1.2.31.3.02.0.0
calverYYYY.MM.PATCH2026.03.02026.03.12026.04.0
calver-shortYY.MM.PATCH26.03.026.03.1
calver-seqYYYY.MM.SEQ2026.03.12026.03.2
sequentialN123
zerover0.MINOR.PATCH0.1.00.2.0 (never reaches 1.0)
ferrflow.json
{
"workspace": {
"versioning": "calver"
}
}

Defines a package to version. You can have one or many.

FieldRequiredDefaultDescription
nameyesPackage identifier, used in git tag prefix
pathyesRelative path to the package directory
changelogno{path}/CHANGELOG.mdPath to the changelog file
sharedPathsno[]Paths that trigger this package when changed
dependsOnno[]Package names this package depends on. When a dependency is bumped, this package gets a patch bump automatically.
versioningnoinherited from workspaceOverride versioning strategy for this package
tagTemplatenoinherited from workspaceOverride tag template for this package
floatingTagsnoinherited from workspaceOverride floating tags for this package

Files where the version number should be updated.

ferrflow.json
{
"package": [
{
"name": "my-app",
"path": ".",
"versionedFiles": [
{ "path": "Cargo.toml", "format": "toml" },
{ "path": "npm/package.json", "format": "json" }
]
}
]
}
formatFileField updated
tomlCargo.toml, pyproject.toml[package].version or [project].version
jsonpackage.jsonversion
xmlpom.xmlFirst <version> element
gradlebuild.gradle, build.gradle.ktsversion = "..."
helmChart.yamlversion and appVersion (when present)
gomodgo.modNo file update — version comes from git tags only
txtVERSION, VERSION.txtEntire 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).

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 packages
{
"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"
}
}
}
FieldTypeDefaultDescription
preBumpstringRun after bump calculation, before writing version files
postBumpstringRun after version files are written
preCommitstringRun after changelog, before git commit
prePublishstringRun after commit+tag, before push
postPublishstringRun after push and release creation
onFailurestring"abort""abort" cancels the release on failure, "continue" prints a warning

Every hook receives these variables:

VariableDescriptionExample
FERRFLOW_PACKAGEPackage nameapi
FERRFLOW_OLD_VERSIONVersion before bump (empty on first release)1.2.3
FERRFLOW_NEW_VERSIONVersion after bump1.3.0
FERRFLOW_BUMP_TYPEmajor, minor, patch, or noneminor
FERRFLOW_TAGFull git tag nameapi@v1.3.0
FERRFLOW_DRY_RUNtrue if --dry-run is setfalse
FERRFLOW_PACKAGE_PATHAbsolute path to package root/home/user/repo/packages/api

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"
}
}
]
}

In this example, the api package runs cargo test for preBump (overriding the workspace echo) but inherits the workspace postPublish hook.

  • --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 postBump or preCommit hooks are automatically included in the release commit.
ferrflow.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" }
]
}
]
}
ferrflow.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" }
]
}
]
}