Versionary is a software-agnostic automated release tool focused on SemVer, conventional commits, release PR workflows, and extensibility.
Versionary is designed as a practical middle ground between semantic-release
and release-please.
- Like
semantic-release, it supports direct release execution. - Like
release-please, it supports a release PR workflow so maintainers can preview and review changes before publication.
The core idea is to keep versioning, changelog generation, tagging, and SCM release metadata in one tool, while leaving package publication (npm, crates.io, etc.) to dedicated CI workflows triggered by tags or releases.
Versionary is being built to:
- support both direct releases and release-PR-gated releases
- work across repository types (Node, Rust, docs/LaTeX, etc.)
- stay SCM-agnostic at the core with built-in integration adapters (GitHub first; GitLab/Codeberg later)
- keep a small, stable core with explicit extension points
- handle trunk-based development and monorepo workflows cleanly
In scope:
- semantic version planning from commits
- changelog generation
- release PR automation
- tags + SCM release metadata (e.g. GitHub Releases)
Out of scope (intentional):
- publishing artifacts to language registries
- replacing package-specific publish tooling
- external/user-provided plugin loading
Use your CI/CD platform for registry publishing, triggered from a created release/tag.
Current implementation focuses on:
- strategy-based version updates (
simple,node,rust,r) - release planning and changelog generation
- review-mode vs direct-mode release flow
- a static internal SCM client (
githubprovider today)
Planned/harder areas include deeper monorepo ergonomics, broader SCM coverage, and stronger failure recovery around release steps.
Versionary is set up so new language strategies can be added internally without
changing release orchestration. A new strategy should implement the
VersionStrategy contract in src/strategy/types.ts and be wired in
src/strategy/resolve.ts.
Checklist for new strategies (for example python):
- define strategy
name - define
getVersionFile(config)defaults and config override behavior - implement
readVersion(cwd, config)with explicit malformed-file errors - implement
writeVersion(cwd, config, version)returning deterministic updated file paths - optionally implement
readPackageName(cwd, config)so monorepo release tags can derive from language metadata (similar to Node/Rust/R) - optionally implement
propagateDependentPatchImpacts(cwd, packages)if dependency updates in this ecosystem should trigger dependent package patch bumps - optionally implement
finalizeVersionWrites(cwd, writes)for ecosystem post-processing after all target version files are written - add focused strategy tests for ecosystem-specific behavior and edge cases
- add/extend strategy contract tests in
tests/strategy-contract.test.ts - update schema/docs for new
release-typebehavior and defaults
Current ecosystem policy defaults:
- changelog source for publish:
- root target uses root
changelog-file - package target uses package changelog defaults:
packages.<path>.changelog-filewhen configured- otherwise
NEWS.mdfor R targets orCHANGELOG.mdfor other targets at<package-path>/<default-file>
- root target uses root
- lockfiles:
- Node strategy updates root
package-lock.json/npm-shrinkwrap.jsonwhen present - Rust release PR prep refreshes all discovered
Cargo.lockfiles
- Node strategy updates root
- workspace/inheritance:
- Rust supports
version.workspace = truevia[workspace.package].version - other strategies should document equivalent inheritance behavior explicitly
- Rust supports
Current runtime code uses a flat src/ layout with clear module boundaries:
src/cli/: command router (run,verify,plan,changelog,pr,release)src/release/: release orchestration (plan/changelog/PR/release/state/recovery)src/strategy/: strategy contracts, resolver, and built-in implementations (simple,node,rust,r)src/scm/: SCM client contracts and provider implementation(s)src/config/: config loading and schema validationsrc/git/: git commit/range and repository URL helperssrc/verify/: repository/config verificationsrc/types/: shared config/plugin-facing types
Configuration is loaded from versionary.jsonc by default (or
versionary.json).
Schema URL for editor support:
https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json
For a quick trial, use:
version-file(defaultversion.txt) as version sourcechangelog-file(defaultCHANGELOG.md) as release notes outputrelease-type: "node"usespackage.jsonas version source and updates it during release PR preprelease-type: "r"usesDESCRIPTIONas version source and updates theVersion:fieldrelease-type: "rust"uses Cargo manifests (Cargo.toml) as version source;version-filemust point to aCargo.toml(default:Cargo.toml)- simple/default strategy keeps
version.txtas source of truth and does not updatepackage.json - stable release branch (
release-branch, default:versionary/release) so release PRs are updated in-place baseline-file(default.versionary-manifest.json) tracks baseline SHA for deterministic commit ranges independent of tags- pre-1.0 policy defaults to conservative major handling: for
0.y.z, breaking changes bump to0.(y+1).0; setallow-stable-major: trueto allow explicit auto-transition to1.0.0on a breaking release - review mode (
review-mode):pr(PR/MR style) ordirect(no review request) release-draft(defaultfalse) publishes GitHub releases as drafts when enabledrelease-reference-commentscontrols release comments on linked issues/PRs:off(default): do not post commentsbest-effort: post comments and continue on API/permission failuresstrict: fail release if comment posting fails
- optional monorepo planning with
monorepo-modeandpackages:independentcomputes package bumps per pathfixedcomputes one shared bump across configured package paths- per-package
package-namecan override release identity (labels + tag base) - per-package
changelog-filewrites package release notes to<package-path>/<changelog-file>
Rust strategy examples:
// Workspace root (virtual or root crate + members)
{
"release-type": "rust",
"version-file": "Cargo.toml"
}Current rust auto-update behavior (phase scope):
- updates crate versions in each targeted crate
[package].version - supports targeted crates using
version.workspace = trueby updating[workspace.package].versionin the owning workspace manifest - updates internal workspace dependency versions when the dependency name matches another targeted crate name
- refreshes
Cargo.lockviacargo generate-lockfilewhenCargo.lockexists in repo root - applies dependency version rewrites in:
[dependencies],[dev-dependencies],[build-dependencies][target.*.dependencies],[target.*.dev-dependencies],[target.*.build-dependencies]
Current rust non-goals/limits:
- does not update external dependency versions
- does not update
workspace.dependencies - does not add missing
version = ...fields to dependency inline tables - does not perform Cargo publish/release to crates.io
If Cargo.lock exists, cargo must be available in PATH during PR preparation.
For independent monorepo targets, Versionary derives release tags as:
- root package (
"."):v<version> - non-root package:
<release-name>-v<version>
release-name precedence is:
packages.<path>.package-name(explicit override)- strategy-native package name from version file:
- Node:
package.jsonname - Rust:
Cargo.toml[package].name - R:
DESCRIPTIONPackage:
- Node:
- package path fallback
When multiple packages resolve to the same <release-name> and version, the run
fails fast with a duplicate-tag error and suggests setting unique
package-name values.
Release planning is based on Conventional Commit parsing semantics:
- parses type/scope/description from commit headers
- exposes structured parsed fields (
header,body,footer,type,scope,description,notes,references,mentions,revert) - separates parser output from release policy mapping (
inferReleaseType*) - recognizes breaking changes from
!andBREAKING CHANGE/BREAKING-CHANGEfooters - maps release impact as
feat => minor,fix|perf => patch, breaking => major - treats
revert:commits as patch-releasable by default (and major if marked breaking, e.g.revert!:orBREAKING CHANGE) - suppresses commits that are reverted within the analyzed release window so they do not affect bump/changelog output
- emits parser diagnostics for malformed headers/footers/references and ambiguous revert messages
Commands:
pnpm verifypnpm run(default orchestration: no-op, create/update release PR, or publish release based on context)pnpm run -- --json(machine-readable orchestration result)pnpm planpnpm changelog -- --writepnpm prpnpm release
pnpm pr prepares release commit + branch and opens/updates a review request
through the SCM client. pnpm run is the recommended CI entrypoint and
auto-dispatches between PR/update and release publish.
For first-run bootstrapping, set bootstrap-sha (similar to release-please).
Subsequent runs use the baseline state file.
Release publish (pnpm release or the publish path in pnpm run) is idempotent
by target tag:
- if a tag already exists, Versionary reuses it rather than recreating it
- if release metadata already exists for the tag (e.g., GitHub Release), it is reused
- if a prior run created/pushed the tag but failed before metadata creation, a rerun creates the missing metadata and proceeds
Versionary fails fast when recovery is unsafe (for example, local and remote tags with the same name point to different SHAs). In these cases, the error message includes remediation guidance so CI logs are actionable.
Versionary currently uses a static internal SCM client model:
src/scm/types.tsdefines theScmClientcontractsrc/scm/client.tsreturns the active provider client- current provider is
githubviasrc/scm/github-plugin.ts
There is no runtime discovery/loading of external SCM providers in the release
flow. Adding another provider is an internal extension: implement ScmClient
and wire provider selection in src/scm/client.ts.
Required environment for the GitHub SCM provider:
GITHUB_REPOSITORY(format:owner/repo)- one token env var:
VERSIONARY_PR_TOKENorGH_TOKENorGITHUB_TOKEN
Token precedence is:
VERSIONARY_PR_TOKEN>GH_TOKEN>GITHUB_TOKEN
Minimum GitHub token/repo permissions for Versionary-managed metadata:
- release PR create/update flow:
contents: write,pull-requests: write - release metadata flow (GitHub Release create/read):
contents: write
review-mode behavior:
pr(preferred;reviewis a backward-compatible alias):pnpm run runprepares/updates the release branch and creates or updates a release PRdirect:pnpm run runprepares/updates the release branch and skips review request creation
Concise GitHub Actions examples:
# 1) Release PR / update flow (run on push to default branch)
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- id: versionary
uses: jolars/versionary@v1
with:
token: ${{ secrets.RELEASE_TOKEN }}# 2) Release publish flow after merge (release commit context)
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- id: versionary
uses: jolars/versionary@v1
with:
token: ${{ secrets.RELEASE_TOKEN }}
- if: ${{ steps.versionary.outputs.release_created == 'true' }}
run: echo "Released ${{ steps.versionary.outputs.tag_name }}"token is used for both GitHub API calls and git push authentication in
the composite action. This means release-branch force-pushes are attributed to
that token and can trigger downstream workflows when using a PAT/App token.
(github-token remains as a deprecated alias for backward compatibility.)
Action outputs:
action:noop,pr-prepared,release-published,release-skippedmessage: human-readable summaryrelease_created:"true"when at least one release was publishedtag_name: first published tag (for single-target flows)tag_names: JSON array of published tagsreview_url: review request URL when PR flow runs
For GitHub Action consumers, publish immutable tags (for example v1.2.3) and
maintain a moving major tag (v1, v2, ...). A small release-triggered
workflow should update v<major> to the latest release tag so uses: jolars/versionary@v1 stays current without breaking major compatibility.
Package publication is intentionally out of scope in the current release flow. Use separate CI workflows for publishing after Versionary has prepared/tagged the release.
You can install directly from a git ref:
{
"devDependencies": {
"versionary": "github:jolars/versionary#<commit-or-tag>"
}
}The package runs a prepare build during git installation so the versionary
CLI binary is available after pnpm install.