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.

Config formats

TypeScript

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

JSON

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

TOML

[workspace]
tag_template = "v{version}"

[[package]] name = "my-app" path = "." changelog = "CHANGELOG.md"

[[package.versioned_files]] path = "Cargo.toml" format = "toml"

JSON5

{
  $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" },
      ],
    },
  ],
}

YAML

workspace:
  tagTemplate: "v{version}"

package:

  • name: my-app path: "." changelog: CHANGELOG.md versionedFiles:
    • path: Cargo.toml format: toml

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

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

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

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

JSON

{
  "workspace": {
    "tagTemplate": "v{version}"
  }
}

TOML

[workspace]
tag_template = "v{version}"

JSON5

{
  workspace: {
    tagTemplate: "v{version}",
  },
}

YAML

workspace:
  tagTemplate: "v{version}"

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

JSON

{
  "workspace": {
    "tagTemplate": "{name}@v{version}"
  }
}

TOML

[workspace]
tag_template = "{name}@v{version}"

JSON5

{
  workspace: {
    tagTemplate: "{name}@v{version}",
  },
}

YAML

workspace:
  tagTemplate: "{name}@v{version}"

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.

JSON

{
  "workspace": {
    "floatingTags": ["major"]
  }
}

TOML

[workspace]
floating_tags = ["major"]

JSON5

{
  workspace: {
    floatingTags: ["major"],
  },
}

YAML

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.

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

JSON

{
  "workspace": {
    "orphanedTagStrategy": "treeHash"
  }
}

TOML

[workspace]
orphaned_tag_strategy = "treeHash"

JSON5

{
  workspace: {
    orphanedTagStrategy: "treeHash",
  },
}

YAML

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

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

JSON

{
  "workspace": {
    "releaseCommitMode": "pr",
    "autoMergeReleases": true
  }
}

TOML

[workspace]
release_commit_mode = "pr"
auto_merge_releases = true

JSON5

{
  workspace: {
    releaseCommitMode: "pr",
    autoMergeReleases: true,
  },
}

YAML

workspace:
  releaseCommitMode: pr
  autoMergeReleases: true

Versioning strategies

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

Strategy Format Example progression
semver MAJOR.MINOR.PATCH 1.2.31.3.02.0.0
calver YYYY.MM.PATCH 2026.03.02026.03.12026.04.0
calver-short YY.MM.PATCH 26.03.026.03.1
calver-seq YYYY.MM.SEQ 2026.03.12026.03.2
sequential N 123
zerover 0.MINOR.PATCH 0.1.00.2.0 (never reaches 1.0)

JSON

{
  "workspace": {
    "versioning": "calver"
  }
}

TOML

[workspace]
versioning = "calver"

JSON5

{
  workspace: {
    versioning: "calver",
  },
}

YAML

workspace:
  versioning: calver

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

Files where the version number should be updated.

JSON

{
  "package": [
    {
      "name": "my-app",
      "path": ".",
      "versionedFiles": [
        { "path": "Cargo.toml", "format": "toml" },
        { "path": "npm/package.json", "format": "json" }
      ]
    }
  ]
}

TOML

[[package]]
name = "my-app"
path = "."

[[package.versioned_files]] path = "Cargo.toml" format = "toml"

[[package.versioned_files]] path = "npm/package.json" format = "json"

JSON5

{
  package: [
    {
      name: "my-app",
      path: ".",
      versionedFiles: [
        { path: "Cargo.toml", format: "toml" },
        { path: "npm/package.json", format: "json" },
      ],
    },
  ],
}

YAML

package:
  - name: my-app
    path: "."
    versionedFiles:
      - path: Cargo.toml
        format: toml
      - path: npm/package.json
        format: json
format File Field updated
toml Cargo.toml, pyproject.toml [package].version or [project].version
json package.json version
xml pom.xml First 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

hooks

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

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

Configuration

JSON

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

TOML

[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"

JSON5

{
  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",
    },
  },
}

YAML

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

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

Package-level hooks replace workspace-level hooks for that package (they are not merged).

JSON

{
  "workspace": {
    "hooks": {
      "preBump": "echo releasing $FERRFLOW_PACKAGE",
      "postPublish": "make notify"
    }
  },
  "package": [
    {
      "name": "api",
      "path": "packages/api",
      "hooks": {
        "preBump": "cargo test"
      }
    }
  ]
}

TOML

[hooks]
pre_bump     = "echo releasing $FERRFLOW_PACKAGE"
post_publish = "make notify"

[[package]] name = "api" path = "packages/api"

[package.hooks] pre_bump = "cargo test"

JSON5

{
  workspace: {
    hooks: {
      preBump: "echo releasing $FERRFLOW_PACKAGE",
      postPublish: "make notify",
    },
  },
  package: [
    {
      name: "api",
      path: "packages/api",
      hooks: {
        preBump: "cargo test",
      },
    },
  ],
}

YAML

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.

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

Complete examples

Single repo

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

TOML

[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"

JSON5

{
  $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" },
      ],
    },
  ],
}

YAML

workspace:
  tagTemplate: "v{version}"

package:

  • name: ferrflow path: "." changelog: CHANGELOG.md versionedFiles:
    • path: Cargo.toml format: toml
    • path: npm/package.json format: json

Monorepo

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

TOML

[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"

JSON5

{
  $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" },
      ],
    },
  ],
}

YAML

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