FerrFlow treats a repository as a monorepo when the config defines more than one package. Each package is versioned independently based on its own git history.

Package isolation

FerrFlow uses path prefixes to determine which commits belong to which package. Only commits that touch files under path (or sharedPaths) trigger a release for that package.

JSON

{
  "package": [
    {
      "name": "api",
      "path": "packages/api"
    },
    {
      "name": "site",
      "path": "packages/site"
    }
  ]
}

TOML

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

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

JSON5

{
  package: [
    {
      name: "api",
      path: "packages/api",
    },
    {
      name: "site",
      path: "packages/site",
    },
  ],
}

YAML

package:
  - name: api
    path: packages/api
  - name: site
    path: packages/site

Shared dependencies

If you have code shared between packages (e.g., a packages/shared/ library), declare it as a sharedPaths entry. A change to any shared path triggers a release for every package that lists it:

JSON

{
  "package": [
    {
      "name": "api",
      "path": "packages/api",
      "sharedPaths": ["packages/shared/"]
    },
    {
      "name": "site",
      "path": "packages/site",
      "sharedPaths": ["packages/shared/"]
    }
  ]
}

TOML

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

[[package]] name = "site" path = "packages/site" shared_paths = ["packages/shared/"]

JSON5

{
  package: [
    {
      name: "api",
      path: "packages/api",
      sharedPaths: ["packages/shared/"],
    },
    {
      name: "site",
      path: "packages/site",
      sharedPaths: ["packages/shared/"],
    },
  ],
}

YAML

package:
  - name: api
    path: packages/api
    sharedPaths:
      - packages/shared/
  - name: site
    path: packages/site
    sharedPaths:
      - packages/shared/

Package dependencies

Use dependsOn to declare that a package depends on another. When a dependency is released, the dependent package is bumped too — even if none of its own files changed — with the same bump type by default. This cascades transitively: if app depends on cli and cli depends on core, bumping core bumps both cli and app.

JSON

{
  "package": [
    {
      "name": "core",
      "path": "packages/core"
    },
    {
      "name": "cli",
      "path": "packages/cli",
      "dependsOn": ["core"]
    },
    {
      "name": "app",
      "path": "packages/app",
      "dependsOn": ["cli"]
    }
  ]
}

TOML

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

[[package]] name = "cli" path = "packages/cli" depends_on = ["core"]

[[package]] name = "app" path = "packages/app" depends_on = ["cli"]

JSON5

{
  package: [
    {
      name: "core",
      path: "packages/core",
    },
    {
      name: "cli",
      path: "packages/cli",
      dependsOn: ["core"],
    },
    {
      name: "app",
      path: "packages/app",
      dependsOn: ["cli"],
    },
  ],
}

YAML

package:
  - name: core
    path: packages/core
  - name: cli
    path: packages/cli
    dependsOn:
      - core
  - name: app
    path: packages/app
    dependsOn:
      - cli

Propagation policy

Write an entry as an object to choose how the upstream bump translates:

{
  "name": "cli",
  "path": "packages/cli",
  "dependsOn": [{ "name": "core", "propagate": "patch" }]
}
Policy A major in the dependency becomes A minor becomes A patch becomes
same (default) major minor patch
major-on-major major patch patch
patch patch patch patch
none nothing — the dependent is not bumped nothing nothing

A bare string is shorthand for same, so "dependsOn": ["core"] and { "name": "core", "propagate": "same" } are identical. When several dependencies move at once under different policies, the strongest resulting bump wins.

Updating dependents' manifests

Set workspace.updateDependents to true to rewrite the version constraint each dependent declares for the bumped package, staged in the same release commit:

● core  1.0.01.1.0  (minor)
● cli   2.3.02.4.0  (minor, dependency: core)
  ↳ core → 1.1.0 in cli/package.json

cli/package.json goes from "core": "^1.0.0" to "core": "^1.1.0". Only json and toml manifests are rewritten, and only plain operator + version constraints — the operator is preserved. A workspace:*, file:/git: spec, 1.x or a multi-part range carries intent a version pin would destroy, so it is left for you to handle.

Dependency cycles

dependsOn must describe a directed acyclic graph. If two packages depend on each other — directly or transitively — there is no order in which to release them, so FerrFlow stops with error E8003 and names the loop:

cycle detected: api → web → api

The check runs before any version is written, so a cyclic configuration never produces a partial release. Break the loop by removing one of the dependsOn edges. Otherwise the graph is released dependencies-first: a package is always released after the packages it depends on.

Linked and fixed version groups

Sometimes packages must share a version number, not just cascade a bump. linked and fixed list groups of packages that move in lockstep:

[workspace]
linked = [["react", "react-dom"]]
fixed  = [["@scope/a", "@scope/b", "@scope/c"]]

When any member of a group has a releasable commit, every member is bumped to the same version — the highest version the group would reach. A feat on one member and a fix on another release the whole group on the minor. Package names stay distinct; only the version is shared, and members with no commits of their own are pulled into the release at the shared version.

  • linked — packages share a version line when they are released together (e.g. react and react-dom both go 1.2.3 → 1.2.4).
  • fixed — packages are locked to an identical version forever. It behaves like linked, and ferrflow validate additionally warns when a fixed group's versions have already drifted apart, so you catch a manual edit before the next release realigns them.

Each group must list at least two packages, and a package may appear in only one linked or fixed group. Naming a package that isn't defined in package[], or listing one in two groups, stops the release with a clear error before anything is written — the same pre-flight guarantee as dependency cycles.

linked/fixed and dependsOn compose: a package that depends on a grouped package still receives its cascade bump after the group is aligned.

Git tag format

By default, monorepo tags use the {name}@v{version} format:

api@v1.2.0
site@v0.4.1

Configure this with the tagTemplate field:

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

For a single-package repo, the default is v{version} (no name prefix).

FerrFlow looks for the most recent tag matching the template to determine what commits are new.

Independent cadences

Packages release independently. In a single ferrflow release run:

  • api may bump from 1.2.01.3.0 (new feat: commit)
  • site may bump from 0.4.00.4.1 (only fix: commits)
  • shared may not release at all (only chore: commits)

Per-package overrides

Each package can override the workspace-level versioning strategy and tagTemplate:

JSON

{
  "workspace": {
    "versioning": "semver",
    "tagTemplate": "{name}@v{version}"
  },
  "package": [
    {
      "name": "api",
      "path": "packages/api",
      "versioning": "calver"
    },
    {
      "name": "site",
      "path": "packages/site",
      "tagTemplate": "site-v{version}"
    }
  ]
}

TOML

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

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

[[package]] name = "site" path = "packages/site" tag_template = "site-v{version}"

JSON5

{
  workspace: {
    versioning: "semver",
    tagTemplate: "{name}@v{version}",
  },
  package: [
    {
      name: "api",
      path: "packages/api",
      versioning: "calver",
    },
    {
      name: "site",
      path: "packages/site",
      tagTemplate: "site-v{version}",
    },
  ],
}

YAML

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

package:

  • name: api path: packages/api versioning: calver
  • name: site path: packages/site tagTemplate: "site-v{version}"