diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6ba3714..289b2b98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,6 +114,14 @@ release, publishing a binary to crates.io and eight npm packages for a typo, and withdrawn. A pull request that ships code and its documentation together takes the code's type, as usual. +A user-facing change also gets an entry in the public changelog at +[ferrflow.com/changelog](https://ferrflow.com/changelog/), in the same pull request: one file per change +in `docs/site/changelog/`, named `YYYY-MM-DD-slug.md`, with `title`, `summary`, `date`, +`product: ferrflow`, `type` (`new`, `fix`, `perf`, `breaking`, `deprecation` or `security`) and an +optional `prLink` and `docsLink` in the frontmatter, then a few sentences on what changed, why, and +how to use it. Internal refactors, test-only and CI-only changes, and dependency bumps that change +nothing for users do not get one. + Never edit `docs/site/docs-vN/` or `docs/site/docs-fr-vN/`. Those record what a past major actually documented, mistakes included, because someone pinned to that major reads them to understand the binary they are running. CI rejects a pull request that modifies one, and allows adding a new one. diff --git a/README.md b/README.md index 1f7389d3..d0652a35 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ updates your version files, writes the changelog, and cuts the tagged release. A [![Socket Badge](https://badge.socket.dev/cargo/package/ferrflow/latest)](https://socket.dev/cargo/package/ferrflow) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/FerrLabs/FerrFlow/badge)](https://scorecard.dev/viewer/?uri=github.com/FerrLabs/FerrFlow) -[Documentation](https://ferrflow.com/docs) | [Changelog](https://ferrlabs.com/changelog/) | [GitHub App](https://github.com/apps/ferrflow) +[Documentation](https://ferrflow.com/docs) | [Changelog](https://ferrflow.com/changelog/) | [GitHub App](https://github.com/apps/ferrflow) diff --git a/docs/site/changelog/2026-04-24-ferrflow-changelog-ordering.md b/docs/site/changelog/2026-04-24-ferrflow-changelog-ordering.md new file mode 100644 index 00000000..963360b2 --- /dev/null +++ b/docs/site/changelog/2026-04-24-ferrflow-changelog-ordering.md @@ -0,0 +1,11 @@ +--- +title: 'FerrFlow: changelog ordering fix' +summary: 'Changelog entries are now sorted by commit date instead of alphabetically inside each section.' +date: 2026-04-24T11:15:00Z +product: 'ferrflow' +type: 'fix' +--- + +Changelog entries were sorted alphabetically inside each section instead of by commit date. They are now grouped by date descending, which matches what every other release tool does and what humans actually expect. + +Affects everyone running v4.x. Re-run `ferrflow changelog` to regenerate the file with the corrected ordering. diff --git a/docs/site/changelog/2026-04-28-ferrflow-editorial-hero.md b/docs/site/changelog/2026-04-28-ferrflow-editorial-hero.md new file mode 100644 index 00000000..82e36a86 --- /dev/null +++ b/docs/site/changelog/2026-04-28-ferrflow-editorial-hero.md @@ -0,0 +1,11 @@ +--- +title: 'FerrFlow: editorial hero on the landing page' +summary: 'New display headline (Fraunces 900, italic accent), softer paper background, monospace eyebrows.' +date: 2026-04-28T20:59:00Z +product: 'ferrflow' +type: 'new' +--- + +The FerrFlow landing page now leads with an editorial hero: large display headline in Fraunces, italic accent line, monospace eyebrow markers. First step of the FerrLabs design system rollout for ferrflow.com. + +Same content, more confident voice. Nothing changed in the CLI itself. diff --git a/docs/site/changelog/2026-04-29-ferrflow-docs-claude-style.md b/docs/site/changelog/2026-04-29-ferrflow-docs-claude-style.md new file mode 100644 index 00000000..388b75c9 --- /dev/null +++ b/docs/site/changelog/2026-04-29-ferrflow-docs-claude-style.md @@ -0,0 +1,22 @@ +--- +title: 'FerrFlow: docs au style Claude' +summary: 'ferrflow.com/docs/ adopte le langage visuel de docs.claude.com: fond off-white, accent FerrFlow orange, code blocks soft-dark, callouts à barre accent, layout 3 colonnes, composants //.' +date: 2026-04-29T16:30:00Z +product: 'ferrflow' +type: 'new' +--- + +La doc Starlight par défaut faisait son boulot mais avait l'air générique. On adopte le langage visuel des docs Anthropic, en gardant l'orange FerrFlow comme accent unique au lieu de la clay Anthropic. + +Ce qui change visuellement : + +- **Fond off-white `#fafafa`** (light) / **near-black `#0a0a0a`** (dark) avec texte zinc. +- **Inter system stack** pour le corps, Fraunces gardé pour les titres. +- **Accent FerrFlow orange `#e8733a`** (light) / `#ef8a55` (dark) pour les liens, focus rings, sidebar active item. +- **Code blocks soft-dark `#1c1c1c`** même en light mode, 12px rounded, label langue top-left, bouton copy top-right au hover. +- **Callouts à carte teintée** avec barre accent 4px à gauche : ``, ``, ``, ``. +- **Composants Mintlify-style** `` et `` pour les grilles de valeur. +- **Breadcrumb small-caps** "Section › Page" au-dessus du H1. +- **Widget "Was this page helpful?"** + lien "Edit on GitHub" en bas de chaque page. + +Layout, sidebar versions/langues, ExpressiveCode, FR translations et snapshots `v0/v1/v2/v3` héritent automatiquement (le CSS est global). Pagefind search inchangée. La modale Cmd+K garde son style Starlight pour l'instant. diff --git a/docs/site/changelog/2026-04-29-ferrflow-editorial-body.md b/docs/site/changelog/2026-04-29-ferrflow-editorial-body.md new file mode 100644 index 00000000..45437c04 --- /dev/null +++ b/docs/site/changelog/2026-04-29-ferrflow-editorial-body.md @@ -0,0 +1,16 @@ +--- +title: 'FerrFlow: editorial features grid, install grid, dark CTA' +summary: 'The body of ferrflow.com now matches the new editorial design: six feature cards in a single bordered grid, four install methods side by side, dark contrast CTA block.' +date: 2026-04-29T11:23:00Z +product: 'ferrflow' +type: 'new' +--- + +The ferrflow.com landing page is now fully on the editorial design system, end to end. The hero refresh shipped a few days ago; this one finishes the body: + +- **Stats strip**: hairline row listing the 13 file formats FerrFlow reads natively (Cargo.toml, package.json, pyproject.toml, pom.xml, *.csproj, build.gradle, Chart.yaml, pubspec.yaml, mix.exs, *.gemspec, Package.swift, go.mod, composer.json). +- **Features section**: `№ 02` numbered, six feature cards in a single bordered container with hairline dividers. No more colored icon boxes. +- **Install section**: `№ 03`, four install methods side by side (Cargo / npm / Docker / GitHub Action), each with a mono `$` prompt in accent orange. +- **CTA block**: `№ 04`, dark contrast block with an 8px orange border-left, big Fraunces title with italic accent, two pill CTAs. + +Old terminal mock and 5-feature icon grid removed. diff --git a/docs/site/changelog/2026-04-29-ferrflow-v4-2-prerelease-channels.md b/docs/site/changelog/2026-04-29-ferrflow-v4-2-prerelease-channels.md new file mode 100644 index 00000000..aeac0ed2 --- /dev/null +++ b/docs/site/changelog/2026-04-29-ferrflow-v4-2-prerelease-channels.md @@ -0,0 +1,27 @@ +--- +title: 'FerrFlow v4.2: pre-release channels' +summary: 'Ship `alpha`, `beta`, and `rc` releases without touching your floating tags.' +date: 2026-04-29T09:00:00Z +product: 'ferrflow' +type: 'new' +docsLink: 'https://ferrflow.com/docs/guides/prerelease-channels' +--- + +You can now ship `alpha`, `beta`, and `rc` releases without touching your floating tags. Add `--prerelease=beta` to the CLI or set `prerelease.channel` in `.ferrflow` and FerrFlow will tag `v4.2.0-beta.1`, `v4.2.0-beta.2` and so on without bumping `latest`. + +```bash +ferrflow release --prerelease=beta +``` + +## Why we built it + +Three teams asked for it last quarter. Every one of them was using a hacky `--no-publish` workaround. This fixes the workaround. + +## What changes + +- New `prerelease.channel` config field (string, no default). +- New `--prerelease=` CLI flag (overrides config). +- Floating tags (`latest`, `next`) stay pinned to non-prerelease versions. +- Generated changelogs include a `Pre-releases` section under each major. + +No breaking changes. Existing repos get the feature for free on next bump to `v4.2.0`. diff --git a/docs/site/changelog/2026-05-05-ferrflow-do-not-track-and-first-run-notice.md b/docs/site/changelog/2026-05-05-ferrflow-do-not-track-and-first-run-notice.md new file mode 100644 index 00000000..ff828465 --- /dev/null +++ b/docs/site/changelog/2026-05-05-ferrflow-do-not-track-and-first-run-notice.md @@ -0,0 +1,22 @@ +--- +title: 'FerrFlow: DO_NOT_TRACK env var + first-run telemetry notice' +summary: 'Telemetry now honours DO_NOT_TRACK=1 alongside FERRFLOW_TELEMETRY=0, and the first invocation prints a one-line notice pointing to ferrlabs.com/telemetry.' +date: 2026-05-05T19:30:00Z +product: ferrflow +type: new +docsLink: https://ferrlabs.com/telemetry +--- + +Two small additions to make the FerrFlow CLI's anonymous telemetry friendlier and more discoverable. + +**`DO_NOT_TRACK=1`** now disables anonymous telemetry, exactly like `FERRFLOW_TELEMETRY=0`. Same precedence: `DO_NOT_TRACK` wins if set to `1`/`true`/`yes`/`on`. Adopting the [Console Do Not Track](https://consoledonottrack.com/) standard means a single env var in your shell profile turns off telemetry for FerrFlow, Bun, Astro, Next.js, Vite, Deno and the rest of the dev tools that honour it. No need to learn one variable per CLI. + +**First-run notice.** The first time you run the CLI on a machine, it prints one line to stderr: + +``` +Anonymous telemetry on. See https://ferrlabs.com/telemetry: opt out: FERRFLOW_TELEMETRY=0 or DO_NOT_TRACK=1 +``` + +A marker file in `$XDG_STATE_HOME/ferrflow/.telemetry-notice-shown` (or `~/Library/Application Support/ferrflow/` on macOS, `%LOCALAPPDATA%\ferrflow\` on Windows, override with `FERRFLOW_STATE_DIR`) suppresses it on subsequent runs. We never silently re-enable telemetry between versions; if you opted out, the notice doesn't print and nothing is sent. + +Part of [FerrLabs-Cloud#166](https://github.com/FerrLabs/FerrLabs-Cloud/issues/166). diff --git a/docs/site/changelog/2026-06-07-ferrflow-bot-release-rulesets.md b/docs/site/changelog/2026-06-07-ferrflow-bot-release-rulesets.md new file mode 100644 index 00000000..8905002b --- /dev/null +++ b/docs/site/changelog/2026-06-07-ferrflow-bot-release-rulesets.md @@ -0,0 +1,29 @@ +--- +title: 'FerrFlow: bot releases now work under branch rulesets (v5.2.2)' +summary: The FerrFlow bot identity pushes releases correctly even when the release branch is protected by a ruleset that bypasses ferrflow[bot]. Shipped in FerrFlow v5.2.2. +date: 2026-06-07T11:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/539 +--- + +Releases run with `bot: true` now push correctly even when your release branch is protected by a GitHub ruleset that bypasses `ferrflow[bot]`. Before this fix, the release commit could be rejected with `GH013: required status checks are expected` even though `ferrflow[bot]` was listed as a bypass actor. Shipped in **FerrFlow v5.2.2**: pinning the major (`FerrLabs/FerrFlow@v5`) picks it up automatically. + +### Why it happened + +`actions/checkout` persists your workflow's `GITHUB_TOKEN` as an `http..extraheader` git config entry. An HTTP extra-header outranks a credential helper, so even though FerrFlow swaps in its OIDC-minted bot token, git kept sending the original `GITHUB_TOKEN` on every push. The release commit therefore landed as `github-actions[bot]`. Which is not a bypass actor: and the ruleset rejected it. + +### What changes + +FerrFlow now resets that persisted extra-header on its own git invocations, so its bot token is the only credential on the wire and the push lands as `ferrflow[bot]`. This works regardless of your `actions/checkout` version and no longer requires `persist-credentials: false` on the checkout step. + +```yaml +- uses: actions/checkout@v6 + with: + fetch-depth: 0 +- uses: FerrLabs/FerrFlow@v5 + with: + bot: true +``` + +No config changes needed: upgrade to `@v5` (or pin `@v5.2.2` explicitly) and protected release branches just work. diff --git a/docs/site/changelog/2026-06-16-ferrflow-declarative-publishers.md b/docs/site/changelog/2026-06-16-ferrflow-declarative-publishers.md new file mode 100644 index 00000000..a337f092 --- /dev/null +++ b/docs/site/changelog/2026-06-16-ferrflow-declarative-publishers.md @@ -0,0 +1,40 @@ +--- +title: 'FerrFlow: declarative publishers: publish to cargo, npm, docker, helm & more on release' +summary: 'FerrFlow can now publish your packages to registries declaratively from ferrflow.json: cargo, npm, docker (multi-arch + Sigstore), helm, GitHub release assets and webhooks: replacing hand-rolled postPublish shell hooks. Shipped across FerrFlow v5.4–v5.7.' +date: 2026-06-16T10:00:00Z +product: ferrflow +type: new +docsLink: https://ferrflow.com/docs/configuration/config-file/#publishers +--- + +FerrFlow now publishes your packages to registries declaratively. Instead of wiring a `postPublish` shell hook that shells out to `cargo publish`, `npm publish`, or `docker buildx`, you describe the intent in `ferrflow.json` and FerrFlow runs it after the GitHub Release is created: per package, idempotently, and crash-resume safe. + +Six publisher kinds are supported: + +- **cargo**: `cargo publish` to crates.io or a private registry, with `allowDirty` and `noVerify`. +- **npm**: `npm publish` with a scoped, throwaway `.npmrc` so your project's `.npmrc` is never touched. +- **docker**: `docker buildx build --push` with multi-arch (`{version}`/`{major}`/`{minor}`/`latest` tag templates), and optional `sign: sigstore` (`cosign`). +- **helm**: `helm package` + `helm push` to an OCI registry. +- **github-release-asset**: attach a sidecar file (SBOM, signature, …) to the release with `--clobber`. +- **webhook**: POST to Slack/Discord/your own endpoint with `{name}`, `{version}`, `{tag}` and `{env:NAME}` interpolation. + +```json title="ferrflow.json" +{ + "workspace": { + "registries": { + "kellnr": { "tokenEnv": "CARGO_REGISTRIES_KELLNR_TOKEN" } + } + }, + "package": [{ + "name": "my-crate", + "path": "crates/my-crate", + "publishers": [ + { "kind": "cargo", "registry": "kellnr", "noVerify": true } + ] + }] +} +``` + +Registry tokens are referenced by env-var name (`tokenEnv`): the token value never lives in the config file, so `ferrflow.json` stays safe to commit. Every command publisher also takes an `args` array, a verbatim escape hatch for flags FerrFlow doesn't model natively (`--locked`, `--provenance`, `--build-arg`, …). + +Publishers are idempotent: an already-published version is detected and skipped, so a re-run after a partial failure picks up exactly where it left off. The block is additive: your existing `postPublish` hooks keep working while you migrate. See the [publishers documentation](https://ferrflow.com/docs/configuration/config-file/#publishers) for the full reference. diff --git a/docs/site/changelog/2026-06-25-ferrflow-jobs-flag.md b/docs/site/changelog/2026-06-25-ferrflow-jobs-flag.md new file mode 100644 index 00000000..f413ea4e --- /dev/null +++ b/docs/site/changelog/2026-06-25-ferrflow-jobs-flag.md @@ -0,0 +1,24 @@ +--- +title: 'FerrFlow: control parallelism with --jobs / FERRFLOW_JOBS' +summary: 'FerrFlow now exposes a global --jobs flag (and FERRFLOW_JOBS env var) to cap how many threads it uses for CPU-parallel work. Default stays all logical cores; --jobs 1 forces single-threaded: handy for constrained CI runners and reproducible benchmarks.' +date: 2026-06-25T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/598 +docsLink: https://ferrflow.com/docs/reference/cli/#global-flags +--- + +Since FerrFlow started planning packages and creating releases in parallel, two needs emerged: capping CPU use on small CI runners, and keeping benchmark numbers reproducible regardless of a machine's core count. The new global `--jobs ` flag: or the `FERRFLOW_JOBS` env var, with the flag taking precedence: covers both. + +- **Default (unset):** all logical cores, exactly as before. +- **`--jobs N`:** caps per-package planning to N threads, and bounds the release-creation HTTP pool by N too: so `--jobs 1` makes the whole run single-threaded. + +```bash +# Pin to one thread: deterministic, light on a 2-core runner +ferrflow --jobs 1 release + +# Or via the environment +FERRFLOW_JOBS=4 ferrflow release +``` + +It works with every command, since it's a global flag. diff --git a/docs/site/changelog/2026-06-25-ferrflow-publish-tag-scope.md b/docs/site/changelog/2026-06-25-ferrflow-publish-tag-scope.md new file mode 100644 index 00000000..f8dde28e --- /dev/null +++ b/docs/site/changelog/2026-06-25-ferrflow-publish-tag-scope.md @@ -0,0 +1,35 @@ +--- +title: 'FerrFlow: ferrflow publish auto-scopes to the triggering tag' +summary: 'ferrflow publish now figures out which package to publish from the tag that triggered the run: so one workflow publishes each package on its own tag, no per-package wiring. It also takes multiple package names, and a --all flag to force everything.' +date: 2026-06-25T11:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/602 +docsLink: https://ferrflow.com/docs/reference/cli/#ferrflow-publish +--- + +`ferrflow publish` runs your declarative publishers in a separate CI job. Until now it published one named package or all of them: which meant a tag-triggered job had to parse `api@v2.2.1` and pass `api` itself. No more: `ferrflow publish` reads the triggering tag (`GITHUB_REF` / `CI_COMMIT_TAG`) and publishes just that package. + +So a single workflow handles a whole monorepo. Every package publishes on its own tag, with zero per-package wiring: + +```yaml title=".github/workflows/publish.yml" +on: + push: + tags: ['v*', '*@v*'] # single-repo and monorepo per-package tags +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: FerrLabs/FerrFlow@v5 + with: + mode: publish # self-scopes to the tag that fired +``` + +The selection rules: + +- **`ferrflow publish`**: triggering tag → that package; otherwise every package (unchanged, so the existing release→publish job is unaffected). +- **`ferrflow publish api web`**: explicit packages (now more than one). +- **`ferrflow publish --all`**: every package, ignoring any tag scope. + +Scope is by package name / tag, not conventional-commit scope, and the version is still read from package state: the tag's version suffix is just informational. diff --git a/docs/site/changelog/2026-06-26-ferrflow-arm-targets.md b/docs/site/changelog/2026-06-26-ferrflow-arm-targets.md new file mode 100644 index 00000000..b7cebfa4 --- /dev/null +++ b/docs/site/changelog/2026-06-26-ferrflow-arm-targets.md @@ -0,0 +1,24 @@ +--- +title: 'FerrFlow: Windows arm64 and Linux armv7 prebuilt binaries' +summary: 'FerrFlow now ships prebuilt, signed binaries: and matching npm packages: for Windows on ARM (aarch64) and 32-bit ARM Linux (armv7). The release matrix grows from 5 targets to 7, so `npm i ferrflow` and direct downloads now work on Surface/Snapdragon Windows laptops and 32-bit Raspberry Pi / SBCs.' +date: 2026-06-26T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/607 +docsLink: https://ferrflow.com/docs/verifying-releases/ +--- + +ARM64 was already covered on Linux and macOS, but not Windows: and 32-bit ARM Linux had no build at all. This release closes both gaps, taking the target matrix to seven. + +Two new targets ship with every release, signed with Sigstore and listed in the SBOM exactly like the existing five: + +- **`aarch64-pc-windows-msvc`** → `ferrflow-windows-arm64.zip` / `@ferrflow/win32-arm64`: Windows on ARM (Surface Pro X/11, Snapdragon X laptops, Dev Kit 2023). +- **`armv7-unknown-linux-musleabihf`** → `ferrflow-linux-armv7.tar.gz` / `@ferrflow/linux-arm`: 32-bit ARM Linux (Raspberry Pi on a 32-bit OS, older SBCs). + +The npm wrapper resolves the right binary automatically through `optionalDependencies`, so nothing changes in how you install: + +```bash +npm install -D ferrflow +``` + +On a Windows-on-ARM or armv7 machine npm now pulls the native package instead of failing. Prebuilt archives are also on the [GitHub release page](https://github.com/FerrLabs/FerrFlow/releases), and the GitHub Action installs the matching binary for the runner's architecture. diff --git a/docs/site/changelog/2026-07-13-ferrflow-hook-points.md b/docs/site/changelog/2026-07-13-ferrflow-hook-points.md new file mode 100644 index 00000000..68741c4b --- /dev/null +++ b/docs/site/changelog/2026-07-13-ferrflow-hook-points.md @@ -0,0 +1,32 @@ +--- +title: 'FerrFlow: Six new release hook points: post-commit, pre-tag, post-tag, pre-release, on-success, on-error' +summary: 'The release lifecycle gains six new hook points so you can run commands at the exact commit/tag/push boundaries: publish between tag and push, notify once when a release finishes, or react to a failure with the error code in hand.' +date: 2026-07-13T09:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/657 +docsLink: https://ferrflow.com/docs/configuration/config-file#hooks +--- + +FerrFlow already ran shell hooks at `pre_bump`, `post_bump`, `pre_commit`, `pre_publish`, and `post_publish`. That left gaps around the most delicate part of a release: the commit, the tag, and the push. This release fills them. + +- **`post_commit`**: after the release commit, before tagging. +- **`pre_tag`**: after the commit, immediately before `git tag`. Smoke-test the bumped tree before the tag lands. +- **`post_tag`**: after tags are created, before push. This is the natural place to `cargo publish`: the tag exists locally but isn't pushed yet, so a failed publish is recoverable by dropping the local tag instead of rewriting remote history. +- **`pre_release`**: after the release pull request opens, before merge (`releaseCommitMode: pr` only). Post a review ping or run cross-repo validation. +- **`on_success`**: once, after the whole release finishes cleanly. Trigger a downstream deploy. +- **`on_error`**: once, when a release fails. It receives the failing error code in `FERRFLOW_ERROR_CODE`, so a Slack alert can link straight to the error reference. + +```json +{ + "workspace": { + "hooks": { + "postTag": "cargo publish", + "onSuccess": "curl -X POST $DEPLOY_WEBHOOK", + "onError": "curl -X POST $SLACK_WEBHOOK -d \"release failed: $FERRFLOW_ERROR_CODE\"" + } + } +} +``` + +Per-package hooks (`post_commit`, `pre_tag`, `post_tag`) run once per released package; the once-per-run hooks (`pre_release`, `on_success`, `on_error`) fire a single time for the whole invocation. Note that `on_error` (the reactive hook) is distinct from `on_failure`, which stays what it always was: the `abort` / `continue` strategy for how a failing hook is handled. diff --git a/docs/site/changelog/2026-07-16-ferrflow-migrate-semantic-release.md b/docs/site/changelog/2026-07-16-ferrflow-migrate-semantic-release.md new file mode 100644 index 00000000..e2e713bc --- /dev/null +++ b/docs/site/changelog/2026-07-16-ferrflow-migrate-semantic-release.md @@ -0,0 +1,41 @@ +--- +title: 'FerrFlow: Migrate from semantic-release in one command' +summary: 'A new `ferrflow migrate` command reads your existing `.releaserc` and generates the equivalent FerrFlow config: mapping tag format, branches, and the common plugins, and surfacing anything without an equivalent instead of guessing.' +date: 2026-07-16T09:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/699 +docsLink: https://ferrflow.com/docs/reference/cli#ferrflow-migrate +--- + +Switching release tools shouldn't mean rewriting your config by hand. `ferrflow migrate` reads an existing semantic-release setup and writes the equivalent `ferrflow.json` for you. + +```bash +ferrflow migrate # auto-detects .releaserc +ferrflow migrate --from semantic-release +``` + +It maps what has a FerrFlow equivalent: + +- `tagFormat: "v${version}"` → `tagTemplate: "v{version}"` +- `branches` → FerrFlow channels: `main`/`master` become the stable line, a `prerelease` branch becomes a channel +- `@semantic-release/changelog` → the package's `changelog` path +- `@semantic-release/exec` → hooks (`prepareCmd` → `preBump`, `publishCmd` → `postPublish`, and the rest) +- `@semantic-release/github` / `gitlab` → `forge` + +The point is what it does with the rest. Anything without a FerrFlow equivalent is **surfaced, never guessed**: `@semantic-release/npm`, custom `commit-analyzer` release rules, and `repositoryUrl` are each reported with a note on what to do, not silently dropped. The command prints a summary of what mapped, what it ignored, and what needs a manual look: and it won't overwrite a config you already have. + +``` +Migrated semantic-release config from .releaserc → ferrflow.json + +Mapped: + • tagFormat → tagTemplate (v${version}) + • branch main → stable + • branch beta → prerelease `beta` + • @semantic-release/github → forge + +Review manually: + ! @semantic-release/npm → not mapped. Configure `publishers` by hand if you publish to npm. +``` + +JSON `.releaserc` is supported today; a YAML `.releaserc` or a JavaScript `release.config.js` is reported as unsupported rather than mis-read. After migrating, review the generated config, then run `ferrflow validate` and `ferrflow check`. diff --git a/docs/site/changelog/2026-07-17-ferrflow-linked-fixed-groups.md b/docs/site/changelog/2026-07-17-ferrflow-linked-fixed-groups.md new file mode 100644 index 00000000..cd1d34c7 --- /dev/null +++ b/docs/site/changelog/2026-07-17-ferrflow-linked-fixed-groups.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: Linked and fixed package version groups' +summary: 'New `linked` and `fixed` workspace options let a set of monorepo packages share a version: when one bumps, the whole group moves to the same version, the way changesets and lerna do it.' +date: 2026-07-17T09:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/724 +docsLink: https://ferrflow.com/docs/configuration/monorepo#linked-and-fixed-version-groups +--- + +Until now the only way to couple packages in a monorepo was `dependsOn`, which cascades a bump but leaves each package on its own version. Some packages, though, are meant to move as one: `react` and `react-dom`, or a set of packages you publish under a single version line. `linked` and `fixed` express exactly that. + +```toml +[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 the group would reach. A `feat` on one package and a `fix` on another release the whole group on the minor, and a member with no commits of its own is pulled in at the shared version. Package names stay distinct; only the number is shared. + +`linked` packages share a version line when they're released together. `fixed` packages are locked to an identical version forever: the same behaviour, plus `ferrflow validate` warns if their versions have drifted apart so you catch a stray manual edit before the next release realigns them. + +Misconfigurations are caught before anything is written: a group with a package that isn't defined, a package listed in two groups, or a group with fewer than two members stops the release with a clear error: the same pre-flight guarantee as dependency-cycle detection. diff --git a/docs/site/changelog/2026-07-17-ferrflow-telemetry-removed.md b/docs/site/changelog/2026-07-17-ferrflow-telemetry-removed.md new file mode 100644 index 00000000..fb70a9d0 --- /dev/null +++ b/docs/site/changelog/2026-07-17-ferrflow-telemetry-removed.md @@ -0,0 +1,14 @@ +--- +title: 'FerrFlow: Telemetry removed. The CLI no longer phones home' +summary: 'FerrFlow no longer collects any usage telemetry. The module is gone from the binary entirely, along with the network calls it made on every command: which also makes monorepo commands dramatically faster.' +date: 2026-07-17T15:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/716 +--- + +Starting with v5.33, FerrFlow makes no network requests of its own beyond the git and forge operations you explicitly ask for. The anonymous-usage telemetry that previous versions sent on each command has been removed outright: not made opt-in, removed. There is nothing to disable and no data to think about. + +There is a performance angle too, and it is not small. The old telemetry sent one event per bumped package and blocked the CLI's exit until every request completed. On a 50-package monorepo `check`, that was the difference between 1.5 seconds and 80 milliseconds. Most of what felt like "work" was the CLI waiting on its own telemetry. Removing it made the wait disappear for every command, on every repository. + +Existing configurations need no changes: the `anonymousTelemetry` key (and its `telemetry` alias) remains accepted and simply does nothing, so no config warns or fails validation. The `FERRFLOW_TELEMETRY` and `DO_NOT_TRACK` environment variables still work on older versions: see the updated telemetry page for what ≤v5.32 sent and how to opt out there. diff --git a/docs/site/changelog/2026-07-18-ferrflow-bitbucket.md b/docs/site/changelog/2026-07-18-ferrflow-bitbucket.md new file mode 100644 index 00000000..aa52daae --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-bitbucket.md @@ -0,0 +1,20 @@ +--- +title: 'FerrFlow: Bitbucket support' +summary: FerrFlow now recognises Bitbucket Cloud repositories and handles their releases. No more scripting a hook to get automatic release tagging on Bitbucket. +date: 2026-07-18T13:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/729 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +Bitbucket users used to fall through FerrFlow's forge detection and had to script release creation by hand. Now `bitbucket.org` remotes are detected automatically (or set `forge = "bitbucket"` explicitly), authenticated with `BITBUCKET_TOKEN`. + +Bitbucket has no "release" object the way GitHub and GitLab do, so on Bitbucket a release **is** the annotated tag FerrFlow already creates and pushes: FerrFlow looks it up via the Bitbucket Cloud API and surfaces its URL in the release output. The changelog lives in your `CHANGELOG.md` and the tag message, as usual. + +```toml +[workspace] +forge = "bitbucket" # or leave it "auto": bitbucket.org is detected +``` + +Pull-request release mode and Bitbucket Server / Data Center aren't supported yet. This first pass covers Bitbucket Cloud release creation, mirroring how Gitea/Forgejo landed. diff --git a/docs/site/changelog/2026-07-18-ferrflow-breaking-change-variants.md b/docs/site/changelog/2026-07-18-ferrflow-breaking-change-variants.md new file mode 100644 index 00000000..7cd8800a --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-breaking-change-variants.md @@ -0,0 +1,20 @@ +--- +title: 'FerrFlow: Robust BREAKING CHANGE detection' +summary: 'FerrFlow now recognises mixed-case, hyphenated, and lowercase `BREAKING CHANGE` footers: and a `!` typed inside the scope (`feat(api!):`): so a real breaking change no longer slips through and skips the major bump.' +date: 2026-07-18T11:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/727 +docsLink: https://ferrflow.com/docs/reference/conventional-commits#accepted-footer-variants +--- + +A breaking change that FerrFlow doesn't recognise is quietly misclassified: it skips the major bump and ships a release that understates the change. Several real-world spellings of the `BREAKING CHANGE` marker used to fall through: now they don't. + +Newly detected as breaking: + +- **Case-insensitive footers**: `breaking-change:`, `breaking change:`, `Breaking Change:`: alongside the spec's `BREAKING CHANGE:` and `BREAKING-CHANGE:`. +- **`!` inside the scope**: `feat(api!):`, the common typo for `feat(api)!:`. + +Detection stays strict where it matters, so a stray mention never triggers an accidental major bump: the footer still has to start a line and be followed by a colon **and a space**, the plural `BREAKING CHANGES:` and prose mentions are ignored, and a `!` that isn't right before the closing paren (`feat(a!b):`) is not a marker. + +If your project used any of these variants, the next release will now correctly bump the major version. Run `ferrflow check` to preview. diff --git a/docs/site/changelog/2026-07-18-ferrflow-commit-graph.md b/docs/site/changelog/2026-07-18-ferrflow-commit-graph.md new file mode 100644 index 00000000..cbe145f8 --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-commit-graph.md @@ -0,0 +1,14 @@ +--- +title: 'FerrFlow: Faster repeat runs via commit-graph' +summary: 'On large repositories, FerrFlow now writes a git commit-graph on the first run so that later `check` and `release` walks are materially faster: a benefit that repeat users (pre-commit hooks, local dev, warm CI runners) get for free.' +date: 2026-07-18T12:00:00Z +product: ferrflow +type: perf +prLink: https://github.com/FerrLabs/FerrFlow/pull/728 +--- + +FerrFlow's tag scans and commit walks already read a git commit-graph when one exists, but a fresh clone. Every CI checkout: has a pack and no graph, so the speedup never kicked in where FerrFlow runs most. + +Now, on a cold `check` (cache miss) or a real `release`, if the repository has no commit-graph and a history larger than ~1000 commits, FerrFlow writes one with `git commit-graph write --reachable`. On a 200-package, 10k-commit monorepo that trims the walk-heavy portion of a run by roughly 30% on every subsequent invocation. + +It's transparent and best-effort: the write is skipped once a graph exists (so it's a one-time cost), skipped on small histories where it wouldn't pay off, skipped entirely on `--dry-run` so that stays read-only, and it never fails your command if git can't write the graph. diff --git a/docs/site/changelog/2026-07-18-ferrflow-doctor.md b/docs/site/changelog/2026-07-18-ferrflow-doctor.md new file mode 100644 index 00000000..903ebff5 --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-doctor.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: ferrflow doctor' +summary: 'A new `ferrflow doctor` subcommand runs read-only diagnostics on your repo, config, and forge setup and prints a categorised report: so "is my setup sane?" has a first-class answer instead of grepping `--verbose` logs.' +date: 2026-07-18T09:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/725 +docsLink: https://ferrflow.com/docs/reference/cli#ferrflow-doctor +--- + +When a release behaved unexpectedly: a wrong bump, a tag that didn't move, no commits detected: the only recourse was `--verbose` and staring at logs. `ferrflow doctor` gives you a first-class health check instead. + +```bash +ferrflow doctor # human report +ferrflow doctor --format json # machine-readable, stable for CI +ferrflow doctor --online # also check the GitHub API rate limit +``` + +It runs a series of read-only checks and groups them into five sections: **Repo** (git repository, commit history, clean working tree, remote, tags), **Config** (which config file wins, whether it parses, plus the full `ferrflow validate` suite), **Versioning** (strategy and each package's version), **Forge** (detected forge and whether an auth token is present), and **CI** (workflow files, and whether a workflow pins the FerrFlow action). Every check is green, a warning, or an error. + +Run it on a fresh checkout and it explains exactly what's missing before your first release; run it on a misconfigured repo and it pinpoints the file or value at fault. The exit code is scriptable: `0` all green, `1` warnings, `2` errors: and `--format json` has a stable shape you can assert on in CI. diff --git a/docs/site/changelog/2026-07-18-ferrflow-schema-subcommand.md b/docs/site/changelog/2026-07-18-ferrflow-schema-subcommand.md new file mode 100644 index 00000000..5796c60d --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-schema-subcommand.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: ferrflow schema' +summary: 'A new `ferrflow schema` subcommand prints the config JSON schema that is now bundled into the binary: so editor tooling and pre-commit hooks can validate `.ferrflow.json` completely offline.' +date: 2026-07-18T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/726 +docsLink: https://ferrflow.com/docs/reference/cli#ferrflow-schema +--- + +The FerrFlow config schema has always been published at `ferrflow.com/schema/ferrflow.json` for editor autocompletion: great in VS Code or Zed, useless offline or in a pre-commit hook that can't reach the internet. `ferrflow schema` fixes that by shipping the schema inside the binary. + +```bash +ferrflow schema # compact JSON to stdout +ferrflow schema --pretty # formatted +ferrflow schema --pretty --output ferrflow.schema.json +``` + +Write it to a file and point your editor at the local copy with `"$schema": "./ferrflow.schema.json"`, or generate it in CI to validate `.ferrflow.json` with no network call. The schema is embedded straight from the source of truth, so the bundled copy always matches the version you're running, and the command parses it before printing: a corrupt build artefact exits non-zero rather than emitting garbage. diff --git a/docs/site/changelog/2026-07-18-ferrflow-selfhosted-forge-autodetect.md b/docs/site/changelog/2026-07-18-ferrflow-selfhosted-forge-autodetect.md new file mode 100644 index 00000000..0e7c3bf1 --- /dev/null +++ b/docs/site/changelog/2026-07-18-ferrflow-selfhosted-forge-autodetect.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: Auto-detect self-hosted forges' +summary: FerrFlow now recognises self-hosted GitLab, GitHub Enterprise, and Gitea/Forgejo instances automatically. No need to set `forge` by hand for a git host on your own domain. +date: 2026-07-18T14:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/730 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +Until now, forge detection only knew the SaaS hostnames: a self-hosted GitLab or Gitea on `git.company.com` came back as "no forge", so you had to set `forge = "gitlab"` yourself to get automatic release creation. + +Now, when FerrFlow doesn't recognise the remote's hostname, it probes the host's API over HTTPS to figure out what it is: + +- a GitLab instance (self-hosted GitLab CE/EE), +- a GitHub Enterprise instance, +- or a Gitea / Forgejo instance. + +The probe is unauthenticated, short (a ~2s timeout), best-effort, and cached: so a run checks a given host at most once, and a hostname FerrFlow already knows never touches the network. You only need to set `forge` explicitly now if your host isn't reachable over HTTPS or you'd rather skip the probe. diff --git a/docs/site/changelog/2026-07-20-ferrflow-hook-all-packages.md b/docs/site/changelog/2026-07-20-ferrflow-hook-all-packages.md new file mode 100644 index 00000000..84ca6af8 --- /dev/null +++ b/docs/site/changelog/2026-07-20-ferrflow-hook-all-packages.md @@ -0,0 +1,32 @@ +--- +title: 'FerrFlow: Hooks see the whole release batch' +summary: 'Release hooks now receive `allPackages`. Every package the run bumps, with its new version and bump type: so a monorepo hook can act on the batch, not just its own package.' +date: 2026-07-20T16:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/741 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +A hook used to see only its own package. In a monorepo that made batch-aware automation awkward: a `postBump` on one package had no idea what else was being released alongside it. + +Every hook now also gets `allPackages`: the full list of packages the run is bumping, each with its resolved new version and bump type. It's the same snapshot for every package's hook and for the batch-level `onSuccess` / `onError`, computed once after versions are settled, so even the first package's hook already sees the whole release. + +Shell hooks read `FERRFLOW_ALL_PACKAGES_JSON`; JS/TS function hooks get `ctx.allPackages` as a real array: + +```js +export default { + workspace: { + hooks: { + postBump(ctx) { + const summary = ctx.allPackages + .map((p) => `${p.name}@${p.version} (${p.bump})`) + .join(', '); + console.log(`Releasing: ${summary}`); + }, + }, + }, +}; +``` + +For a single-package repo it's just the one entry. diff --git a/docs/site/changelog/2026-07-20-ferrflow-rich-hook-context.md b/docs/site/changelog/2026-07-20-ferrflow-rich-hook-context.md new file mode 100644 index 00000000..2e059454 --- /dev/null +++ b/docs/site/changelog/2026-07-20-ferrflow-rich-hook-context.md @@ -0,0 +1,35 @@ +--- +title: 'FerrFlow: Richer context for release hooks' +summary: Release hooks now receive the rendered changelog, the parsed commits, the list of bumped files, and whether the run is a monorepo or pre-release. Both as `FERRFLOW_*` env vars and on the JS/TS `ctx` object. +date: 2026-07-20T12:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/736 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +Hooks used to see only the basics: package name, old and new version, bump type, tag. That was enough to echo a message, but not enough to post a real release note or gate on what actually changed. + +Every hook now gets the full picture. Shell hooks read new `FERRFLOW_*` environment variables; JS/TS function hooks get the same data as properties on the `ctx` object (both are built from one source, so they never drift): + +- `changelog`: the rendered changelog section for this bump, in markdown. +- `commits`: the commits in the bump as structured objects (`hash`, `message`, `type`, `scope`, `breaking`), so you can branch on a breaking change instead of grepping a string. +- `bumpedFiles`. Every file the release actually modified, with its format. +- `monorepo` and `isPrerelease`: flags to tell a workspace release apart from a single-package one, and a channel pre-release from a stable one. + +`commits` and `bumpedFiles` arrive as real arrays in function hooks (`FERRFLOW_COMMITS_JSON` / `FERRFLOW_BUMPED_FILES_JSON` as JSON for shell hooks, ready to pipe through `jq`): + +```js +export default { + workspace: { + hooks: { + postBump(ctx) { + const breaking = ctx.commits.filter((c) => c.breaking); + if (breaking.length) notify(`${breaking.length} breaking change(s) in ${ctx.newVersion}`); + }, + }, + }, +}; +``` + +The enrichment reaches every hook point: pre/post bump, commit, tag, publish, release, plus `onSuccess` / `onError`. diff --git a/docs/site/changelog/2026-07-21-ferrflow-diff-command.md b/docs/site/changelog/2026-07-21-ferrflow-diff-command.md new file mode 100644 index 00000000..f5a22a59 --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-diff-command.md @@ -0,0 +1,20 @@ +--- +title: 'FerrFlow: Compare two versions with `diff`' +summary: 'A new `ferrflow diff v1.4.0..v1.6.0` command shows exactly what went into a version range: the commits and their bumps, the breaking changes, the files touched, and the changelog FerrFlow would generate for the range.' +date: 2026-07-21T15:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/751 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +Sometimes you want to look at a release after the fact: what actually shipped between two versions, why the bump landed where it did, or you need release notes for a range you never tagged notes for. `ferrflow diff` answers that. + +```bash +ferrflow diff v1.4.0..v1.6.0 # single-package repo +ferrflow diff api v1.4.0..v1.6.0 # monorepo: name the package +``` + +It prints the range's commits each tagged with its own bump (`major` / `minor` / `patch` / `none`), highlights the breaking changes, summarises the files changed, and renders the changelog section for the range using your changelog config. Add `--json` for a structured object you can pipe into anything. + +Each side of the range is a version or tag: `v1.4.0` resolves to the tag directly in a single-package repo, or to the package's tag (`api@v1.4.0`) in a monorepo. diff --git a/docs/site/changelog/2026-07-21-ferrflow-hook-release-url.md b/docs/site/changelog/2026-07-21-ferrflow-hook-release-url.md new file mode 100644 index 00000000..80cebef4 --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-hook-release-url.md @@ -0,0 +1,27 @@ +--- +title: 'FerrFlow: Post-publish hooks get the release URL' +summary: '`postPublish` hooks now receive `releaseUrl`: the URL of the GitHub/GitLab release FerrFlow just created: so you can link to it from a Slack notification, a dashboard, or a downstream job.' +date: 2026-07-21T08:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/743 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +A `postPublish` hook knew the version and tag, but not where the release actually landed. To announce a release with a link, you had to reconstruct the URL by hand. + +FerrFlow now hands it to you. Once the forge release is created, `postPublish` hooks receive `releaseUrl`: the canonical URL of the GitHub or GitLab release for that package's tag. Shell hooks read `FERRFLOW_RELEASE_URL`; JS/TS function hooks get `ctx.releaseUrl`. + +```js +export default { + workspace: { + hooks: { + postPublish(ctx) { + if (ctx.releaseUrl) notify(`${ctx.package}@${ctx.newVersion} → ${ctx.releaseUrl}`); + }, + }, + }, +}; +``` + +It's `null` wherever the URL isn't known yet: at `prePublish` (the release isn't created until after), on `--dry-run`, and when no forge is configured. diff --git a/docs/site/changelog/2026-07-21-ferrflow-hosted-api.md b/docs/site/changelog/2026-07-21-ferrflow-hosted-api.md new file mode 100644 index 00000000..a64f8dce --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-hosted-api.md @@ -0,0 +1,26 @@ +--- +title: 'FerrFlow: Hosted API for validate, preview, schema and releases' +summary: 'A new hosted API at api.ferrlabs.com/v1/ferrflow/* lets you validate a config, preview version bumps and changelog, fetch the config JSON Schema, and resolve the latest release: over HTTP, with results identical to the CLI.' +date: 2026-07-21T16:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrLabs-Cloud/pull/668 +docsLink: https://ferrflow.com/docs/reference/api +--- + +Some things are easier over HTTP than by shelling out to the CLI: a web validator, an editor integration, a PR-preview comment, a "is there a newer version?" check. FerrFlow now exposes a small hosted API for exactly those, under `https://api.ferrlabs.com/v1/ferrflow/*`. Every endpoint is public (no auth) and safe to call from CI, an editor, or a browser. + +The important part: `validate` and `preview` run the **same FerrFlow core the CLI runs**, so their results are identical to `ferrflow validate` and `ferrflow check`. There's no second implementation to drift out of sync. + +```bash +# Preview the bump + changelog for a set of commits, no repo needed +curl -X POST https://api.ferrlabs.com/v1/ferrflow/preview \ + -H 'content-type: application/json' \ + -d '{ + "config": "{\"package\":[{\"name\":\"api\",\"path\":\".\"}]}", + "commits": [{ "message": "feat(api): add endpoint" }], + "current_versions": { "api": "1.2.3" } + }' +``` + +The full set: `POST /validate` (hosted config validation), `POST /preview` (bump + changelog), `GET /schema` (the config JSON Schema, straight from the source of truth: point your editor's `$schema` at it), `GET /latest` (latest release + per-platform download and signature URLs), and `GET /health`. The machine-readable contract lives at `/v1/ferrflow/openapi.json`. See the [API reference](https://ferrflow.com/docs/reference/api) for every endpoint. diff --git a/docs/site/changelog/2026-07-21-ferrflow-migrate-js-yaml.md b/docs/site/changelog/2026-07-21-ferrflow-migrate-js-yaml.md new file mode 100644 index 00000000..619cfe8a --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-migrate-js-yaml.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: Migrate reads JS and YAML configs' +summary: '`ferrflow migrate` now imports JavaScript and YAML release configs directly: `.releaserc.js`, `release.config.js`, `.releaserc.yaml`, `.versionrc.js`, and friends: not just JSON, so you no longer have to hand-convert your config before migrating.' +date: 2026-07-21T14:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/750 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +Plenty of semantic-release and standard-version setups keep their config in JavaScript (`release.config.js`) or YAML (`.releaserc.yaml`) rather than JSON. `ferrflow migrate` used to read JSON only and told you to convert the rest by hand first. + +Now it reads them directly. A JavaScript config is evaluated with `node`. It imports the config, resolves it if it's a function, and reads the result: the same way FerrFlow already runs your `ferrflow.js` config, run locally against your own repo. A YAML config is parsed straight through. Both then feed the existing converters, so the mapping and the mapped/ignored/review report are identical to the JSON path. + +```bash +ferrflow migrate # auto-detects .releaserc.js, .releaserc.yaml, release.config.js, … +``` + +JavaScript configs need Node.js on your PATH; if it's missing, the error says so. diff --git a/docs/site/changelog/2026-07-21-ferrflow-migrate-more-tools.md b/docs/site/changelog/2026-07-21-ferrflow-migrate-more-tools.md new file mode 100644 index 00000000..bb20236f --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-migrate-more-tools.md @@ -0,0 +1,24 @@ +--- +title: 'FerrFlow: Migrate from changesets, release-please & standard-version' +summary: '`ferrflow migrate` now imports configs from changesets, release-please, and standard-version too: not just semantic-release: generating an equivalent `ferrflow.json` and a report of what mapped, what was ignored, and what needs a look.' +date: 2026-07-21T13:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/745 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +Switching to FerrFlow shouldn't mean rewriting your release config by hand. `ferrflow migrate` reads your existing release tool's config and writes the equivalent `ferrflow.json`. It already understood semantic-release; now it covers the other three big ones too. + +- **release-please** (`release-please-config.json`): the `packages` map becomes FerrFlow packages, each package's `release-type` picks the right version file (Cargo.toml, package.json, pyproject.toml, Chart.yaml, …), `include-component-in-tag` becomes a `tagTemplate`, and the PR flow becomes `releaseCommitMode: pr`. +- **standard-version** (`.versionrc`): `tagPrefix` becomes a `tagTemplate` and `bumpFiles` / `packageFiles` become `versionedFiles`. +- **changesets** (`.changeset/config.json`): `baseBranch` becomes `branch`, and `linked` / `fixed` become version groups. + +It auto-detects the source, or you can force it with `--from release-please`. Every run prints what mapped, what it ignored, and what needs manual review. Nothing is silently guessed. For changesets it also flags the one thing that matters: changesets versions from `.changeset/*.md` files, while FerrFlow versions from conventional commits. + +```bash +ferrflow migrate # auto-detect +ferrflow migrate --from standard-version +``` + +Then review the generated config and run `ferrflow validate`. diff --git a/docs/site/changelog/2026-07-21-ferrflow-persistent-release-pr.md b/docs/site/changelog/2026-07-21-ferrflow-persistent-release-pr.md new file mode 100644 index 00000000..8a5f8160 --- /dev/null +++ b/docs/site/changelog/2026-07-21-ferrflow-persistent-release-pr.md @@ -0,0 +1,17 @@ +--- +title: 'FerrFlow: One persistent release PR' +summary: 'In `releaseCommitMode: pr`, FerrFlow now keeps a single long-lived release pull request and updates it on each new commit, instead of opening a fresh PR: and leaving a stale one. Every time the version changes.' +date: 2026-07-21T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/744 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +`releaseCommitMode: pr` opens your version bump and changelog as a pull request instead of committing straight to the branch. Until now that PR was tied to the version it was opened for, so a second `feat:` landing before you merged spawned a _new_ PR and left the first one stale. You'd end up with a pile of `v1.3.0`, `v1.4.0`, … release PRs to clean up. + +Now it's one PR. FerrFlow keeps a single release branch per target: `ferrflow/release-`: and on every new commit it recomputes the version and changelog and force-pushes that same branch, so the open PR's title and body update in place. This is the release-please / Changesets model. + +`autoMergeReleases` (default `true`) re-enables auto-merge on each update, and is a no-op when off: the PR just waits for a human. PR mode works on GitHub and GitLab. + +It also won't stomp on your work: if you push a commit onto the release branch yourself: a review fix, say: FerrFlow notices it isn't a `chore(release):` commit, warns, and leaves the branch and PR untouched for that run rather than force-pushing over it. diff --git a/docs/site/changelog/2026-07-25-ferrflow-concurrent-release-replan.md b/docs/site/changelog/2026-07-25-ferrflow-concurrent-release-replan.md new file mode 100644 index 00000000..5d8c1c3b --- /dev/null +++ b/docs/site/changelog/2026-07-25-ferrflow-concurrent-release-replan.md @@ -0,0 +1,14 @@ +--- +title: 'FerrFlow: Concurrent releases no longer fail with E2006' +summary: 'When two release runs race, the losing run now replans against the version the winner published instead of aborting with E2006. And no longer silently drops the other packages it was about to release.' +date: 2026-07-25T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/766 +--- + +On an active repo, merging a few PRs in quick succession triggers near-simultaneous release runs. One of them could fail with `error[E2006]: Tag(s) already exist on remote pointing to a different commit`, and the message suggested deleting the divergent tag or re-running with `--force`. Both were wrong here: the tag belonged to a concurrent run's perfectly valid release, so either remedy would have destroyed or overwritten a published version. + +The run rebased its release commit onto the new remote tip but kept the version plan it had computed *before* rebasing, so it still tried to create a tag another run had already taken. Worse, the failure aborted the whole run: a second package with unreleased changes was left unreleased with no signal beyond the failed job. + +A losing run now resets to the remote tip and recomputes its plan against the winner's history. The bump is applied on top of whatever was just published, so a fix planned as `1.0.1` becomes `1.1.1` if a concurrent run shipped `1.1.0` in the meantime. It never lands below the latest release. Every package in the run is replanned together, so nothing is dropped. E2006 is now reserved for genuine divergence, which makes its "delete the divergent remote tag" advice correct when it does appear. diff --git a/docs/site/changelog/2026-07-27-ferrflow-cabal-cmake-formats.md b/docs/site/changelog/2026-07-27-ferrflow-cabal-cmake-formats.md new file mode 100644 index 00000000..80e12a9a --- /dev/null +++ b/docs/site/changelog/2026-07-27-ferrflow-cabal-cmake-formats.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: Cabal and CMake version files' +summary: 'FerrFlow now bumps the version in Haskell `*.cabal` files and in the `project()` call of a `CMakeLists.txt`, bringing the supported formats to 16.' +date: 2026-07-27T14:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/773 +docsLink: https://ferrflow.com/docs/configuration/formats +--- + +Two ecosystems that kept coming up are now first-class: Haskell and CMake-based C/C++. Point a versioned file at them and FerrFlow bumps them like any other format, rewriting only the version span so the rest of the file keeps its layout and comments. + +```toml +[[package.versioned_files]] +path = "my-package.cabal" +format = "cabal" + +[[package.versioned_files]] +path = "CMakeLists.txt" +format = "cmake" +``` + +Both formats are careful about the version-shaped things sitting next to the one you actually want. In a `.cabal` file, `cabal-version:` declares which Cabal feature set the file uses: bumping it would change how the file is parsed: so only the top-level `version:` field is touched, and indented `version:` fields inside stanzas are left alone. In CMake, FerrFlow updates the `VERSION` argument of `project()`, including the multi-line form, while `cmake_minimum_required(VERSION …)` and any `set(_VERSION …)` variable stay untouched. diff --git a/docs/site/changelog/2026-07-27-ferrflow-cosign-bundles.md b/docs/site/changelog/2026-07-27-ferrflow-cosign-bundles.md new file mode 100644 index 00000000..68a911b6 --- /dev/null +++ b/docs/site/changelog/2026-07-27-ferrflow-cosign-bundles.md @@ -0,0 +1,25 @@ +--- +title: 'FerrFlow: Releases are signed with a single Sigstore bundle' +summary: 'From v5.48.0 each release artifact ships one `.bundle` instead of a separate `.sig` and `.crt`. Verification is one flag shorter; older releases keep working as before.' +date: 2026-07-27T20:00:00Z +product: ferrflow +type: breaking +prLink: https://github.com/FerrLabs/FerrFlow/pull/774 +docsLink: https://ferrflow.com/docs/verifying-releases +--- + +Sigstore's tooling moved on: cosign v3 replaced the separate signature and certificate outputs with a single bundle that carries both. FerrFlow releases follow suit. Every artifact: the platform tarballs and zips, the completions archive, the SBOM: now ships one `.bundle` sidecar instead of a `.sig` + `.crt` pair. + +Verification loses a flag: + +```bash +cosign verify-blob \ + --bundle ferrflow-linux-x64.tar.gz.bundle \ + --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + ferrflow-linux-x64.tar.gz +``` + +**If you verify releases in CI, this is the change to make.** A pipeline that downloads `.sig` and `.crt` will fail on v5.48.0 and later, because those files are no longer published. Releases up to **v5.47.4** are untouched and still verify with `--certificate` and `--signature`. Nothing was removed retroactively. + +The hosted API follows the same rule: `GET /v1/ferrflow/latest` now returns `bundle_url` for releases that have one, and keeps returning `signature_url` and `cert_url` for the older releases that carry them, so a client can simply use whichever field is present. diff --git a/docs/site/changelog/2026-07-27-ferrflow-tags-before-releases.md b/docs/site/changelog/2026-07-27-ferrflow-tags-before-releases.md new file mode 100644 index 00000000..8e6ac44e --- /dev/null +++ b/docs/site/changelog/2026-07-27-ferrflow-tags-before-releases.md @@ -0,0 +1,14 @@ +--- +title: 'FerrFlow: Tags are pushed before releases are published' +summary: 'A release run now pushes its tags to the remote before creating the GitHub or Gitea release, so a failure in between can no longer leave a published release pointing at a tag that never landed: and annotated tag messages are no longer silently replaced.' +date: 2026-07-27T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/771 +--- + +A release run used to create the forge releases first and push the annotated tags afterwards. Any failure in between: a network blip, branch protection, a tag a concurrent run had just taken: left the forge ahead of git: releases published against a ref the remote did not have. + +On GitHub and Gitea it also cost you the tag message. FerrFlow sent the release with a target commit, so for a tag that did not exist yet those APIs created it themselves, as a *lightweight* tag. The annotated tag FerrFlow had built: the one carrying the changelog body: was pushed a moment later, pointed at the same commit, and was therefore treated as already synced and skipped. The lightweight tag was what survived. + +Tags now land first, and the release is created against a ref that provably exists. FerrFlow no longer sends a target commit at all, so the forge can't invent tags behind git's back, and `git show ` returns the release notes as intended. Pushing and publishing are also separate resume points now: if release creation fails, re-running retries only that step instead of replaying the push. diff --git a/docs/site/changelog/2026-07-28-ferrflow-changesets-workspace-discovery.md b/docs/site/changelog/2026-07-28-ferrflow-changesets-workspace-discovery.md new file mode 100644 index 00000000..63d45f64 --- /dev/null +++ b/docs/site/changelog/2026-07-28-ferrflow-changesets-workspace-discovery.md @@ -0,0 +1,17 @@ +--- +title: 'FerrFlow: Migrating from changesets now finds your packages' +summary: '`ferrflow migrate --from changesets` reads your workspace globs and scaffolds one package entry per workspace package, instead of a single root package you had to expand by hand.' +date: 2026-07-28T14:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/775 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +changesets is a monorepo tool, so migrating from it used to end with the least useful part done by hand: FerrFlow scaffolded a single root package and asked you to list every workspace package yourself. On a repo with thirty packages that's thirty entries to write before anything runs. + +The migration now reads the same workspace declaration changesets does: `workspaces` in `package.json` (both the array and the yarn-berry `{ packages: [...] }` form) and `packages:` in `pnpm-workspace.yaml`: expands the globs, and writes one `package` entry per discovered package, each pointing at its own `package.json` and `CHANGELOG.md`. + +The practical win is in the version groups. `linked` and `fixed` name packages by npm name, and those names now match packages that actually exist in the generated config, so it passes `ferrflow validate` straight out of the migration. If a group names something outside the workspace, the migration report says which one rather than leaving you to find out at validation time. + +Dependency directories are skipped, so a `node_modules` full of manifests is never mistaken for your workspace. A repo without a workspace declaration still gets a single root package, as before. diff --git a/docs/site/changelog/2026-08-04-ferrflow-diff-package-scope.md b/docs/site/changelog/2026-08-04-ferrflow-diff-package-scope.md new file mode 100644 index 00000000..7c8d0e51 --- /dev/null +++ b/docs/site/changelog/2026-08-04-ferrflow-diff-package-scope.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: `diff` now shows only the package you asked about' +summary: 'In a monorepo, `ferrflow diff ..` filters the range to commits that touched that package, so its commit list and changelog match what a release would actually produce.' +date: 2026-08-04T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/781 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +`ferrflow diff` compares two tags and shows you what changed between them. In a monorepo it was showing the raw git range. Every commit between the two tags, including the ones that only touched other packages. Asking about your API and being told it shipped a frontend redesign is worse than not asking. + +The range is now scoped by path: a commit counts if it touched the package's `path` or one of its `sharedPaths`. That is what the commit list, the breaking-change list and the generated changelog are built from. Which is the whole point of previewing it. + +This entry first said the scoping matched what `ferrflow release` produced. It did not. `release` went on collecting every commit since the package's last tag until that was fixed on 2026-08-29. Between the two dates the preview and the release disagreed, and the preview was the accurate one. + +```bash +ferrflow diff api api@v1.0.0..api@v1.1.0 +``` + +Shared paths still count, so a change to a `proto/` directory listed in the API's `sharedPaths` shows up under the API, as it should. Single-package repositories are unaffected. They own every commit, and nothing is filtered. `--json` output is scoped identically. diff --git a/docs/site/changelog/2026-08-05-ferrflow-api-own-domain.md b/docs/site/changelog/2026-08-05-ferrflow-api-own-domain.md new file mode 100644 index 00000000..da99c14c --- /dev/null +++ b/docs/site/changelog/2026-08-05-ferrflow-api-own-domain.md @@ -0,0 +1,25 @@ +--- +title: 'FerrFlow: the hosted API moves to api.ferrflow.com' +summary: 'The FerrFlow API and the hosted bot token exchange now answer on api.ferrflow.com. The old api.ferrlabs.com host keeps working indefinitely.' +date: 2026-08-05T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow-Cloud/pull/772 +docsLink: https://ferrflow.com/docs/reference/api +--- + +The hosted FerrFlow API: `validate`, `preview`, `latest`, the config schema, and the bot token exchange: was published under the holding company's hostname, `api.ferrlabs.com`. Pointing a `$schema` at a *ferrlabs* URL to validate a *ferrflow* config never read right, and every other product already serves its API from its own domain. It now answers on `api.ferrflow.com`, which is the host the documentation uses from here on. + +Paths are unchanged, so moving over is a hostname swap and nothing else: + +```bash +curl "https://api.ferrflow.com/v1/ferrflow/latest?platform=linux-x64" +``` + +```json +{ "$schema": "https://api.ferrflow.com/v1/ferrflow/schema" } +``` + +**Nothing breaks.** `api.ferrlabs.com/v1/ferrflow/*` reaches exactly the same endpoints and will keep doing so indefinitely. It is the same service behind a second hostname, not a redirect and not a deprecation. FerrFlow versions already released have the old endpoint compiled in and continue to work untouched; new versions of the CLI and the GitHub Action exchange their bot token on the new host. If you override the endpoint yourself, `bot_endpoint:` on the Action and `FERRFLOW_BOT_ENDPOINT` in the CLI still take precedence. + +One thing that deliberately did **not** move: the OIDC audience stays `ferrflow.ferrlabs.com`. It is the identifier your runner mints the token for, not a URL, and the service validates against that exact string. diff --git a/docs/site/changelog/2026-08-07-ferrflow-action-input-hardening.md b/docs/site/changelog/2026-08-07-ferrflow-action-input-hardening.md new file mode 100644 index 00000000..0130196d --- /dev/null +++ b/docs/site/changelog/2026-08-07-ferrflow-action-input-hardening.md @@ -0,0 +1,18 @@ +--- +title: 'FerrFlow: the Action no longer interpolates its inputs into shell' +summary: 'force_version, package and version reached the Action’s run scripts as raw shell source. They now arrive as environment variables and are validated before use.' +date: 2026-08-07T09:00:00Z +product: ferrflow +type: security +prLink: https://github.com/FerrLabs/FerrFlow/pull/810 +--- + +The FerrFlow GitHub Action built three of its shell steps with `${{ }}` interpolation, which substitutes a value into the script *before* the shell parses it. Any workflow that wired `force_version`, `package` or `version` from data it did not control: an issue title, a `workflow_dispatch` input, a matrix value derived from a pull request: could therefore have had that data executed as shell, in a job that by design holds a repository write token. + +All three inputs now reach the step as environment variables, are quoted at every use, and are checked before they are passed on: `version` must be `latest` or a semver triple, `force_version` must be `VERSION` or `NAME@VERSION`, and each entry in `package` must look like a package name. Anything else fails the step with a clear message instead of running. + +Arguments are assembled into an array and passed as `ferrflow "${args[@]}"`, so a value containing spaces or shell metacharacters is one inert argument rather than parsed source. + +**What you need to do:** nothing beyond moving to a current version of the Action. No input changed name, shape or meaning. If you were passing a value that does not match the formats above: a version string with unusual characters, for instance. The step will now tell you rather than silently doing something else. + +Also fixed in the same change: `mode: publish` did not forward the `bot` inputs, so `bot: true` combined with `mode: publish` silently ran without the hosted bot token. diff --git a/docs/site/changelog/2026-08-07-ferrflow-mit-license.md b/docs/site/changelog/2026-08-07-ferrflow-mit-license.md new file mode 100644 index 00000000..fad7352a --- /dev/null +++ b/docs/site/changelog/2026-08-07-ferrflow-mit-license.md @@ -0,0 +1,26 @@ +--- +title: 'FerrFlow: now MIT licensed' +summary: 'FerrFlow moves from MPL-2.0 to MIT. Fewer obligations for everyone using it; the change ships as v6, so Action pins move from @v5 to @v6.' +date: 2026-08-07T10:00:00Z +product: ferrflow +type: breaking +prLink: https://github.com/FerrLabs/FerrFlow/pull/813 +--- + +FerrFlow is now distributed under the MIT licence instead of MPL-2.0. This applies to the CLI, the Rust library published on crates.io, the `ferrflow` npm package and its platform binaries, and the WASM build. + +MPL-2.0 is weak copyleft: modify a FerrFlow source file and you had to publish that file. In practice that obligation almost never applied. FerrFlow is invoked, not linked: but it was enough to route the tool into legal review at organisations whose dependency policy allowlists MIT, BSD and Apache and flags everything else. That review always ended in approval, and always cost weeks. MIT removes the question. + +**Nothing is taken away.** Every right you had under MPL-2.0 you still have under MIT, plus the file-level reciprocity obligation is gone. Releases up to 5.52.x remain available under MPL-2.0 in perpetuity if you have a reason to prefer them. + +**What you need to do:** if you pin the GitHub Action to a floating major tag, move it to `@v6`. + +```yaml +- uses: FerrLabs/FerrFlow@v6 + with: + bot: true +``` + +`v5` stays where it is and will not receive further releases. Nothing else changes. No flags, no config fields, no output formats. The major bump exists so that "which licence?" has a clean answer per major line: v5 and earlier are MPL-2.0, v6 onward is MIT. Pinning a full version (`@v6.0.0`) or a commit SHA is unaffected. + +If you consume FerrFlow as a Rust library, `cargo update` picks the new licence up with the version; if your build enforces a licence allowlist, MIT should already be on it. diff --git a/docs/site/changelog/2026-08-07-ferrflow-verified-installs.md b/docs/site/changelog/2026-08-07-ferrflow-verified-installs.md new file mode 100644 index 00000000..29ce72da --- /dev/null +++ b/docs/site/changelog/2026-08-07-ferrflow-verified-installs.md @@ -0,0 +1,28 @@ +--- +title: 'FerrFlow: the Action verifies the binary it downloads' +summary: 'Releases now ship a signed SHA256SUMS, and the GitHub Action checks the archive against it: and its build provenance: before extracting anything.' +date: 2026-08-07T09:30:00Z +product: ferrflow +type: security +prLink: https://github.com/FerrLabs/FerrFlow/pull/811 +--- + +The FerrFlow Action installed the CLI by piping a release asset straight into `tar`. Nothing checked what came back before it was extracted and run: and it runs holding a repository write token. The release pipeline had been producing cosign signatures and build provenance attestations for a while, but nothing consumed them, and no checksum manifest was published at all. + +Releases now include a `SHA256SUMS` covering every platform archive. It is generated before the signing steps, so it is itself attested and cosign-signed, and it ships alongside `SHA256SUMS.bundle`. + +The Action downloads the archive to a temporary directory, verifies its digest against `SHA256SUMS`, and only then extracts. When you pin a specific version it additionally verifies build provenance with `gh attestation verify`: provenance binds to a concrete release, so it cannot be checked against the mutable `latest` URL. + +**What you need to do:** nothing. This is transparent on a current version of the Action. + +Two behaviours worth knowing. Releases cut before this change carry no `SHA256SUMS`; the Action warns and continues rather than breaking every workflow pinned to an older version. But a `SHA256SUMS` that exists and has no entry for the archive being installed is a hard failure: otherwise removing one line would be a trivial bypass. + +You can run the same checks yourself: + +```bash +cosign verify-blob \ + --bundle ferrflow-linux-x64.tar.gz.bundle \ + --certificate-identity-regexp "https://github.com/FerrLabs/FerrFlow/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + ferrflow-linux-x64.tar.gz +``` diff --git a/docs/site/changelog/2026-08-10-ferrflow-commit-formats.md b/docs/site/changelog/2026-08-10-ferrflow-commit-formats.md new file mode 100644 index 00000000..df784f53 --- /dev/null +++ b/docs/site/changelog/2026-08-10-ferrflow-commit-formats.md @@ -0,0 +1,46 @@ +--- +title: 'FerrFlow: commit formats are configurable, and the defaults are more permissive' +summary: 'workspace.commitFormats lets you declare which commit subjects map to which bump level. The defaults now also recognise Feat:, Fix/, feature: and friends. Which is why this ships as v7.' +date: 2026-08-10T18:00:00Z +product: ferrflow +type: breaking +prLink: https://github.com/FerrLabs/FerrFlow/pull/824 +docsLink: https://ferrflow.com/docs/configuration/config-file +--- + +FerrFlow only ever recognised strict lowercase conventional prefixes. Teams whose history uses `Feat/add-login`, `Fix: resolve crash` or `feature:`: the shapes that come out of branch-named squash merges: got no releases at all, with nothing in the output explaining why. + +`workspace.commitFormats` now declares the mapping. Each of `major`, `minor` and `patch` takes a pattern, a list of patterns, or `"all"`: + +```json +{ + "workspace": { + "commitFormats": { + "minor": ["feat:*", "feat(?*):*", "Feat/*"], + "patch": "all", + "caseSensitive": false + } + } +} +``` + +`*` matches any run of characters (including `/`, since commit subjects are prose and routinely contain file paths), `?` matches exactly one. Resolution runs major → minor → patch, first match wins. `"all"` is a catch-all, useful for repos that follow no convention at all: `"patch": "all"` releases on every commit while `minor` and `major` patterns still promote the ones that match. + +**What changes without you doing anything.** The built-in defaults are now permissive: `Feat:`, `Feat/`, `feature:`, `Fix:`, `Fix/`, `Refactor:` and `Refactor/` all trigger releases, alongside the lowercase conventional forms that already did. If your repository has commits in those shapes, they will start producing versions where they previously produced none. That is the breaking part, and the reason this is v7. + +To keep the old strictness, declare it: + +```json +{ + "workspace": { + "commitFormats": { + "minor": ["feat:*", "feat(?*):*"], + "patch": ["fix:*", "fix(?*):*", "perf:*", "perf(?*):*", "refactor:*", "refactor(?*):*"] + } + } +} +``` + +Breaking markers are not configurable and never needed to be: `feat!:`, `fix(api)!:`, the `feat(api!):` typo and a `BREAKING CHANGE:` footer are detected structurally, whatever patterns you set. A glob cannot express them precisely: `*!:*` would match any subject containing `!:` anywhere, turning `fix: handle the !: token in the parser` into a major release. + +**Action tag:** `v7` is the current floating major. Update `uses: FerrLabs/FerrFlow@v6` to `@v7`; `v6` will not receive further releases. diff --git a/docs/site/changelog/2026-08-15-ferrflow-lockfile-refresh.md b/docs/site/changelog/2026-08-15-ferrflow-lockfile-refresh.md new file mode 100644 index 00000000..c4eb6365 --- /dev/null +++ b/docs/site/changelog/2026-08-15-ferrflow-lockfile-refresh.md @@ -0,0 +1,16 @@ +--- +title: 'FerrFlow: lockfiles no longer go stale after a release' +summary: 'updateLockfiles could leave Cargo.lock at the previous version while the release still reported success. Both causes are fixed: the crate name is read from the manifest, and a cold registry cache no longer defeats the update.' +date: 2026-08-15T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/844 +--- + +If you set `updateLockfiles: true`, FerrFlow could bump `Cargo.toml` and leave `Cargo.lock` pointing at the previous version. Nothing failed: the update error was logged as a warning, the release finished green, and the drift surfaced later as a dirty working tree or a lockfile check in CI. + +There were two separate causes. FerrFlow ran `cargo update -p ` with the package name from your config, which is not the cargo crate name: a package called `api` holding a crate called `my-app-api` produced `package ID specification 'api' did not match any packages`. FerrFlow now reads the real crate name from the manifest it is versioning, so the two can differ freely. Separately, the update ran with `--offline`, which cannot resolve dependencies when the cargo registry cache is empty. That is the normal state of a release job that checks out and installs the toolchain without building, and it made the failure look intermittent: the same repo would update its lock on a warm runner and skip it on a cold one. + +`--offline` is still tried first, because it is faster and works with no network and no private-registry credentials. When it fails, FerrFlow retries once without it. + +Nothing to change on your side. If a lockfile drifted while this was broken, commit it once and releases will keep it in sync from here. diff --git a/docs/site/changelog/2026-08-20-ferrflow-version-source-option.md b/docs/site/changelog/2026-08-20-ferrflow-version-source-option.md new file mode 100644 index 00000000..5431b8ee --- /dev/null +++ b/docs/site/changelog/2026-08-20-ferrflow-version-source-option.md @@ -0,0 +1,29 @@ +--- +title: 'FerrFlow · choose whether the tag or the file is the source of truth' +summary: 'When a package has both a git tag and a version in a versioned file, FerrFlow took whichever was higher. That is still the default, and versionSource now lets a repo say the tags are the record, or that the file is.' +date: 2026-08-20T12:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/873 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +FerrFlow resolves the version it starts from by looking at two places: the highest tag reachable from HEAD, and the version written in the package's first versioned file. When both exist it took whichever was higher. That is a good default and remains the default, but it has a cost worth naming: a mistake in either source only ever ratchets upward. A bad merge that resurrects an old `version =` line, a hand-edit, a generator writing a future date into a manifest, and FerrFlow releases from the inflated number with nothing to walk it back. + +The reverse case is just as real, which is why this is a choice rather than a fix. Moving a package between repositories carries its manifest across while the tags stay behind, and there the file is the honest source. + +`versionSource` lets a repo say which one it trusts: + +```jsonc +{ + "workspace": { + "versionSource": "highest" // default, unchanged + // "tag" - the tags are the record of what shipped, ignore the file + // "file" - the file is the source, ignore the tag + } +} +``` + +It can be set per package too, so a monorepo migrating one package does not have to flip the policy for the rest. With only one source present the setting has no effect, and neither does it change how the version is then bumped. + +Nothing changes if you upgrade without touching your config. The release and check output now says which source a version came from and whether it won on height or because you configured it, so you can tell whether your repo needs the option at all before reaching for it. diff --git a/docs/site/changelog/2026-08-20-ferrflow-version-source.md b/docs/site/changelog/2026-08-20-ferrflow-version-source.md new file mode 100644 index 00000000..60f6340d --- /dev/null +++ b/docs/site/changelog/2026-08-20-ferrflow-version-source.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow · release output says where the current version came from' +summary: 'The version FerrFlow starts from is resolved from a tag, a versioned file, or neither. The release and check output now names the source, so a package whose tag was never pushed is distinguishable from one that has no tags yet.' +date: 2026-08-20T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/870 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +FerrFlow resolves the version it starts from in one of four ways: the highest tag reachable from HEAD, the version written in the package's first versioned file, the higher of the two when both exist, or the strategy's starting point when neither does. Until now all four printed the same bare number, so four different situations were indistinguishable from the output. + +That hides a real failure: a version that looks right for the wrong reason. A package whose tag was never pushed reads its version from the file and releases as if nothing were wrong, and a first release looks identical to a package that has simply lost its tags. Answering "where did that number come from" meant reading the source. + +The release and check lines now name the source: + +``` +● discord 2026.8.18 → 2026.8.19 (calverseq, from discord/Cargo.toml) +● discord 2026.8.18 → 2026.8.19 (calverseq, from discord/Cargo.toml, over tag v2026.8.1) +● api 0.0.0 → 0.1.0 (minor, bootstrapped) +``` + +The same information is in `release --json` and `check --json` as a `version_source` object, tagged with a `kind` of `tag`, `file`, `tag_over_file`, `file_over_tag`, or `bootstrap`, so CI can branch on it without parsing prose. The field is additive and optional, and nothing about how versions are resolved has changed: only how much of that resolution you can see. diff --git a/docs/site/changelog/2026-08-25-ferrflow-bot-verified-commits.md b/docs/site/changelog/2026-08-25-ferrflow-bot-verified-commits.md new file mode 100644 index 00000000..0fa0426e --- /dev/null +++ b/docs/site/changelog/2026-08-25-ferrflow-bot-verified-commits.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: release commits from the bot now show as verified' +summary: 'Commits authored by ferrflow[bot] showed as unverified, and no signing key could fix it because a GitHub App cannot register one. With bot: true on GitHub, the release commit is now authored through GitHub itself, which signs it.' +date: 2026-08-25T18:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/941 +docsLink: https://ferrflow.com/docs/configuration/github-action/ +--- + +Release commits made under `bot: true` carried the `ferrflow[bot]` identity but showed as unverified in the GitHub UI, sitting next to everyone else's verified work. There was no configuration that fixed it: GitHub Apps have no setting for a GPG or SSH key, so there was no key for FerrFlow to sign with. The alternative, shipping a signing key into every user's CI, is exactly what you do not want a release tool to ask of you. + +GitHub offers the way out. A commit created through its `createCommitOnBranch` API is signed by GitHub and marked verified, and an App can use it to author commits in its own name. No private key exists anywhere in the chain. With `bot: true` on a GitHub repository, FerrFlow now creates the release commit that way instead of through local git, in both `commit` and `pr` modes. + +One condition comes with it, and it shapes the result. GitHub signs a bot's commit only when the request carries no custom author, committer or signature information. So the commit is attributed to `ferrflow[bot]` and to nothing else. There is no version of this where FerrFlow sets a custom author and still gets the verified mark. + +Nothing changes anywhere else. Without `bot: true`, or on GitLab, Gitea, Forgejo and Bitbucket, the commit is built by local git exactly as before. If you want verified commits under your own identity rather than the bot's, set `commit.gpgsign` and FerrFlow honours it on every forge and on both commit paths. + +Worth knowing: annotated tags are still created and pushed by git, so they are not covered by this. Only the commit is signed. diff --git a/docs/site/changelog/2026-08-25-ferrflow-config-include.md b/docs/site/changelog/2026-08-25-ferrflow-config-include.md new file mode 100644 index 00000000..1f35b381 --- /dev/null +++ b/docs/site/changelog/2026-08-25-ferrflow-config-include.md @@ -0,0 +1,30 @@ +--- +title: 'FerrFlow: one config file per package' +summary: 'A monorepo no longer has to declare every package in a single root config. The new include key points at per-package files, each owning its own settings, and a package path inside one of those files defaults to its directory.' +date: 2026-08-25T12:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/932 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +FerrFlow looked for its config at the repository root and nowhere else, so a monorepo with twenty packages carried one long file that every team edited. The usual consequences followed: merge conflicts on an unrelated package's block, no way to give a project ownership of its own release settings through `CODEOWNERS`, and a file long enough that nobody reads it before changing it. + +The `include` key splits it up. The root config keeps the workspace-level settings and points at the per-package files: + +```json +{ + "workspace": { "versioning": "semver" }, + "include": ["projects/*/ferrflow.json"] +} +``` + +Each included file describes one package, using the same keys as a `package` entry: + +```json +{ "name": "api", "dependsOn": ["core"] } +``` + +Paths inside an included file are relative to that file, and `path` defaults to its directory. The example above needs no `path` at all: `projects/api/ferrflow.json` describes the package in `projects/api`, and renaming or moving that directory requires no edit. Dependencies still refer to packages by name, so `dependsOn` works across files and the release order comes out of the same graph as before. Included files may use a different format than the root config, and they can sit alongside an inline `package` array while a repo migrates. + +Four situations are rejected rather than quietly ignored: an `include` pattern that matches no file, two packages sharing a name, an included file that declares `workspace`, `include`, or `package`, and one that resolves to a path outside the repository. Each of them would otherwise have produced a run that did nothing and exited successfully, which on a release tool is how a team discovers weeks later that nothing shipped. diff --git a/docs/site/changelog/2026-08-25-ferrflow-pr-mode-tags-on-merge.md b/docs/site/changelog/2026-08-25-ferrflow-pr-mode-tags-on-merge.md new file mode 100644 index 00000000..bbe78586 --- /dev/null +++ b/docs/site/changelog/2026-08-25-ferrflow-pr-mode-tags-on-merge.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: release PRs are tagged when they merge, not when they open' +summary: 'In pr mode FerrFlow tagged and published the moment it opened the release PR, on the commit before the version bump. The PR then reported nothing left to release and was never updated again. Tagging now happens after the merge, on the commit that carries the bump.' +date: 2026-08-25T16:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/936 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +`releaseCommitMode: "pr"` is meant to propose a release and let a human accept it. It did not work that way. The run that opened the pull request also pushed the tags and published the GitHub releases, on the current tip of the target branch, which is the commit *before* the version bump. The bump itself lived on the PR branch and was tagged by nothing. + +Two things followed from that. A tag such as `v1.1.0` pointed at a tree whose version files still declared `1.0.0`. And because the versions were already tagged, every later run reported "Nothing to release", so the release PR was never regenerated: it froze at the version it was opened with and quietly stopped tracking the branch. If the PR was never merged, the tags and releases were public anyway, for a release that never happened. + +`pr` mode now runs in the two phases it always described. The proposing run computes the bump, writes the version files and changelog onto the release branch, and opens or updates the PR, without creating a single tag. Every new commit on the target branch regenerates that branch, so the open PR keeps showing the version that would ship right now. The finalising run happens after the merge: FerrFlow finds the `chore(release):` commit on the target branch, sees that the versions it carries have no tags, and tags exactly those before publishing the releases. + +The versions are read from the version files rather than recomputed, which matters more than it sounds. Simply not tagging early would have left the merged release PR looking like fresh unreleased work, so the next run would have proposed another bump on top, and no version would ever have been tagged. Squash merges and merge commits both work, and re-running after a successful release does nothing. + +One limit is worth stating: a package declared without `versionedFiles` has no version to read on the target branch, so it is not finalised this way. Use `commit` mode for tag-only packages. + +Repositories already using `pr` mode are not repaired by the upgrade. Their existing tags sit one commit behind the bump, and any release published for a PR that never merged is still there. Both need removing by hand before the next clean release. diff --git a/docs/site/changelog/2026-08-25-ferrflow-push-error-classification.md b/docs/site/changelog/2026-08-25-ferrflow-push-error-classification.md new file mode 100644 index 00000000..7645e9c5 --- /dev/null +++ b/docs/site/changelog/2026-08-25-ferrflow-push-error-classification.md @@ -0,0 +1,24 @@ +--- +title: 'FerrFlow: a failed push now says what actually went wrong' +summary: 'Every push failure was reported as "pushed onto a stale branch" and answered by regenerating the release commit three times, whatever the real cause. Transient server errors are now retried where they belong, real races are detected by their actual wording, and permanent failures surface immediately.' +date: 2026-08-25T17:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/937 +docsLink: https://ferrflow.com/docs/reference/errors/ +--- + +When a release could not push, FerrFlow said this, three times, before giving up: + +``` +Release attempt 1/3 pushed onto a stale 'main': E2004: Failed to push branch 'main' +Resetting working tree to remote tip and regenerating the release commit against the latest history… +``` + +It said that whatever had happened. The check behind the message matched `E2004`, the generic "push failed" code that rides on every push error, so a GitHub server hiccup, an expired token and a genuine race all produced the same diagnosis. Anyone reading the log went looking for a concurrent push that had never occurred, and a permanent failure such as a revoked permission burned all three attempts before revealing its real cause. + +Two things now decide separately. A transient server-side failure is recognised as such and retried with backoff by the push layer, which is what that layer exists for and where a hiccup is usually resolved on the second attempt. Anything that is neither transient nor a race surfaces immediately with the message git actually produced, no retries and no misleading claim about the branch being stale. + +Races themselves are now detected by what git actually says, which turned out to matter more than expected. The old wording list looked for `non-fast-forward`, but git only says that once you have fetched. Push without fetching first, which is what a release job does, and git says `fetch first` instead. That case had been reaching the retry path through the generic code rather than on its own merit, so tightening the check meant covering it properly, along with `stale info` and the divergent-remote-tag case. + +Nothing to change in your configuration. Release logs get more specific, transient failures stop costing you a release, and permanent ones stop pretending to be something else. diff --git a/docs/site/changelog/2026-08-27-ferrflow-rollback.md b/docs/site/changelog/2026-08-27-ferrflow-rollback.md new file mode 100644 index 00000000..364c7efc --- /dev/null +++ b/docs/site/changelog/2026-08-27-ferrflow-rollback.md @@ -0,0 +1,27 @@ +--- +title: 'FerrFlow: undo a release that failed partway' +summary: 'A release that dies after pushing tags used to leave you reconstructing what it did from a job log, then deleting refs by hand. ferrflow rollback reads the checkpoint that run left behind and undoes exactly what it did, refusing the parts that cannot be undone.' +date: 2026-08-27T12:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/946 +docsLink: https://ferrflow.com/docs/cli/rollback/ +--- + +Releases fail in the middle. A registry answers 401, a docker build breaks, a hook aborts. By then the tags are pushed and the release commit is on the branch, and FerrFlow could only go forward: a checkpoint let you resume, never rewind. Getting back to a clean state meant working out which tags the run had pushed, deleting each one, removing the forge releases by hand, reverting the commit and repairing the manifest. On a monorepo with a cascade that is a lot of state to reconstruct from a job log, under time pressure. + +`ferrflow rollback` does it from the checkpoint instead of from memory: + +```bash +ferrflow rollback # prints the plan, changes nothing +ferrflow rollback --yes # applies it +ferrflow rollback --yes api web +``` + +The dry run is the default, since the destructive form deletes remote refs. + +What makes it safe is mostly what it refuses to do. It never deletes a tag that has moved: every tag is recorded with the commit it pointed at, and one that no longer matches is reported and skipped, because a rollback can happen long after the failure and a tag someone else recreated is not that run's to remove. + +It also stops on any package already published to a registry that cannot be unpublished. crates.io and PyPI keep every version forever, and npm refuses to republish an unpublished one, so deleting the tag would strand a version anyone can still install. Rollback says so and leaves that package alone; the right answer is a new patch version. Docker tags, Helm charts, release assets and webhooks are replaceable and block nothing. In a monorepo where one package published and three did not, the three roll back and the one is refused, rather than half of each being undone. + +The release commit is reverted only when the whole run rolls back with nothing blocked, because that commit carries every package's version bump and reverting it while one package stays released would quietly undo that package's version too. The revert is left for you to push: rollback has just deleted remote refs, and forcing a branch update on top of that is a decision worth taking with the branch in front of you. diff --git a/docs/site/changelog/2026-08-27-ferrflow-workspace-defaults.md b/docs/site/changelog/2026-08-27-ferrflow-workspace-defaults.md new file mode 100644 index 00000000..b71467a7 --- /dev/null +++ b/docs/site/changelog/2026-08-27-ferrflow-workspace-defaults.md @@ -0,0 +1,34 @@ +--- +title: 'FerrFlow: a config without a workspace block now keeps its defaults' +summary: 'Omitting the workspace block from a config file silently dropped every workspace default. The remote became an empty string and the release failed at the push; the target branch became empty and said nothing at all.' +date: 2026-08-27T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/945 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +The `workspace` block is optional, and a config that leaves it out is the shape most people write first: + +```json +{ + "package": [ + { "name": "app", "path": ".", "versionedFiles": [{ "path": "Cargo.toml", "format": "toml" }] } + ] +} +``` + +That config lost every workspace default. The failure people would actually hit came at the very end of a release, after the version bump, the changelog and the tag had all been written: + +``` +error[E2004]: Failed to push branch '' + Remote '' has no URL +``` + +Writing `"workspace": {}` behaved correctly, and omitting the key did not, which is not a distinction anyone would expect to matter. + +The cause was two sets of defaults that disagreed. Each field declared its own fallback for the case where the block is present but the key is missing, while an entirely absent block took a different path that filled everything with empty values. Only the second path was reachable from a minimal config, and only the first was tested. + +The empty remote was the loud symptom. The quieter ones mattered more: the target branch, normally detected from `origin/HEAD` and falling back to `main`, became an empty string that then fed tag lookup and the release branch name without complaint, and auto-merge for release pull requests silently turned itself off despite being documented as on. + +Both paths now produce the same values. Nothing to change in your configuration, and a config that already carried a `workspace` block was never affected. diff --git a/docs/site/changelog/2026-08-28-ferrflow-annotated-tag-deref.md b/docs/site/changelog/2026-08-28-ferrflow-annotated-tag-deref.md new file mode 100644 index 00000000..a530c7f6 --- /dev/null +++ b/docs/site/changelog/2026-08-28-ferrflow-annotated-tag-deref.md @@ -0,0 +1,26 @@ +--- +title: 'FerrFlow: an annotated tag already on the remote no longer blocks the push' +summary: 'FerrFlow compared the commit a tag points at against the tag object itself, so any annotated tag already on the remote looked like it had moved. The push failed with a message about a difference that was not there, telling you to delete a perfectly healthy tag.' +date: 2026-08-28T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/955 +docsLink: https://ferrflow.com/docs/reference/errors/ +--- + +FerrFlow checks the remote before pushing tags, so that a tag someone else already put there at a different commit stops the release instead of being silently overwritten. The check compared the wrong pair of values. + +An annotated tag is two objects: the tag itself, carrying its message, and the commit it points at. FerrFlow read the commit locally and the tag object remotely, then compared them. They are never equal, so any annotated tag already on the remote was reported as diverged: + +``` +E2006: Tag(s) already exist on remote pointing to a different commit: +v1.0.0 (local ebf8a82 != remote ffed861) +``` + +Both refer to the same tag at the same commit. The message names a commit difference that does not exist, and advises deleting a tag that is fine. + +FerrFlow creates annotated tags, so this was the ordinary case rather than an edge one. It stayed hidden because the release planner normally skips a package whose tag already exists, so the push is never attempted. The gap opened when the local view of tags was stale while the remote had the tag, most plausibly after a failed tag fetch at the start of a release, whose error was not surfaced. + +The cause was one refspec. Asking `git ls-remote` for `refs/tags/` does not return the `refs/tags/^{}` entry that carries the commit, so the dereferencing step had nothing to work with. FerrFlow now asks for both. Lightweight tags were never affected, since for them the tag and the commit are the same object, which is also why every existing test in this area missed it. + +Nothing to change on your side. A genuinely divergent tag still stops the release, with the same message, which now means what it says. diff --git a/docs/site/changelog/2026-08-28-ferrflow-cache-staleness.md b/docs/site/changelog/2026-08-28-ferrflow-cache-staleness.md new file mode 100644 index 00000000..276c808c --- /dev/null +++ b/docs/site/changelog/2026-08-28-ferrflow-cache-staleness.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: check no longer serves a stale plan after an uncommitted edit' +summary: 'The cache behind ferrflow check keyed on the commit, the tags and the config, but not on the version files it actually reads. Editing a version without committing returned the previous answer for up to five minutes. Interrupted runs also left temp files that nothing ever cleaned up.' +date: 2026-08-28T16:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/963 +docsLink: https://ferrflow.com/docs/cli/check/ +--- + +`ferrflow check` caches its result for five minutes so repeated previews are instant. The cache key was built from the commit, the tag refs and the config file, which covers everything `check` reads from git. It does not cover everything `check` reads. + +To render `1.0.0 → 1.1.0`, `check` opens the version files on disk. Change one without committing, by hand or through a `release` that wrote files before failing, and none of the three parts of the key move. The old answer came straight back, presented exactly like a fresh one. A wrong answer delivered confidently is worse than a slow one, which is the whole reason to fix a five-minute window on a preview command. + +The key now includes the contents of every configured version file. We hashed contents rather than timestamps and sizes: `1.0.0` and `1.1.0` are the same length, so size alone misses precisely the edit this is meant to catch, and file timestamps are too coarse on some filesystems to be trusted for it. Reading a few small manifests costs far less than the history walk the cache is there to avoid. + +Separately, the cache writes each entry to a temporary file and renames it into place. A run killed between those two steps, a cancelled CI job or a panic, left the temporary file behind, and the cleanup pass only ever looked at finished entries. On a repository with a lot of interrupted runs, `.git/ferrflow-cache/` grew without bound. Leftovers older than an hour are now cleaned up alongside everything else, while a file a write may still be using is left alone. + +Nothing to change on your side. Both fixes are internal to the cache, and `ferrflow cache clear` still empties it outright. diff --git a/docs/site/changelog/2026-08-28-ferrflow-cargo-publish-url.md b/docs/site/changelog/2026-08-28-ferrflow-cargo-publish-url.md new file mode 100644 index 00000000..f7728299 --- /dev/null +++ b/docs/site/changelog/2026-08-28-ferrflow-cargo-publish-url.md @@ -0,0 +1,28 @@ +--- +title: 'FerrFlow: the crates.io link after a publish points at your crate again' +summary: 'FerrFlow built the crates.io link from the package name in its own config rather than the one in Cargo.toml. A workspace routinely uses short names against prefixed crates, and short crate names are long since taken, so the link led to an unrelated crate instead of yours.' +date: 2026-08-28T15:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/962 +docsLink: https://ferrflow.com/docs/configuration/publishers/ +--- + +After publishing to a registry, FerrFlow prints a link to the released version. For cargo it built that link from the package name in your FerrFlow config, not from `[package].name` in the manifest cargo actually publishes under. + +In a workspace those routinely differ, because the config keeps short names while the crates carry a prefix: + +```json +{ "name": "bridge", "path": "crates/bridge", "publishers": [{ "kind": "cargo" }] } +``` + +```toml +[package] +name = "idlewarden-bridge" +``` + +That produced `crates.io/crates/bridge/26.8.27` instead of `crates.io/crates/idlewarden-bridge/26.8.27`. Short crate names have been taken for years, so the link resolved to a real, unrelated crate rather than returning a 404. A log line pointing at somebody else's project as if it were the release that just went out is worse than a missing link, and it is the sort of thing that gets pasted into a release announcement. + +FerrFlow now reads the name from `Cargo.toml`, and falls back to the config name when the manifest cannot be read or carries no `[package]` table. The "already exists on the registry" message uses the real name for the same reason: it makes a claim about the registry, so it has to name the crate the registry knows. Messages about which of your packages failed keep the config name, since that is the one you would look for in your config. + +The publish itself was never affected. `cargo publish` runs in the package directory and cargo reads the manifest itself, so the right crate was always uploaded. This follows the same fix for the npm publisher, where a short config name against a scoped package name caused the identical problem. diff --git a/docs/site/changelog/2026-08-28-ferrflow-npm-publish-url.md b/docs/site/changelog/2026-08-28-ferrflow-npm-publish-url.md new file mode 100644 index 00000000..96f289dc --- /dev/null +++ b/docs/site/changelog/2026-08-28-ferrflow-npm-publish-url.md @@ -0,0 +1,27 @@ +--- +title: 'FerrFlow: the npm link after a publish points at your package again' +summary: 'FerrFlow built the npmjs.com link from the package name in its own config rather than the one in package.json. In a monorepo those differ by default, and the short names are all taken on npm, so the link led to a stranger''s package instead of yours.' +date: 2026-08-28T14:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/961 +docsLink: https://ferrflow.com/docs/configuration/publishers/ +--- + +After publishing to npm, FerrFlow prints a link to the released version. It built that link from the package name in your FerrFlow config, not from the `name` field npm actually publishes under. + +In a monorepo those are rarely the same. The usual shape is a short name in the config and a scoped name in the manifest: + +```json +{ "name": "site", "path": "site", "versionedFiles": [{ "path": "site/package.json", "format": "json" }] } +``` + +```json +{ "name": "@acme/site" } +``` + +That produced `npmjs.com/package/site` instead of `npmjs.com/package/@acme/site`. And because short names like `site`, `api` and `core` are long since taken on npm, the link resolved to somebody else's package rather than returning a 404, which is the kind of wrong that goes unnoticed for a while. + +FerrFlow now reads the name from `package.json`, and falls back to the config name when the manifest cannot be read. The "already exists on the registry" message uses the real name too, for the same reason: it makes a claim about what is on npm, so it has to name the package npm knows. + +The publish itself was never affected. `npm publish` runs inside the package directory and npm reads the manifest itself, so the right thing was always uploaded. Only what FerrFlow printed afterwards was wrong. diff --git a/docs/site/changelog/2026-08-28-ferrflow-panic-cleanup.md b/docs/site/changelog/2026-08-28-ferrflow-panic-cleanup.md new file mode 100644 index 00000000..ffbcad32 --- /dev/null +++ b/docs/site/changelog/2026-08-28-ferrflow-panic-cleanup.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: a crash no longer leaves the release lock or a registry token behind' +summary: 'The release binary is built to abort on panic, which skips the cleanup that runs when a release ends normally. A crash left the lock file in place, blocking the repository for half an hour, and could leave a scoped npm token on disk.' +date: 2026-08-28T18:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/964 +docsLink: https://ferrflow.com/docs/cli/release/ +--- + +FerrFlow takes a lock while a release runs, so two releases cannot start at once in the same repository, and writes a temporary npm config when a registry needs a token. Both are removed when the run ends, whether it succeeded or failed. + +Neither was removed when the process crashed. The release binary is compiled to abort immediately on a panic rather than unwind, a deliberate choice that cuts the binary by about a quarter, and aborting skips the cleanup step that normally runs as each value goes out of scope. The consequence had not been followed through. + +So a crash mid-release left the lock file in place. The next release refused to start, and since a stale lock is only taken over after a timeout, the repository stayed blocked for half an hour with nothing explaining why. A crash during an npm publish could likewise leave a file containing a registry token in the system temporary directory until the operating system got around to clearing it. + +FerrFlow now keeps a list of files that must not outlive the process and removes them from a panic handler, which does still run when the binary aborts. The normal path is unchanged: whatever was already cleaned up is taken off the list rather than deleted twice. + +One gap remains, and it is worth knowing about: interrupting a local release with Ctrl-C still leaves the lock behind, for the same underlying reason. Handling that needs machinery we did not want to add without weighing it separately. If it happens, `ferrflow release --force-unlock` takes the lock back immediately. diff --git a/docs/site/changelog/2026-08-29-ferrflow-calver-short-seq.md b/docs/site/changelog/2026-08-29-ferrflow-calver-short-seq.md new file mode 100644 index 00000000..fdc208ad --- /dev/null +++ b/docs/site/changelog/2026-08-29-ferrflow-calver-short-seq.md @@ -0,0 +1,31 @@ +--- +title: 'FerrFlow: calver-short-seq, and a warning when a release is held back' +summary: 'A date-only version scheme can only publish once a day, and the second attempt was indistinguishable from having nothing to release. FerrFlow now says so out loud, and calver-short-seq gives the compact year a sequence counter.' +date: 2026-08-29T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/967 +docsLink: https://ferrflow.com/docs/configuration/versioning-strategies/ +--- + +A version made only of a date can be produced once a day, by definition. Under `calver-short`, the second release of the day recomputes the same `YY.M.D`, the version does not move, and FerrFlow declines to publish. That part is inherent to the scheme rather than a defect. + +What was a defect is that it looked exactly like having nothing to release. Both printed "Nothing to release", so a merged fix could sit unpublished until the next day with nobody told. The only way to notice was to find later that a deployed image was missing a change that had been merged hours earlier. + +FerrFlow now names it: + +``` +! app has releasable commits but stays at 26.8.29, so nothing was published +``` + +That line appears whenever there is releasable work and the strategy produces the version that is already out, whatever the strategy. It is worth having even for repositories that never hit the daily limit, since the same silence covered any misconfiguration that froze the version. + +The other half is a way out. `calver-seq` already pairs the full year with a counter, `YYYY.M.SEQ`, but there was no equivalent for the compact year, so a repository on `calver-short` had nowhere to go at constant format. `calver-short-seq` fills that in with `YY.M.SEQ`: + +```json +{ "workspace": { "versioning": "calver-short-seq" } } +``` + +Switching to it costs nothing in version continuity, which is the reason to prefer it over `calver-seq` here. The counter picks up from whatever the last release was, so a repository whose most recent version was `26.8.29` gets `26.8.30` next, then `26.8.31` the same day. Moving to `calver-seq` instead would work, but it jumps the leading number from `26` to `2026`, which is irreversible and makes the year ambiguous when reading older tags alongside newer ones. + +Set the strategy explicitly if you use it. `YY.M.SEQ` and `YY.M.D` are the same shape, and a value like `26.8.29` is a plausible day as well as a plausible counter, so tag detection cannot tell them apart on its own. diff --git a/docs/site/changelog/2026-08-29-ferrflow-scoped-package-commits.md b/docs/site/changelog/2026-08-29-ferrflow-scoped-package-commits.md new file mode 100644 index 00000000..1af30fe3 --- /dev/null +++ b/docs/site/changelog/2026-08-29-ferrflow-scoped-package-commits.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: a package release no longer carries other packages'' commits' +summary: 'In a monorepo, each package collected every commit since its own last tag, whatever those commits touched. That padded the changelog with other packages'' work and, less visibly, let their commits decide the version bump.' +date: 2026-08-29T12:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/974 +docsLink: https://ferrflow.com/docs/configuration/monorepo/ +--- + +Path prefixes decide which package a commit belongs to. That is what the documentation says, and it is how FerrFlow decided whether a package had anything to release. It was not how FerrFlow decided what that release contained. + +Each package took every commit since its own last tag. A repository with a site and a server would publish the site with the server's refactors listed underneath it, because those commits sat in the window even though nothing in them touched the site. + +`ferrflow diff` was fixed to scope its range by path back in August, on the understanding that a release already worked that way. It did not, so from that point until now the preview and the release disagreed with each other, and the preview was the one telling the truth. They agree again. + +The visible half is a changelog claiming work that shipped elsewhere. The half worth knowing about is the version. The bump is the strongest change in that list, so a `feat` anywhere in the repository turned every package released alongside it into a minor. A one-line fix to a site could go out as `1.1.0` because a server gained an endpoint the same week. + +Releases now carry only the commits that touch the package, matching `path` and any `sharedPaths` it declares. Nothing to configure, and `sharedPaths` behaves as before: a commit under a shared directory belongs to every package that declares it, and to no other. + +Expect quieter changelogs and, in mixed repositories, smaller version bumps than you have been getting. The bumps you were getting were wrong, so this is worth a look if you have release automation keyed to major or minor. + +The effect is largest with `recoverMissedReleases` enabled, since recovery reaches back to the last tag rather than the last release, and everything the other packages did in between came along. diff --git a/docs/site/changelog/2026-09-01-ferrflow-cascade-strongest-bump.md b/docs/site/changelog/2026-09-01-ferrflow-cascade-strongest-bump.md new file mode 100644 index 00000000..524f9f5b --- /dev/null +++ b/docs/site/changelog/2026-09-01-ferrflow-cascade-strongest-bump.md @@ -0,0 +1,25 @@ +--- +title: 'FerrFlow: a dependent now takes the strongest bump reaching it' +summary: 'A package fed by two dependency edges of different strength took whichever arrived first, so it could ship as a patch while a package it depends on moved a minor. Expect larger bumps if your graph has a diamond in it.' +date: 2026-09-01T18:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/988 +docsLink: https://ferrflow.com/docs/configuration/monorepo/ +--- + +When a release bumps a package, FerrFlow walks the dependency graph and bumps what depends on it. Each edge decides how much of the bump it passes on, through `propagate`. + +A package sitting at the end of two edges of different strength took whichever reached it first, not the strongest. Say `web` depends on `shared` with `propagate: patch` and on `api` with the default. A feature on `shared` gives `api` a minor, and `web` should follow with a minor through that edge. It shipped as a patch instead, because the capped edge from `shared` is the shorter path and got there first: + +``` +web 1.0.0 → 1.0.1 (patch, dependency: shared, api) +``` + +The line already named both edges, which is what makes it awkward: FerrFlow knew about the minor and did not use it. Within a single step it always took the strongest of the edges available. The rule only broke down across steps. + +It now settles: the walk repeats until no package can be improved, so the same release produces `web 1.0.0 → 1.1.0`. + +**Some releases get larger.** If your graph has a diamond, a package fed by a strong and a weak path will now take the strong one, where before it depended on which path was shorter. Worth a look before upgrading if you have automation keyed to major or minor. Repositories with a straight dependency chain and no diamonds are unaffected, since there was never a second bump competing. + +`ferrflow graph --impact` follows the same rule, so you can see what a release would now do before running one. diff --git a/docs/site/changelog/2026-09-01-ferrflow-changelog-hook-rewrite.md b/docs/site/changelog/2026-09-01-ferrflow-changelog-hook-rewrite.md new file mode 100644 index 00000000..425f3e47 --- /dev/null +++ b/docs/site/changelog/2026-09-01-ferrflow-changelog-hook-rewrite.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: a changelog your hook rewrote is now what gets published' +summary: 'A `postBump` hook could already rewrite CHANGELOG.md and have it committed, but the tag, the release body and the release commit kept the text generated before the hook ran. They now agree.' +date: 2026-09-01T16:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/982 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +`post_bump` runs after the changelog section is written and receives it in `FERRFLOW_CHANGELOG`. A hook can rewrite `CHANGELOG.md`, and FerrFlow stages the result into the release commit. That much already worked. + +What did not is everything else that quotes the changelog. The section is built before the hook runs, and that earlier value fed the git tag, the forge release body and the release commit message. So a hook that turned commit subjects into prose gave you a file that read well and a Releases page that still said `feat(auth): support passkeys`. Same release, two texts, and the one people actually read was the raw one. + +FerrFlow now reads the section back from the file after the hook, so all four carry the same thing. + +If a rewrite loses the `## [version]` heading, the generated text is published instead. A hook that mangles the changelog should not also empty the release notes. + +This closes the gap for anyone wanting human-written release notes without FerrFlow taking a position on how they are produced. Whether the hook calls a model, runs a script, or reformats with `sed` is not FerrFlow's business, and keeping it that way means no API key passes through the release tool, nothing breaks in air-gapped CI, and the result stays reproducible if your rewriter is. + +One thing to know: none of this runs under `--dry-run`, which writes no changelog. Preview it with a real release on a branch. diff --git a/docs/site/changelog/2026-09-01-ferrflow-doctor-lockfile.md b/docs/site/changelog/2026-09-01-ferrflow-doctor-lockfile.md new file mode 100644 index 00000000..0de9033f --- /dev/null +++ b/docs/site/changelog/2026-09-01-ferrflow-doctor-lockfile.md @@ -0,0 +1,36 @@ +--- +title: 'FerrFlow: doctor now catches a lockfile that disagrees with its manifest' +summary: 'A package whose manifest and lockfile record different versions cannot build with `--locked`, and `ferrflow doctor` used to report it as healthy. It now says so, and warns before the drift happens.' +date: 2026-09-01T14:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/981 +docsLink: https://ferrflow.com/docs/reference/cli/ +--- + +A release bumps the version in your manifest. If the lockfile beside it is not bumped too, cargo stops building: + +``` +error: cannot update the lock file because --locked was passed to prevent this +``` + +`doctor` had nothing to say about this. It read the manifest, printed the version it found, and moved on. A repository could sit in that state for days with a green-looking report while every `--locked` build failed. + +It now reports the disagreement: + +``` +✓ api: v2026.8.1 +⚠ api lockfile: api/Cargo.lock records v6.1.0 while api/Cargo.toml says v2026.8.1; builds passing --locked will refuse to run +``` + +The second check is the one worth acting on, because it fires before anything breaks. `updateLockfiles` is off by default, and a repository with a lockfile sitting next to a managed manifest and the flag unset is not broken yet, it is simply waiting for the next release to break it: + +``` +⚠ api lockfile: api/Cargo.lock is not updated on release; set updateLockfiles so the next bump does not leave it behind +``` + +With the flag on and the versions agreeing you get a plain `✓`, and a package with no lockfile is not mentioned. + +Version comparison applies to `Cargo.lock`. pnpm, yarn, poetry, uv, bundler and mix lock dependencies without restating the version of the package that owns them, so a mismatch of this shape cannot occur there and there is nothing to compare. Those still get the `updateLockfiles` warning, which is about the setting rather than the file. + +Nothing to change on your side. `doctor` stays read-only and runs no package manager, so it works on a machine with no toolchain installed and no registry access. diff --git a/docs/site/changelog/2026-09-01-ferrflow-graph-impact.md b/docs/site/changelog/2026-09-01-ferrflow-graph-impact.md new file mode 100644 index 00000000..55d7b924 --- /dev/null +++ b/docs/site/changelog/2026-09-01-ferrflow-graph-impact.md @@ -0,0 +1,34 @@ +--- +title: 'FerrFlow: see what a release drags along before you run it' +summary: 'In a monorepo, releasing a widely depended-on package pulls others out with it. `ferrflow graph --impact` names them, with the version each would get, before anything is tagged.' +date: 2026-09-01T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/979 +docsLink: https://ferrflow.com/docs/reference/cli/ +--- + +Releasing a package that others depend on releases those too. FerrFlow has always done this, and `ferrflow graph` has always printed the topology it works from, but the two never met: you could read the graph and work out the consequences yourself, or run the release and find out. + +`--impact` answers it directly: + +```bash +ferrflow graph --impact shared +``` + +``` +shared 1.0.0 → 1.1.0 (minor, assumed) + +would also release + api 1.0.0 → 1.1.0 minor bump, depends on shared + web 1.0.0 → 1.0.1 patch bump, depends on shared + mobile 1.0.0 → 1.1.0 minor bump, through api (2 deep) +``` + +The bump is assumed rather than read from your commits, which is the point: the useful question is usually hypothetical. `--bump major` shows what a breaking change would cost before you write it. + +Three things it accounts for that are easy to get wrong on a whiteboard. A `dependsOn` entry can set `propagate` to `none`, which keeps that dependent out entirely, or to `patch`, which caps what the edge passes on, so `web` above takes a patch from the same upstream that gives `api` a minor. Members of a `linked` or `fixed` group come along with no dependency edge between them at all, which is the one most often forgotten. And transitive dependents carry their distance, so `mobile` reads as reached through `api` rather than directly. + +A preview is only worth having if it matches what happens, so this shares its propagation with the release rather than reimplementing it. The rule the cascade applies each round was extracted and both call it, which means the two cannot drift apart as the logic changes. + +`--json` works here as it does for the graph, if you want to gate a workflow on the answer rather than read it. diff --git a/docs/site/changelog/2026-09-01-ferrflow-own-bump-raised.md b/docs/site/changelog/2026-09-01-ferrflow-own-bump-raised.md new file mode 100644 index 00000000..13362957 --- /dev/null +++ b/docs/site/changelog/2026-09-01-ferrflow-own-bump-raised.md @@ -0,0 +1,25 @@ +--- +title: 'FerrFlow: your own commits set the floor for a bump, not the answer' +summary: 'A package with a fix of its own shipped as a patch even when a dependency in the same release took a minor. It now takes the stronger of the two. Expect larger bumps where a shared package and its consumers are released together.' +date: 2026-09-01T20:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/991 +docsLink: https://ferrflow.com/docs/configuration/monorepo/ +--- + +An earlier fix today made a package take the strongest bump reaching it through the dependency graph. That covered packages with nothing of their own to release. A package that had its own commits kept whatever those commits implied, however far its dependencies moved. + +So a `fix` in `web`, released alongside an `api` that took a minor, shipped as a patch: + +``` +● shared 1.0.0 → 1.1.0 (minor) +● api 1.0.0 → 1.1.0 (minor, dependency: shared) +● web 1.0.0 → 1.0.1 (patch) +``` + +`web` depends on `api`. Under the default `propagate: same`, a minor in `api` is a minor for `web`, and the fix of its own does not make that smaller. Its commits set the floor. The same release now produces `web 1.0.0 → 1.1.0`. + +The changelog is unchanged in content: `web` still lists its own commit, because that is still what it did. Only the version moved further. + +**This reaches more releases than the earlier fix.** That one only touched packages with no commits of their own. This one applies to any package that has commits and a dependency moving in the same release, which in most monorepos is the common case rather than the edge one. If you release a shared library and its consumers together, expect the consumers to move up. Set `propagate` on the dependency to `patch` or `none` where that is not what you want. diff --git a/docs/site/changelog/2026-09-02-ferrflow-galaxyyml.md b/docs/site/changelog/2026-09-02-ferrflow-galaxyyml.md new file mode 100644 index 00000000..f4ae1c1c --- /dev/null +++ b/docs/site/changelog/2026-09-02-ferrflow-galaxyyml.md @@ -0,0 +1,25 @@ +--- +title: 'FerrFlow: Ansible Galaxy collections get a version file format' +summary: '`galaxy.yml` carries a top-level `version:` like `Chart.yaml` and `pubspec.yaml` do, but versioning one meant falling back to the plain-text format and a hand-written regex. It is a first-class format now.' +date: 2026-09-02T22:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/999 +docsLink: https://ferrflow.com/docs/configuration/formats/ +--- + +An Ansible collection declares its version in `galaxy.yml`, the same way a Helm chart declares it in `Chart.yaml`. FerrFlow handled the second and not the first, so a collection had to use the `txt` format and a regex written by hand, which works until someone reformats the file. + +`galaxyyml` reads and writes that key directly: + +```json +{ "path": "galaxy.yml", "format": "galaxyyml" } +``` + +`galaxy.yml` is also detected on its own now, so a repository with nothing but a collection manifest gets a working config without naming the format at all. + +Everything around the version is left alone. The write replaces the version and nothing else, so `namespace`, `name`, `readme` and any comments come out byte-identical. A `version:` nested under another key, the way a dependency constraint is, is not touched. + +Ansible was the one ecosystem gap in a list that already covered Rust, Python, Node, PHP, Java, .NET, Kubernetes, Dart, Elixir, Ruby, Swift, Haskell, C and Go. + +Publishing is still yours to run. This versions the manifest; it does not call `ansible-galaxy collection publish`. diff --git a/docs/site/changelog/2026-09-03-ferrflow-docs-public.md b/docs/site/changelog/2026-09-03-ferrflow-docs-public.md new file mode 100644 index 00000000..430fcc6d --- /dev/null +++ b/docs/site/changelog/2026-09-03-ferrflow-docs-public.md @@ -0,0 +1,17 @@ +--- +title: 'FerrFlow: the documentation is now open to pull requests' +summary: 'The pages behind ferrflow.com/docs moved to a public repo. A wrong flag name, a stale example or a paragraph that never quite landed can now be fixed by anyone, and the fix goes live when it merges.' +date: 2026-09-03T09:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow-Cloud/pull/908 +docsLink: https://github.com/FerrLabs/FerrFlow-Docs +--- + +The FerrFlow CLI has been open source since the start, but its documentation was not. The pages lived in the private repo that builds ferrflow.com, so spotting a mistake in them and being able to fix it were two very different things. + +They now live in [FerrLabs/FerrFlow-Docs](https://github.com/FerrLabs/FerrFlow-Docs), which is public and holds markdown and nothing else. Every page served under `/docs`, current and frozen, is in there under `content/`. + +To fix something, open a pull request against that repo. The site fetches it at build time, so merging is the whole publication step and there is nothing to do afterwards. + +The frozen per-version snapshots came along too, and they stay frozen. `content/docs-v5/` records what FerrFlow v5 actually documented, mistakes included, because anyone still pinned to v5 reads those pages to understand the binary they are running. CI rejects a pull request that edits one and points you at the current docs instead. diff --git a/docs/site/changelog/2026-09-04-ferrflow-build-metadata.md b/docs/site/changelog/2026-09-04-ferrflow-build-metadata.md new file mode 100644 index 00000000..b91cbd9d --- /dev/null +++ b/docs/site/changelog/2026-09-04-ferrflow-build-metadata.md @@ -0,0 +1,27 @@ +--- +title: 'buildMetadata: derive part of the version from your code' +summary: 'A new workspace field naming a command whose output is appended to the version after a plus. The suffix lands on the version files and stops there, so the tag and the changelog keep the plain version.' +date: 2026-09-04T20:00:00Z +product: ferrflow +type: new +draft: true +prLink: https://github.com/FerrLabs/FerrFlow/pull/1022 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +Some versions carry a fact the commits do not know. A Minecraft server advertises the protocol range it speaks, a fork records the upstream revision it was cut from, a firmware build stamps the toolchain that produced it. Semver has a slot for exactly this, the `+suffix` at the end, and until now FerrFlow had no way to fill it: the suffix had to be reattached by a `postBump` hook that reopened the manifest FerrFlow had just written. + +`workspace.buildMetadata` names a command instead. FerrFlow runs it once per release from the repository root, before writing anything, and appends its trimmed stdout to the version. + +```toml +[workspace] +buildMetadata = "sh scripts/protocol-versions.sh" +``` + +If the script prints `26.2-26.45`, the manifest reads `1.4.0+26.2-26.45`. + +**The suffix goes on the version files and nowhere else.** The tag is `v1.4.0`, the changelog heading is `1.4.0`, and the minor after it is computed from `1.4.0`. That is not a shortcut, it is what semver says: build metadata is excluded from a version's identity and ignored when comparing precedence, so `1.4.0+a` and `1.4.0+b` are the same release. A version that has already been stamped round-trips correctly, so `1.4.0+26.2-26.45` bumps to `1.5.0` and gets stamped afresh. + +The output has to be dot-separated alphanumerics and hyphens, which is the full grammar semver allows after the `+`. A command that fails, prints nothing, or prints anything outside that set aborts the release rather than writing a manifest no parser will accept. Under `--dry-run` the command is printed and not executed, in line with how hooks behave. + +Reach for it when the derivation is a shell one-liner or a script you already have. If you were doing this with a `postBump` hook, the hook can now shrink to the part that computes the value. diff --git a/docs/site/changelog/2026-09-22-ferrflow-package-schema.md b/docs/site/changelog/2026-09-22-ferrflow-package-schema.md new file mode 100644 index 00000000..0a650240 --- /dev/null +++ b/docs/site/changelog/2026-09-22-ferrflow-package-schema.md @@ -0,0 +1,31 @@ +--- +title: 'FerrFlow: a schema for per-package config files' +summary: 'Files pulled in through include now have their own JSON schema, so editors autocomplete and validate them like the root config. It is served at ferrflow.com/schema/ferrflow-package.json and bundled in the binary behind ferrflow schema --package.' +date: 2026-09-22T08:07:34Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/1127 +docsLink: https://ferrflow.com/docs/reference/cli/ +--- + +The `include` key lets a monorepo keep one config file per package, but those files had no schema. Pointing their `$schema` at `ferrflow.json` did not help: that schema describes the root config, with `workspace`, `include` and a `package` array, so an editor flagged every key of a single package as unexpected and offered no completion at all. + +Since FerrFlow 7.23.0 an included file has a schema of its own: + +```json +{ + "$schema": "https://ferrflow.com/schema/ferrflow-package.json", + "name": "api", + "dependsOn": ["core"] +} +``` + +It accepts exactly the keys of a `package` entry, with `name` required and `path` optional, since `path` defaults to the file's directory. Anything else is rejected, so a misspelled key shows up in the editor before FerrFlow ever runs. The two schemas are checked against each other in FerrFlow's test suite, so the package schema cannot fall behind when a new package field lands. + +For repos that would rather not fetch it, the same schema ships inside the binary: + +```bash +ferrflow schema --package --pretty --output ferrflow-package.schema.json +``` + +Then point `$schema` at the local file. diff --git a/docs/site/changelog/2026-09-22-ferrflow-push-token-per-forge.md b/docs/site/changelog/2026-09-22-ferrflow-push-token-per-forge.md new file mode 100644 index 00000000..d477b0ee --- /dev/null +++ b/docs/site/changelog/2026-09-22-ferrflow-push-token-per-forge.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: GITHUB_TOKEN only reaches remotes that are GitHub' +summary: 'A push to a remote FerrFlow could not identify used to carry GITHUB_TOKEN as its password, so a self-hosted forge or an unknown host received a GitHub credential. Each forge now pushes with its own token, and an unrecognised host gets none.' +date: 2026-09-22T18:36:17Z +product: ferrflow +type: security +prLink: https://github.com/FerrLabs/FerrFlow/pull/1152 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +When FerrFlow pushed the release commit and tags, it picked the credential by asking one question: is this remote GitLab? Everything else got `GITHUB_TOKEN`. That covers GitHub, but also a self-hosted Gitea or Forgejo, Bitbucket, and any host whose forge could not be identified. Every GitHub Actions job has `GITHUB_TOKEN` in its environment, so a remote pointing somewhere else received a GitHub credential as its push password. + +Each forge now pushes with the token it actually uses: `GITHUB_TOKEN` for GitHub, `GITLAB_TOKEN` for GitLab, `GITEA_TOKEN` or `FORGEJO_TOKEN` for Gitea and Forgejo, `BITBUCKET_TOKEN` for Bitbucket. These are the same variables the API side has always read, so the two halves can no longer disagree. `FERRFLOW_TOKEN` still works everywhere: it is the explicit "use this for whatever this remote is". + +A host FerrFlow cannot identify now gets no forge token at all. If `GITHUB_TOKEN` is set and withheld, the run says so once: + +``` +Warning: not sending GITHUB_TOKEN to git.corp.example, which is not recognised as GitHub. Set `forge` in the config if it is a GitHub Enterprise instance, or pass the token as FERRFLOW_TOKEN. +``` + +Two things to check if you self-host. A GitHub Enterprise instance that FerrFlow cannot reach to identify needs `"forge": "github"` under `workspace` in your config. Gitea and Forgejo Actions export their job token under the name `GITHUB_TOKEN`, so those jobs keep pushing for now, with a warning: pass it as `GITEA_TOKEN` instead, because that fallback goes away in the next major version, and creating the release already reads `GITEA_TOKEN` or `FORGEJO_TOKEN` rather than falling back. + +Identifying a self-hosted host got stricter in the same pass. A GitHub Enterprise instance is recognised only by the `X-GitHub-Enterprise-Version` header its API sends, not by anything answering `/api/v3`, and the probe no longer follows redirects, so one host can no longer borrow another's answer to be handed a token. An instance in private mode, which answers 401 without a session, is now detected where it was missed before. The trade-off is that an instance which redirects its own API root is no longer detected either, and takes the same `forge` setting as above. diff --git a/docs/site/changelog/2026-09-22-ferrflow-release-git-identity.md b/docs/site/changelog/2026-09-22-ferrflow-release-git-identity.md new file mode 100644 index 00000000..0009089e --- /dev/null +++ b/docs/site/changelog/2026-09-22-ferrflow-release-git-identity.md @@ -0,0 +1,18 @@ +--- +title: 'FerrFlow: releases on GitHub Actions no longer need a git identity' +summary: 'A release using GITHUB_TOKEN without bot mode wrote the version files and changelog, then failed on the release commit with "Author identity unknown". On Actions it now commits as github-actions[bot]; elsewhere it stops before touching anything.' +date: 2026-09-22T20:00:00Z +product: ferrflow +type: fix +draft: true +prLink: https://github.com/FerrLabs/FerrFlow/pull/1128 +docsLink: https://ferrflow.com/docs/ci/github-actions/ +--- + +Until now, FerrFlow only set a git author in bot mode. A workflow calling the action with `GITHUB_TOKEN` and no `bot: true` got as far as writing the version files and the CHANGELOG, and then `git commit` failed with `Author identity unknown`. The job left a dirty tree behind and no release. + +A release now checks for an identity before it bumps anything, and it checks the same way `git commit` does. Anything git itself would accept still counts: a configured `user.name` and `user.email`, the `GIT_AUTHOR_*` variables, or an identity git works out on its own. + +If there is none and the job runs on GitHub Actions, FerrFlow commits and tags as `github-actions[bot]`, the account a `GITHUB_TOKEN` push is credited to anyway. Your workflow needs no changes, and a `git config` step you already have keeps working. Anywhere else, the release stops at once with the two `git config` commands to run, and nothing is written. + +`ferrflow doctor` also has a new `git identity` check, so you can catch this before a first release. diff --git a/docs/site/changelog/2026-09-23-ferrflow-release-mr-title-length.md b/docs/site/changelog/2026-09-23-ferrflow-release-mr-title-length.md new file mode 100644 index 00000000..b3e3baec --- /dev/null +++ b/docs/site/changelog/2026-09-23-ferrflow-release-mr-title-length.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: the release pull request title now fits the forge' +summary: 'A monorepo releasing enough packages at once built a title longer than the 255 characters a merge request accepts, so the release PR was never opened and the run still exited green. The title is capped, the full list stays in the description, and a rejected PR now fails the release.' +date: 2026-09-23T09:12:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1175 +docsLink: https://ferrflow.com/docs/ci/pipeline-triggers/ +--- + +In `releaseCommitMode: pr`, the pull request title is `chore(release): ` followed by every package being released. Nothing bounded it, and forges do: 255 characters on GitLab, Gitea and Forgejo, 256 on GitHub. A monorepo releasing five packages with long names crosses that line, GitLab answers 400, and the release PR is never opened. + +The title is now capped at 255, the lowest of the four, so one title works on every forge. When the list does not fit, FerrFlow keeps as many packages as it can and ends the title with `and N more`. Nothing is lost: the description has always listed every tag on its own line, and still does. + +The second half of this is the part you could not see. A release pull request that never got opened only produced a warning, so the branch was pushed, no PR existed, and `ferrflow release` exited 0 with CI green. That is now an error, because a PR-mode release that opened no pull request has released nothing. It covers the forge rejecting the PR, the lookup for an existing one failing, and FerrFlow not reaching a forge at all. Check that last one before you upgrade: a release job with a missing token pushed the branch and said nothing at all, and will now fail. Gitea, Forgejo and Bitbucket stay on a warning only where the forge itself refuses the pull request, since PR mode is not implemented there yet; an unreachable forge fails there too, because a missing token is caught before the forge is known. + +The error also says what the forge said. FerrFlow used to drop the response body and report only `failed to created MR: E3102`, which left you guessing. It now reads: + +``` +Failed to create MR from ferrflow/release-main to main: http status 400: {"message":["Title is too long (maximum is 255 characters)"]} +``` diff --git a/docs/site/changelog/2026-09-24-ferrflow-selector-padding.md b/docs/site/changelog/2026-09-24-ferrflow-selector-padding.md new file mode 100644 index 00000000..49817a8e --- /dev/null +++ b/docs/site/changelog/2026-09-24-ferrflow-selector-padding.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: a txt selector no longer eats the spaces around your version' +summary: 'A versioned file read through a txt selector whose capture included padding lost all of that padding on the first release, because the write replaced the whole capture. It now replaces only what the read returned.' +date: 2026-09-24T12:05:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1190 +docsLink: https://ferrflow.com/schema/ferrflow.json +--- + +A `txt` versioned file can name a `selector`, a regex with one capture group around the version. The write replaced the whole of that capture, so any padding the regex swallowed went with it: `version = 1.2.3` with `(?m)^version =(.+)$` came back as `version =2.0.0`. + +It goes all at once, on the first release, and every release after that writes into a line that has already been stripped. The selector still matches a line that lost its space, so nothing ever reported it. + +Both sides now work from the same trimmed range: the write replaces exactly what the read returned and leaves the rest of the line alone. If your selector captures padding, the padding stays where you put it. + +Two smaller things come with it. A selector whose capture is only whitespace is now an error instead of an empty version on its way to a tag name, and a selector whose group never participates, which an optional group like `(?m)^version(?: =(.*))?$` allows, now fails with `E4702` and a link to the docs rather than a bare message. + +This came out of the review of the read-side trim rather than a report, so you are unlikely to have chased it: until that trim landed, a padded capture also produced a padded version, and the version is what you would have noticed first. diff --git a/docs/site/changelog/2026-09-24-ferrflow-template-leading-v.md b/docs/site/changelog/2026-09-24-ferrflow-template-leading-v.md new file mode 100644 index 00000000..42152145 --- /dev/null +++ b/docs/site/changelog/2026-09-24-ferrflow-template-leading-v.md @@ -0,0 +1,19 @@ +--- +title: 'FerrFlow: a versionTemplate starting with v no longer resets on every release' +summary: 'A versionTemplate whose first literal was v never read the previous version back, so every release started again from zero. v1.2.3 with a patch bump produced v0.0.1, release after release.' +date: 2026-09-24T16:20:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1199 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +To bump a version, FerrFlow reads the previous one back through the same template that produced it. That read stripped a leading `v` from the current version before matching, which is right when the template is bare and the version came from a tag, and wrong when the template carries the `v` itself: the pattern then asked for a `v` that had just been removed, and nothing matched. + +With no previous version to read, every number fell back to zero. `v1.2.3` with a patch bump became `v0.0.1`, and the release after that became `v0.0.1` again. A `{seq}` reset the same way: `v2026.8.3` became `v2026.8.1`. + +`v{major}.{minor}.{patch}` is the first template most people write, so if you use one and your versions never seemed to climb, this is why. + +The version is now matched as written first, and only falls back to stripping the `v` when that fails. Both shapes work: a template carrying its own prefix, and a bare template reading a tag-shaped version. + +Nothing to change on your side. The next release after upgrading reads your current version correctly and continues from it. diff --git a/docs/site/changelog/2026-09-24-ferrflow-template-literals.md b/docs/site/changelog/2026-09-24-ferrflow-template-literals.md new file mode 100644 index 00000000..c1b2548f --- /dev/null +++ b/docs/site/changelog/2026-09-24-ferrflow-template-literals.md @@ -0,0 +1,21 @@ +--- +title: 'FerrFlow: versionTemplate now tells you about a bad literal instead of git' +summary: 'A versionTemplate whose literal text carried a space, a tab or a character git reserves produced a version that could not become a tag, and the run died at git tag with a message about ref names. The template is now checked where it is read.' +date: 2026-09-24T13:40:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1194 +docsLink: https://ferrflow.com/schema/ferrflow.json +--- + +`versionTemplate` checked that every `{variable}` was one it knows, and let the literal text between them through untouched. So a template with a stray space or tab produced a version carrying it, and that version went on to be a git tag, which cannot hold either. + +The release failed, but at the end, with git's complaint about the ref name rather than ours about the template. You were told the tag was bad, which is true and useless. + +The literals are now checked when the template is read, by `ferrflow check` and by the release alike. They may not carry whitespace, control characters, or the characters git reserves in a ref name (`~ ^ : ? * [ \`), and the error names the one it found: + +``` +versionTemplate "{major}.{minor}\t" has a control character ('\t') in a literal. The version becomes a git tag, which cannot carry it. +``` + +Nothing that worked before stops working. A template rejected today already failed, later and less clearly. If `ferrflow check` starts complaining after this upgrade, it is telling you about a release that was going to break. diff --git a/docs/site/changelog/2026-09-28-ferrflow-floating-tag-backward.md b/docs/site/changelog/2026-09-28-ferrflow-floating-tag-backward.md new file mode 100644 index 00000000..7897c44d --- /dev/null +++ b/docs/site/changelog/2026-09-28-ferrflow-floating-tag-backward.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: a hotfix no longer drags a floating tag backward' +summary: 'Releasing 1.4.2 after 1.5.0 silently moved v1 back to 1.4.2. The guard meant to stop that never fired. It does now: the release stops before anything is pushed, and --force lets the alias move.' +date: 2026-09-28T10:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1226 +--- + +Floating tags such as `v1` or `v1.5` are supposed to only move forward: when a release on an older line would point them at a lower version, FerrFlow stops the release, and only `--force` lets the alias move backward, with a warning. That check read the version from the alias tag's message, which git hands back with a trailing newline, so parsing it failed and the check concluded there was nothing to protect. + +In practice, cutting a hotfix on a maintenance branch moved the major alias back to the hotfix, and anyone pinned to `v1` got the older code. The check now works as documented, which changes what a hotfix pipeline sees: a job that used to succeed on a maintenance line now fails at `ferrflow release` with "Floating tag v1 would move backward", before anything is pushed. If you do want the alias to follow the hotfix, add `--force` to that job. diff --git a/docs/site/changelog/2026-09-28-ferrflow-helm-chart-name.md b/docs/site/changelog/2026-09-28-ferrflow-helm-chart-name.md new file mode 100644 index 00000000..de25a653 --- /dev/null +++ b/docs/site/changelog/2026-09-28-ferrflow-helm-chart-name.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: the Helm publisher reads the right chart name' +summary: 'In a Chart.yaml listing dependencies or maintainers before the chart name, the Helm publisher could take a dependency name for the chart and check or push the wrong reference. Only the top-level name key counts now.' +date: 2026-09-28T10:10:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1231 +--- + +The `helm` publisher reads the chart name from `Chart.yaml` to build the OCI reference it checks and pushes. It matched the first `name:` on any line, indented or not, so a chart whose `dependencies:` or `maintainers:` block came before its own `name:` was treated as the dependency: the "already published" check looked at the wrong reference, and the push went there too. + +The publisher now only reads the top-level `name:` key, whatever order the file is written in. Charts that put `name:` first were never affected. diff --git a/docs/site/changelog/2026-09-28-ferrflow-migrate-tag-template.md b/docs/site/changelog/2026-09-28-ferrflow-migrate-tag-template.md new file mode 100644 index 00000000..c9501b15 --- /dev/null +++ b/docs/site/changelog/2026-09-28-ferrflow-migrate-tag-template.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: migrating from semantic-release keeps your tags' +summary: 'ferrflow migrate turned tagFormat "v${version}" into tagTemplate "v{{version}}", which FerrFlow renders as v{1.2.0}. It now writes "v{version}", so the migrated config finds the tags semantic-release created.' +date: 2026-09-28T10:05:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1229 +docsLink: https://ferrflow.com/docs/reference/cli +--- + +`ferrflow migrate` converts a semantic-release `tagFormat` into a FerrFlow `tagTemplate`. The conversion wrote the placeholder with doubled braces, `{{version}}`, while FerrFlow templates use single ones. A config migrated from `tagFormat: "v${version}"` therefore rendered tags like `v{1.2.0}`: the first release after migrating found none of the existing tags and started over. + +The conversion now writes `{version}`. If you already migrated, check the `ferrflow.json` it wrote for `{{version}}` and replace it with `{version}`: + +```json +{ + "workspace": { + "tagTemplate": "v{version}" + } +} +``` + +The migration table in the CLI reference and in the entry that announced `ferrflow migrate` showed the same wrong output and have been corrected. diff --git a/docs/site/changelog/2026-09-28-ferrflow-padded-versions.md b/docs/site/changelog/2026-09-28-ferrflow-padded-versions.md new file mode 100644 index 00000000..03be22e6 --- /dev/null +++ b/docs/site/changelog/2026-09-28-ferrflow-padded-versions.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: versions read from files never carry stray whitespace' +summary: 'A version written with padding or a line break inside its quotes came back with the whitespace attached and could not become a tag. package.json, Cargo.toml, pyproject.toml and Chart.yaml read with the helm format now trim it; mix.exs, gemspec, Gradle and Package.swift reject it with a clear error.' +date: 2026-09-28T10:15:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1218 +--- + +Several version file readers returned the quoted value exactly as written. `"version": " 1.2.0 "` in `package.json`, or a `version:` literal in `mix.exs` that spanned a line break, produced a version with whitespace in it, and the release then failed at `git tag` with an error about invalid ref names. + +Where the version sits in a delimited string (`package.json`, `Cargo.toml`, `pyproject.toml`, and `Chart.yaml` read with the `helm` format that `ferrflow init` detects), the padding is unambiguous and is now trimmed, so those files keep working. Where the version is found by pattern (`mix.exs`, `.gemspec`, Gradle, `Package.swift`), a padded literal is now reported as "no version found" instead of being guessed at, so you can fix the file before anything is tagged. diff --git a/docs/site/changelog/2026-10-02-ferrflow-shadow-release.md b/docs/site/changelog/2026-10-02-ferrflow-shadow-release.md new file mode 100644 index 00000000..30884578 --- /dev/null +++ b/docs/site/changelog/2026-10-02-ferrflow-shadow-release.md @@ -0,0 +1,17 @@ +--- +title: 'FerrFlow: rehearse a release with shadow-release' +summary: 'ferrflow shadow-release runs the real release pipeline in a throwaway clone, hooks included, and stops before anything is pushed or published. Use it to check that a release would go through before running it.' +date: 2026-10-02T16:30:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/1256 +docsLink: https://ferrflow.com/docs/reference/cli/ +--- + +`--dry-run` shows what a release would change, but it stops short of the steps that can actually fail: writing the files, committing, tagging, running the hooks around them. `ferrflow shadow-release` runs those steps for real, in a temporary clone of your repository, so a broken hook or a file that will not bump shows up before the real release. + +```bash +ferrflow shadow-release +``` + +The clone gets the versioned files, changelogs, release commit, tags and floating tags, and the pre-commit to pre-publish hooks run against it. Nothing is pushed, no forge release is created, and no publisher or post-publish hook runs. The command prints the commits, files and tags it produced, then deletes the clone. Pass `--keep` to inspect it instead. A failing step exits non-zero with its error code, so a CI job can run it as a gate. diff --git a/docs/site/changelog/2026-10-03-ferrflow-latest-tag-hotfix.md b/docs/site/changelog/2026-10-03-ferrflow-latest-tag-hotfix.md new file mode 100644 index 00000000..20d8d2f2 --- /dev/null +++ b/docs/site/changelog/2026-10-03-ferrflow-latest-tag-hotfix.md @@ -0,0 +1,13 @@ +--- +title: 'FerrFlow: a hotfix no longer moves the latest alias backward' +summary: 'With latestTag set, releasing 1.2.4 after 2.0.0 pointed latest at 1.2.4. latest now stays on the highest version; the hotfix release still goes through, and --force moves the alias back if you want it to.' +date: 2026-10-03T12:15:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1262 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +The `latestTag` alias followed whichever stable release ran last, so cutting a hotfix on a maintenance line pointed `latest` at an older version than the one most people should install. It now behaves like the floating `v1` and `v1.5` tags and never moves to a lower version on its own. + +Unlike those tags, a backward `latest` does not stop the release: shipping a hotfix on an older line is a normal thing to do, so the release completes, `latest` stays put, and the output says `Kept latest on 2.0.0: 1.2.4 is older`. If you relied on `latest` meaning "most recently released", pass `--force` to that release and the alias moves back as before, with a warning. diff --git a/docs/site/changelog/2026-10-03-ferrflow-pr-lookup-branch-encoding.md b/docs/site/changelog/2026-10-03-ferrflow-pr-lookup-branch-encoding.md new file mode 100644 index 00000000..e9b68d3d --- /dev/null +++ b/docs/site/changelog/2026-10-03-ferrflow-pr-lookup-branch-encoding.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: release PRs are found again on branches with special characters' +summary: 'A target branch containing &, # or + broke the lookup for the existing release PR on GitHub and GitLab, so FerrFlow opened a second one. A failed branch update on GitHub also reported the wrong error.' +date: 2026-10-03T12:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1261 +--- + +With `releaseCommitMode: "pr"`, FerrFlow looks for an open release PR before proposing a new one. The branch name went into that query unencoded, so a branch such as `release/a+b` sent a different name to the API, the existing PR was not found, and a duplicate was opened. Branch names are now percent-encoded on both GitHub and GitLab. + +On GitHub, moving the release branch fell back to creating it after any failure, so an expired token showed up as a confusing "create ref" error. FerrFlow now only creates the branch when GitHub says it does not exist, and otherwise reports the real failure under E3015. diff --git a/docs/site/changelog/2026-10-03-ferrflow-preview-backtick.md b/docs/site/changelog/2026-10-03-ferrflow-preview-backtick.md new file mode 100644 index 00000000..1dc9edd8 --- /dev/null +++ b/docs/site/changelog/2026-10-03-ferrflow-preview-backtick.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: a backtick in a version no longer breaks the PR preview table' +summary: 'The release preview comment wrapped versions in a code span that a backtick could close early, scrambling the table. Version cells now use a fence long enough for whatever they contain.' +date: 2026-10-03T12:05:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1260 +--- + +`ferrflow check --comment` posts a table of current and next versions on the pull request. Each version sat in a single-backtick code span, and the backslash used to escape a backtick inside it is ignored by Markdown, so a version containing a backtick closed the span and broke the row. + +Version cells are now fenced with one more backtick than the longest run in the value, as CommonMark specifies, so the table renders the version exactly as written. diff --git a/docs/site/changelog/2026-10-03-ferrflow-why-doctor-wording.md b/docs/site/changelog/2026-10-03-ferrflow-why-doctor-wording.md new file mode 100644 index 00000000..a8e6f0c9 --- /dev/null +++ b/docs/site/changelog/2026-10-03-ferrflow-why-doctor-wording.md @@ -0,0 +1,10 @@ +--- +title: 'FerrFlow: clearer wording in why and doctor' +summary: 'ferrflow why no longer prints "forced bump forced", and ferrflow doctor shows a declared strategy with its config spelling, such as calver-short instead of calvershort.' +date: 2026-10-03T12:10:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1259 +--- + +Two diagnostics printed text that did not match what you wrote. `ferrflow why` described a version pinned with `--force-version` as `Decision: forced bump forced`; it now reads `forced bump set with --force-version`. `ferrflow doctor` showed a declared versioning strategy with its hyphens dropped, so `calver-short` appeared as `calvershort`, a value the config would reject. It now prints the name exactly as you would write it in `ferrflow.json`. diff --git a/docs/site/changelog/2026-10-07-ferrflow-strategy-names-json.md b/docs/site/changelog/2026-10-07-ferrflow-strategy-names-json.md new file mode 100644 index 00000000..a8c8f852 --- /dev/null +++ b/docs/site/changelog/2026-10-07-ferrflow-strategy-names-json.md @@ -0,0 +1,12 @@ +--- +title: 'FerrFlow: JSON output names date and sequence strategies as you configure them' +summary: 'check --json and release --json reported bump_type as calvershortseq for a calver-short-seq package, and why --json did the same for strategy. They now use the configured spelling, such as calver-short-seq. Scripts matching the old value need updating.' +date: 2026-10-07T09:00:00Z +product: ferrflow +type: fix +prLink: https://github.com/FerrLabs/FerrFlow/pull/1293 +--- + +For packages on a date or sequence strategy, the `bump_type` field of `ferrflow check --json` and `ferrflow release --json`, and the `strategy` field of `ferrflow why --json`, carried the strategy name with its hyphens dropped: `calvershort`, `calverseq`, `calvershortseq`. None of those is a value `ferrflow.json` accepts, and `ferrflow doctor` already printed the hyphenated form. + +They now match the config: `calver-short`, `calver-seq`, `calver-short-seq`. `calver`, `sequential` and the semver bump types (`major`, `minor`, `patch`) are unchanged, and so is `FERRFLOW_BUMP_TYPE` in hooks. If a script compares `bump_type` or `strategy` against the old spelling, switch it to the hyphenated one. diff --git a/docs/site/changelog/2026-10-10-ferrflow-hook-timeout.md b/docs/site/changelog/2026-10-10-ferrflow-hook-timeout.md new file mode 100644 index 00000000..6c001534 --- /dev/null +++ b/docs/site/changelog/2026-10-10-ferrflow-hook-timeout.md @@ -0,0 +1,23 @@ +--- +title: 'FerrFlow: hooks can no longer hang a release' +summary: 'Every hook now has a timeout, 15 minutes by default, after which FerrFlow kills it and everything it started. Hooks also run with stdin closed, so a command that would prompt fails at once instead of waiting forever.' +date: 2026-10-10T10:00:00Z +product: ferrflow +type: new +prLink: https://github.com/FerrLabs/FerrFlow/pull/1300 +docsLink: https://ferrflow.com/docs/configuration/config-file/ +--- + +A hook that never returned used to hold the release, and the release lock, until the CI job hit its own wall-clock limit. The usual causes are mundane: a command asking for a password on a terminal nobody is watching, a `curl` without a time limit, a test suite that deadlocks. + +Hooks now run with stdin closed, which turns the prompting case into an immediate failure, and each hook gets a time limit. When it is reached, FerrFlow kills the hook along with any process it started in the background and treats it as a failed hook, so `onFailure` decides what happens next: `abort` stops the release, `continue` warns and carries on. A long hook prints that it is still running once a minute, which tells a slow build apart from a stuck one. + +The default is 900 seconds. Raise or lower it for the workspace or for a single package: + +```json +{ + "workspace": { + "hooks": { "prePublish": "npm run build", "timeout": 1800 } + } +} +```