What is being asked
A project wants to compute its own version number rather than pick from semver, calver, the four calendar variants, sequential or zerover, and versionTemplate is still a fixed pattern. The obvious shape is versioning = "custom" plus a hook, or a script path accepted where a strategy is named.
The case that raised it: Pumpkin-MC/Pumpkin#3231 wants 0.2.0+26.2-26.45, where the suffix comes from the newest protocol enum variants in a Rust source file. That particular one does not need this feature, and the pull request does not use it: the suffix is semver build metadata, excluded from version identity and ignored for precedence, so a postBump hook rewriting the manifest is correct and already works. This RFC is about the general case, where the computed part belongs to the version itself: the tag, the changelog heading, the published version.
Why the obvious design is blocked
Read-only commands would execute user code. check, status, why, graph and preview all compute versions; compute_next_version is reached from eight modules. A version-producing hook makes every one of them run whatever the config points at.
That is not a theoretical objection here. FerrFlow-Cloud/api links this crate as a library to answer validate and preview for configs submitted through the website, deliberately, so the site answers exactly what the binary answers. A strategy that shells out turns that endpoint into arbitrary code execution unless it is disabled server-side, and disabling it breaks the property the API exists for.
No hook returns a value today. All nine, preRelease through postPublish, are fire-and-forget shell with eleven FERRFLOW_* variables and no return channel. prereleaseIdentifier looks like a counter-example and is not: it is an enum of increment, timestamp, short-hash, timestamp-hash. Capturing stdout, trimming it, validating it parses as a version, and rejecting a value that goes backwards or collides with an existing tag is a new contract rather than an extension of an existing one.
Cost per invocation. A process per package per command, against a tool whose benchmark page argues 7 ms versus 590 ms for the JavaScript alternatives.
The proposal worth exploring: run it late
Run the hook only in commands that actually apply a version, which is release, and in a slot between "the bump is decided" and "the version is written anywhere": before the version files, the tag and the changelog heading.
That answers the first two objections. check, status, why, graph and preview never execute it, so the API stays inert and fast, and the hook contract only has to hold in a command that already runs eight other hooks and mutates the repository.
The slot matters. Later than that and this collapses into postBump, which exists: after the files are written, changing the version means rewriting everything the release already stamped. Earlier and it is back in the read-only path.
What it costs, and the part that needs deciding
preview and check stop being able to state the next version when a version hook is configured. Their whole value is answering exactly what a release would do, so they must say the version is hook-determined rather than print a number that may not be the one that ships. A wrong number would be worse than the feature is worth.
Open questions:
- What does the hook receive? The computed bump, current version, tag prefix and commits are the obvious set, all of which already reach hooks as
FERRFLOW_* variables.
- What does it return, and how strictly is it validated? Refusing a non-version, a regression, and a tag that already exists all seem necessary; whether it may return an empty value to mean "no release" is a question.
- Does it apply per package or per workspace? In a monorepo the hook would run once per bumped package, which multiplies the process cost.
- Does
--dry-run run it? It must, or the dry run stops being a rehearsal, and then the hook has to be free of side effects.
Alternative that avoids all of it
A declarative buildMetadata option, whose value is appended after + and never enters identity or precedence. It covers the Pumpkin shape and anything else that is decoration on a computed version, it stays exact in preview, and it never has to run in the API. It does not cover a version whose identity is externally determined, which is the real target here, so it is a narrower feature rather than a substitute.
What is being asked
A project wants to compute its own version number rather than pick from
semver,calver, the four calendar variants,sequentialorzerover, andversionTemplateis still a fixed pattern. The obvious shape isversioning = "custom"plus a hook, or a script path accepted where a strategy is named.The case that raised it: Pumpkin-MC/Pumpkin#3231 wants
0.2.0+26.2-26.45, where the suffix comes from the newest protocol enum variants in a Rust source file. That particular one does not need this feature, and the pull request does not use it: the suffix is semver build metadata, excluded from version identity and ignored for precedence, so apostBumphook rewriting the manifest is correct and already works. This RFC is about the general case, where the computed part belongs to the version itself: the tag, the changelog heading, the published version.Why the obvious design is blocked
Read-only commands would execute user code.
check,status,why,graphandpreviewall compute versions;compute_next_versionis reached from eight modules. A version-producing hook makes every one of them run whatever the config points at.That is not a theoretical objection here.
FerrFlow-Cloud/apilinks this crate as a library to answervalidateandpreviewfor configs submitted through the website, deliberately, so the site answers exactly what the binary answers. A strategy that shells out turns that endpoint into arbitrary code execution unless it is disabled server-side, and disabling it breaks the property the API exists for.No hook returns a value today. All nine,
preReleasethroughpostPublish, are fire-and-forget shell with elevenFERRFLOW_*variables and no return channel.prereleaseIdentifierlooks like a counter-example and is not: it is an enum ofincrement,timestamp,short-hash,timestamp-hash. Capturing stdout, trimming it, validating it parses as a version, and rejecting a value that goes backwards or collides with an existing tag is a new contract rather than an extension of an existing one.Cost per invocation. A process per package per command, against a tool whose benchmark page argues 7 ms versus 590 ms for the JavaScript alternatives.
The proposal worth exploring: run it late
Run the hook only in commands that actually apply a version, which is
release, and in a slot between "the bump is decided" and "the version is written anywhere": before the version files, the tag and the changelog heading.That answers the first two objections.
check,status,why,graphandpreviewnever execute it, so the API stays inert and fast, and the hook contract only has to hold in a command that already runs eight other hooks and mutates the repository.The slot matters. Later than that and this collapses into
postBump, which exists: after the files are written, changing the version means rewriting everything the release already stamped. Earlier and it is back in the read-only path.What it costs, and the part that needs deciding
previewandcheckstop being able to state the next version when a version hook is configured. Their whole value is answering exactly what a release would do, so they must say the version is hook-determined rather than print a number that may not be the one that ships. A wrong number would be worse than the feature is worth.Open questions:
FERRFLOW_*variables.--dry-runrun it? It must, or the dry run stops being a rehearsal, and then the hook has to be free of side effects.Alternative that avoids all of it
A declarative
buildMetadataoption, whose value is appended after+and never enters identity or precedence. It covers the Pumpkin shape and anything else that is decoration on a computed version, it stays exact inpreview, and it never has to run in the API. It does not cover a version whose identity is externally determined, which is the real target here, so it is a narrower feature rather than a substitute.