Skip to content

Commit fcf98b5

Browse files
authored
feat(versioning): add versionCommand, a command that decides the release version (#1331)
1 parent 64dba9a commit fcf98b5

38 files changed

Lines changed: 751 additions & 66 deletions

‎docs/diagrams/release-algorithm.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ flowchart TB
1313
GRAPH --> TAGS["pre-collect every tag + commit OID<br/>in one tag_foreach scan"]
1414
TAGS --> ANALYSE["per package: find last tag,<br/>read commits since,<br/>classify conventional commits"]
1515
ANALYSE --> PLAN["compute the bump<br/>→ snapshot of every package to change"]
16-
PLAN --> CL["build the changelog section once"]
16+
PLAN --> VC["versionCommand sets the version<br/>(release only, never check or preview)"]
17+
VC --> CL["build the changelog section once"]
1718
CL --> WRITE["write versions into the manifests"]
1819
WRITE --> EXEC["execute_release"]
1920
```
2021

22+
`versionCommand` sits between the decided bump and every write, so the tag, the changelog heading and
23+
the version files all carry the version it prints. Read-only commands skip it and report the version as
24+
command-determined instead of printing a number that may not be the one that ships.
25+
2126
The tag pre-collection matters more than it looks: without it, every package does its own tag walk,
2227
and lookups go from `O(1)` against a prepared map to `O(tags)` callbacks plus an `O(commits)` walk —
2328
per package. On a monorepo with many tags that is the difference between seconds and minutes.

‎docs/site/data/schema/ferrflow-package.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@
186186
],
187187
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Overrides `workspace.buildMetadata` for this package. A command replaces the workspace one, `true` inherits it, and `false` releases this package with a plain version while the rest of the workspace stays stamped."
188188
},
189+
"versionCommand": {
190+
"type": "string",
191+
"description": "Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
192+
},
193+
"version_command": {
194+
"type": "string",
195+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
196+
},
189197
"hooks": {
190198
"$ref": "#/$defs/hooks"
191199
},

‎docs/site/data/schema/ferrflow.json‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,14 @@
506506
"build_metadata": {
507507
"type": "string",
508508
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Command whose stdout is appended to the version as semver build metadata, after a `+`. It is run once per release, never in a dry run, and its output must be dot-separated alphanumerics and hyphens. The tag and the changelog keep the plain version, since build metadata is excluded from version identity and ignored for precedence."
509+
},
510+
"versionCommand": {
511+
"type": "string",
512+
"description": "Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
513+
},
514+
"version_command": {
515+
"type": "string",
516+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
509517
}
510518
},
511519
"additionalProperties": false
@@ -669,6 +677,14 @@
669677
"oneOf": [{ "type": "string" }, { "type": "boolean" }],
670678
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Overrides `workspace.buildMetadata` for this package. A command replaces the workspace one, `true` inherits it, and `false` releases this package with a plain version while the rest of the workspace stays stamped."
671679
},
680+
"versionCommand": {
681+
"type": "string",
682+
"description": "Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
683+
},
684+
"version_command": {
685+
"type": "string",
686+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
687+
},
672688
"hooks": {
673689
"$ref": "#/$defs/hooks"
674690
},

‎docs/site/docs-en/configuration/config-file.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,26 @@ buildMetadata = &quot;sh scripts/protocol-versions.sh&quot;
481481
</div></div>
482482
</div>
483483

484+
### Version command
485+
486+
`versionCommand` names a command whose stdout is the version a release ships. Reach for it when the version itself, not a suffix, is decided outside the commits: a number owned by an upstream project, a date scheme the built-in strategies do not cover. For decoration after a `+`, use `buildMetadata` instead.
487+
488+
The command runs in `release`, `--dry-run` included, after the bump is decided and before any version file, tag or changelog heading is written, so all three carry its output. It receives the usual `FERRFLOW_*` variables; `FERRFLOW_NEW_VERSION` holds the version the strategy would have picked and `FERRFLOW_COMMITS_JSON` the commits. FerrFlow trims the output and drops a leading `v`.
489+
490+
The release aborts if the command fails, prints nothing, prints something the strategy cannot parse, prints a version that does not go past the current one, or prints a version whose tag already exists. Printing the current version releases nothing for that package. In a monorepo the command runs once per package that releases, and a package can set its own.
491+
492+
`check`, `preview`, `status`, `why` and `graph` never run it, because they run in places that must not execute repository code. They report the version as set by the command, and `check --json` prints `null` for `next_version` and `tag`.
493+
494+
A package in a `linked` or `fixed` group cannot set `versionCommand`, and `--force-version` takes precedence over it.
495+
496+
```json
497+
{
498+
"workspace": {
499+
"versionCommand": "sh scripts/next-version.sh"
500+
}
501+
}
502+
```
503+
484504
### Changelog rendering
485505

486506
Omit `changelog` entirely and FerrFlow writes the classic layout: **Breaking Changes**, **Features**, **Bug Fixes** and **Refactoring**, flat bullets, no links. `perf` commits fold into **Bug Fixes** there, and every other type, `docs` and `security` included, is left out.
@@ -560,6 +580,7 @@ Defines a package to version. You can have one or many.
560580
| `floatingTags` | no | inherited from workspace | Override floating tags for this package |
561581
| `latestTag` | no | inherited from workspace | Override the floating alias tag for this package |
562582
| `buildMetadata` | no | inherited from workspace | Override the build metadata command for this package, or `false` to release it with a plain version |
583+
| `versionCommand` | no | inherited from workspace | Override the version command for this package |
563584
| `versionSource` | no | inherited from workspace | Override the tag-vs-file resolution for this package |
564585

565586
### `versionedFiles`

‎docs/site/docs-fr/configuration/config-file.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,29 @@ Définit un package à versionner. Vous pouvez en avoir un ou plusieurs.
420420
| `tagTemplate` | non | hérité du workspace | Surcharger le modèle de tag pour ce package |
421421
| `latestTag` | non | hérité du workspace | Surcharger le tag alias flottant pour ce package |
422422
| `buildMetadata` | non | hérité du workspace | Surcharger la commande de métadonnées, ou `false` pour une version nue |
423+
| `versionCommand` | non | hérité du workspace | Surcharger la commande de version pour ce package |
423424
| `versionSource` | non | hérité du workspace | Surcharger la résolution tag / fichier pour ce package |
424425

426+
### Commande de version
427+
428+
`versionCommand` désigne une commande dont la sortie standard est la version publiée par une release. À utiliser quand la version elle-même, et non un suffixe, est décidée hors des commits : un numéro imposé par un projet amont, un schéma de dates que les stratégies intégrées ne couvrent pas. Pour une décoration après un `+`, préférer `buildMetadata`.
429+
430+
La commande est exécutée par `release`, `--dry-run` compris, une fois le bump décidé et avant l'écriture de tout fichier de version, tag ou titre de changelog, qui portent donc tous sa sortie. Elle reçoit les variables `FERRFLOW_*` habituelles ; `FERRFLOW_NEW_VERSION` contient la version que la stratégie aurait choisie et `FERRFLOW_COMMITS_JSON` les commits. FerrFlow élague la sortie et retire un `v` initial.
431+
432+
La release est interrompue si la commande échoue, n'imprime rien, imprime une valeur que la stratégie ne sait pas lire, une version qui ne dépasse pas la version courante, ou une version dont le tag existe déjà. Imprimer la version courante ne publie rien pour ce package. Dans un monorepo la commande s'exécute une fois par package publié, et chaque package peut définir la sienne.
433+
434+
`check`, `preview`, `status`, `why` et `graph` ne l'exécutent jamais, car ils tournent dans des contextes qui ne doivent pas exécuter le code du dépôt. Ils indiquent que la version est fixée par la commande, et `check --json` renvoie `null` pour `next_version` et `tag`.
435+
436+
Un package d'un groupe `linked` ou `fixed` ne peut pas définir `versionCommand`, et `--force-version` l'emporte sur elle.
437+
438+
```json
439+
{
440+
"workspace": {
441+
"versionCommand": "sh scripts/next-version.sh"
442+
}
443+
}
444+
```
445+
425446
### `versionedFiles`
426447

427448
Fichiers dans lesquels le numéro de version doit être mis à jour.

‎schema/ferrflow-package.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@
186186
],
187187
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Overrides `workspace.buildMetadata` for this package. A command replaces the workspace one, `true` inherits it, and `false` releases this package with a plain version while the rest of the workspace stays stamped."
188188
},
189+
"versionCommand": {
190+
"type": "string",
191+
"description": "Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
192+
},
193+
"version_command": {
194+
"type": "string",
195+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
196+
},
189197
"hooks": {
190198
"$ref": "#/$defs/hooks"
191199
},

‎schema/ferrflow.json‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,14 @@
506506
"build_metadata": {
507507
"type": "string",
508508
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Command whose stdout is appended to the version as semver build metadata, after a `+`. It is run once per release, never in a dry run, and its output must be dot-separated alphanumerics and hyphens. The tag and the changelog keep the plain version, since build metadata is excluded from version identity and ignored for precedence."
509+
},
510+
"versionCommand": {
511+
"type": "string",
512+
"description": "Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
513+
},
514+
"version_command": {
515+
"type": "string",
516+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
509517
}
510518
},
511519
"additionalProperties": false
@@ -669,6 +677,14 @@
669677
"oneOf": [{ "type": "string" }, { "type": "boolean" }],
670678
"description": "Snake_case form of `buildMetadata` — both spellings are accepted. Overrides `workspace.buildMetadata` for this package. A command replaces the workspace one, `true` inherits it, and `false` releases this package with a plain version while the rest of the workspace stays stamped."
671679
},
680+
"versionCommand": {
681+
"type": "string",
682+
"description": "Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
683+
},
684+
"version_command": {
685+
"type": "string",
686+
"description": "Snake_case form of `versionCommand` — both spellings are accepted. Overrides `workspace.versionCommand` for this package. Command whose stdout is the version this release ships, replacing the one the versioning strategy computed. It receives the usual `FERRFLOW_*` variables, with `FERRFLOW_NEW_VERSION` holding the computed proposal. It runs only in `release` (dry run included), after the bump is decided and before any version file, tag or changelog heading is written. `check`, `preview`, `status`, `why` and `graph` never run it and report the version as set by the command. The output must be a version that goes past the current one and does not collide with an existing tag. Not allowed on a package in a linked or fixed group."
687+
},
672688
"hooks": {
673689
"$ref": "#/$defs/hooks"
674690
},

0 commit comments

Comments
 (0)