versionary
Advanced tools
@@ -57,2 +57,3 @@ import { z } from "zod"; | ||
| pattern: z.ZodOptional<z.ZodString>; | ||
| replacement: z.ZodOptional<z.ZodString>; | ||
| }, z.core.$strip>>>; | ||
@@ -59,0 +60,0 @@ follows: z.ZodOptional<z.ZodArray<z.ZodString>>; |
@@ -9,2 +9,3 @@ import { z } from "zod"; | ||
| pattern: z.string().optional(), | ||
| replacement: z.string().optional(), | ||
| }) | ||
@@ -31,2 +32,9 @@ .superRefine((value, ctx) => { | ||
| } | ||
| if (needsJsonPath && value.replacement) { | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: `${value.type} artifact rules do not support "replacement".`, | ||
| path: ["replacement"], | ||
| }); | ||
| } | ||
| if (value["field-path"] && value.jsonpath) { | ||
@@ -33,0 +41,0 @@ ctx.addIssue({ |
@@ -5,2 +5,3 @@ import fs from "node:fs"; | ||
| import YAML from "yaml"; | ||
| import { parseVersion } from "./semver.js"; | ||
| const WILDCARD = Symbol("wildcard"); | ||
@@ -164,3 +165,25 @@ function isRecord(value) { | ||
| } | ||
| function applyRegexRule(content, pattern, version) { | ||
| const REPLACEMENT_TOKEN_PATTERN = /\{\{\s*([A-Za-z]+)\s*\}\}/gu; | ||
| function renderReplacementTemplate(template, version) { | ||
| const parsed = parseVersion(version); | ||
| const tokens = { | ||
| version, | ||
| major: String(parsed.major), | ||
| minor: String(parsed.minor), | ||
| patch: String(parsed.patch), | ||
| prerelease: parsed.prerelease.join("."), | ||
| build: parsed.build.join("."), | ||
| }; | ||
| return template.replace(REPLACEMENT_TOKEN_PATTERN, (_whole, name) => { | ||
| const key = name.toLowerCase(); | ||
| const value = tokens[key]; | ||
| if (value === undefined) { | ||
| throw new Error(`Unknown replacement token "{{${name}}}". Supported tokens: ${Object.keys(tokens) | ||
| .map((token) => `{{${token}}}`) | ||
| .join(", ")}.`); | ||
| } | ||
| return value; | ||
| }); | ||
| } | ||
| function applyRegexRule(content, pattern, version, replacementTemplate) { | ||
| const regex = parseRegexPattern(pattern); | ||
@@ -170,3 +193,3 @@ const matchFlags = regex.flags.includes("g") | ||
| : `${regex.flags}g`; | ||
| const globalRegex = new RegExp(regex.source, matchFlags); | ||
| const globalRegex = new RegExp(regex.source, matchFlags.includes("d") ? matchFlags : `${matchFlags}d`); | ||
| const matches = [...content.matchAll(globalRegex)]; | ||
@@ -185,5 +208,17 @@ if (matches.length !== 1) { | ||
| const full = match[0]; | ||
| const groupOne = match[1]; | ||
| const replacement = typeof groupOne === "string" ? full.replace(groupOne, version) : version; | ||
| return `${content.slice(0, start)}${replacement}${content.slice(start + full.length)}`; | ||
| // With a replacement template, render it and replace the entire match. | ||
| if (replacementTemplate !== undefined) { | ||
| const rendered = renderReplacementTemplate(replacementTemplate, version); | ||
| return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`; | ||
| } | ||
| // Legacy behavior: substitute the full version into the first capture group, | ||
| // leaving the rest of the match intact. Splice by group indices rather than | ||
| // `String.replace` so literal `$` sequences and repeated group content are | ||
| // handled correctly. | ||
| const groupIndices = match.indices?.[1]; | ||
| if (!groupIndices) { | ||
| return `${content.slice(0, start)}${version}${content.slice(start + full.length)}`; | ||
| } | ||
| const [groupStart, groupEnd] = groupIndices; | ||
| return `${content.slice(0, groupStart)}${version}${content.slice(groupEnd)}`; | ||
| } | ||
@@ -375,3 +410,3 @@ function applyTomlRulePreservingFormatting(content, fieldPath, version) { | ||
| } | ||
| return applyRegexRule(content, rule.pattern, version); | ||
| return applyRegexRule(content, rule.pattern, version, rule.replacement); | ||
| } | ||
@@ -378,0 +413,0 @@ if (rule.type === "json") { |
@@ -438,2 +438,11 @@ import { execFileSync } from "node:child_process"; | ||
| } | ||
| // Honor the plan's changelog format (e.g. r-news) so manual notes, which are | ||
| // authored/extracted relative to that format's heading depth, render at the | ||
| // right level. Falling through to renderReleaseNotesSection would always use | ||
| // the markdown-changelog convention and leave r-news highlights one level too | ||
| // high relative to the auto-generated sections. | ||
| if (plan && plan.changelogFormat === "r-news") { | ||
| const notes = renderReleasePlanChangelog(plan, { cwd, highlights }); | ||
| return `${notes}\n\n${renderReviewRequestFooter()}`; | ||
| } | ||
| return renderReleaseNotesSection({ | ||
@@ -440,0 +449,0 @@ currentVersion: previousVersion, |
@@ -10,2 +10,3 @@ export type ConfigFileFormat = "jsonc" | "json" | "toml" | "js"; | ||
| pattern?: string; | ||
| replacement?: string; | ||
| } | ||
@@ -12,0 +13,0 @@ export interface VersionaryPackage { |
+9
-4
| { | ||
| "name": "versionary", | ||
| "version": "0.31.0", | ||
| "version": "0.32.0", | ||
| "description": "Automatic release framework based on conventional commits and semantic versioning", | ||
@@ -30,5 +30,5 @@ "keywords": [ | ||
| "dependencies": { | ||
| "smol-toml": "^1.4.2", | ||
| "@octokit/rest": "^22.0.0", | ||
| "jsonc-parser": "^3.3.1", | ||
| "smol-toml": "^1.4.2", | ||
| "yaml": "^2.8.3", | ||
@@ -38,5 +38,7 @@ "zod": "^4.1.12" | ||
| "devDependencies": { | ||
| "@types/node": "^25.6.0", | ||
| "@types/node": "^26.0.1", | ||
| "tsx": "^4.20.6", | ||
| "typescript": "^6.0.3", | ||
| "vite": "^8.0.0", | ||
| "vitepress": "^1.6.4", | ||
| "vitest": "^4.1.4" | ||
@@ -54,4 +56,7 @@ }, | ||
| "pr": "tsx src/cli/index.ts pr", | ||
| "release": "tsx src/cli/index.ts release" | ||
| "release": "tsx src/cli/index.ts release", | ||
| "docs:dev": "vitepress dev docs", | ||
| "docs:build": "vitepress build docs", | ||
| "docs:preview": "vitepress preview docs" | ||
| } | ||
| } |
+92
-434
| # Versionary | ||
| Versionary is a software-agnostic automated release tool focused on SemVer, | ||
| conventional commits, release PR workflows, and extensibility. | ||
| Conventional Commits, release-PR workflows, and extensibility. | ||
| 📖 **Documentation: <https://versionary.dev/>** | ||
| ## Why this exists | ||
| Versionary is designed as a practical middle ground between `semantic-release` | ||
| and `release-please`. | ||
| Versionary is a practical middle ground between | ||
| [`semantic-release`](https://github.com/semantic-release/semantic-release) and | ||
| [`release-please`](https://github.com/googleapis/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. | ||
| - 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. | ||
| It keeps 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. | ||
| ## Product direction | ||
| It is built to: | ||
| 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 | ||
| - work across ecosystems (Node, Rust, Python, R, Julia, LaTeX, …) | ||
| - stay SCM-agnostic at the core with built-in integration (GitHub first) | ||
| - keep a small, stable core with clear extension points | ||
| - handle trunk-based development and monorepo workflows cleanly | ||
| ## Scope and non-goals | ||
| > **Status:** early, alpha-stage development. Breaking changes are expected | ||
| > before `1.0.0`. | ||
| In scope: | ||
| ## Quick start | ||
| - semantic version planning from commits | ||
| - changelog generation | ||
| - release PR automation | ||
| - tags + SCM release metadata (e.g. GitHub Releases) | ||
| Install: | ||
| Out of scope (intentional): | ||
| ```bash | ||
| pnpm add -D versionary # or npm install --save-dev versionary | ||
| ``` | ||
| - publishing artifacts to language registries | ||
| - replacing package-specific publish tooling | ||
| - external/user-provided plugin loading | ||
| Add a `versionary.jsonc` at the repo root: | ||
| Use your CI/CD platform for registry publishing, triggered from a created | ||
| release/tag. | ||
| ```jsonc | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json", | ||
| "version": 1, | ||
| "release-type": "node" | ||
| } | ||
| ``` | ||
| ## Current status vs roadmap | ||
| Check it and preview the next release: | ||
| Current implementation focuses on: | ||
| ```bash | ||
| npx versionary verify | ||
| npx versionary plan | ||
| ``` | ||
| - strategy-based version updates (`simple`, `node`, `rust`, `r`, `latex`, | ||
| `python`, `julia`) | ||
| - release planning and changelog generation | ||
| - review-mode vs direct-mode release flow | ||
| - a static internal SCM client (`github` provider today) | ||
| Then automate it in CI—see the | ||
| [Getting started](https://versionary.dev/guide/getting-started) and | ||
| [GitHub Actions](https://versionary.dev/guide/github-actions) | ||
| guides. | ||
| Planned/harder areas include deeper monorepo ergonomics, broader SCM coverage, | ||
| and stronger failure recovery around release steps. | ||
| ## Documentation | ||
| ## Adding a new release strategy | ||
| The full documentation lives at <https://versionary.dev/>: | ||
| 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`. | ||
| - [Getting started](https://versionary.dev/guide/getting-started) | ||
| - [GitHub Actions setup](https://versionary.dev/guide/github-actions) | ||
| (including tokens and permissions) | ||
| - [Release workflows](https://versionary.dev/guide/workflows) | ||
| - [Monorepos](https://versionary.dev/guide/monorepos) | ||
| - [Conventional Commits](https://versionary.dev/guide/conventional-commits) | ||
| and [Versioning](https://versionary.dev/guide/versioning) | ||
| - Reference: | ||
| [CLI](https://versionary.dev/reference/cli), | ||
| [Configuration](https://versionary.dev/reference/configuration), | ||
| [Strategies](https://versionary.dev/reference/strategies) | ||
| Checklist for new strategies: | ||
| ## Scope and non-goals | ||
| - 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, context)` 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-type` behavior and defaults | ||
| In scope: semantic version planning from commits, changelog generation, release | ||
| PR automation, and tags + SCM release metadata (e.g. GitHub Releases). | ||
| Current ecosystem policy defaults: | ||
| Out of scope (intentional): publishing artifacts to language registries, | ||
| replacing package-specific publish tooling, and external/user-provided plugin | ||
| loading. Use your CI/CD platform for registry publishing, triggered from a | ||
| created release/tag. | ||
| - changelog source for publish: | ||
| - root target uses root `changelog-file` | ||
| - package target uses package changelog defaults: | ||
| - `packages.<path>.changelog-file` when configured | ||
| - otherwise `NEWS.md` for R targets or `CHANGELOG.md` for other targets at | ||
| `<package-path>/<default-file>` | ||
| - lockfiles: | ||
| - Node strategy updates root `package-lock.json`/`npm-shrinkwrap.json` when | ||
| present | ||
| - Rust release PR prep refreshes all discovered `Cargo.lock` files | ||
| - Python strategy refreshes any `poetry.lock`/`uv.lock`/`pdm.lock` at the | ||
| package root by shelling out to the matching tool (`poetry lock | ||
| --no-update`, `uv lock`, `pdm lock --update-reuse`); the corresponding | ||
| binary must be on `PATH` | ||
| - workspace/inheritance: | ||
| - Rust supports `version.workspace = true` via `[workspace.package].version` | ||
| - other strategies should document equivalent inheritance behavior explicitly | ||
| ## Contributing | ||
| See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, commands, and | ||
| commit conventions. | ||
| ## Architecture layout (canonical) | ||
| Current runtime code uses a flat `src/` layout with clear module boundaries: | ||
| Runtime code uses a flat `src/` layout with clear module boundaries: | ||
@@ -117,366 +105,32 @@ - `src/cli/`: command router (`run`, `verify`, `plan`, `changelog`, `pr`, `release`) | ||
| - `src/git/`: git commit/range and repository URL helpers | ||
| - `src/verify/`: repository/config verification | ||
| - `src/types/`: shared config/plugin-facing types | ||
| Configuration is loaded from `versionary.jsonc` by default (or | ||
| `versionary.json`). | ||
| `versionary.json`). The config schema lives in `src/config/schema.ts`; the | ||
| editor-facing `schemas/config.json` is generated via `pnpm gen:schema`. | ||
| Schema URL for editor support: | ||
| ## Adding a new release strategy | ||
| - `https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json` | ||
| 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`. | ||
| ## Config (manifest style) | ||
| Checklist for new strategies: | ||
| For a quick trial, use: | ||
| - 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, context)` 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-type` behavior and defaults | ||
| - `version-file` (default `version.txt`) as version source | ||
| - `changelog-file` (default `CHANGELOG.md`) as release notes output | ||
| - `release-type: "node"` uses `package.json` as version source and updates it | ||
| during release PR prep | ||
| - `release-type: "r"` uses `DESCRIPTION` as version source and updates the | ||
| `Version:` field | ||
| - `release-type: "rust"` uses Cargo manifests (`Cargo.toml`) as version source; | ||
| `version-file` must point to a `Cargo.toml` (default: `Cargo.toml`) | ||
| - `release-type: "latex"` uses `build.lua` as version source and updates LaTeX | ||
| `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` metadata in `src/**/*.dtx` | ||
| using the release commit date | ||
| - `release-type: "python"` uses `pyproject.toml` (default) and updates | ||
| `[project].version` and/or `[tool.poetry].version`; point `version-file` at a | ||
| Python source file (e.g. `src/<pkg>/__init__.py`) to update a `__version__` | ||
| assignment instead. Refreshes `poetry.lock`/`uv.lock`/`pdm.lock` at the | ||
| package root if present | ||
| - `release-type: "julia"` uses `Project.toml` (default) as version source and | ||
| updates the top-level `version` field (Julia keeps `version`/`name` as root | ||
| keys, not under a section) | ||
| - `release-type` can also be an array of strategy names to compose them across | ||
| manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first | ||
| entry is the *primary* (drives `readVersion`, `readPackageName`, and consumes | ||
| any `version-file` override); each *secondary* writes its default manifest | ||
| with the same target version. Common combinations: | ||
| - `["python", "rust"]` — PyO3/maturin (`pyproject.toml` + `Cargo.toml` + | ||
| `Cargo.lock`) | ||
| - `["node", "rust"]` — napi-rs (`package.json` + `Cargo.toml` + `Cargo.lock`) | ||
| - `["r", "rust"]` — R packages with embedded Rust crates (note: nested | ||
| `src/rust/Cargo.toml` layouts are not supported by the array form yet — | ||
| use a single strategy until per-strategy `version-file` overrides land) | ||
| - simple/default strategy keeps `version.txt` as source of truth and does not | ||
| update `package.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 to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit | ||
| auto-transition to `1.0.0` on a breaking release | ||
| - review mode (`review-mode`): `pr` (PR/MR style) or `direct` (no review | ||
| request) | ||
| - `release-draft` (default `false`) publishes GitHub releases as drafts when | ||
| enabled | ||
| - `release-reference-comments` controls release comments on linked issues/PRs: | ||
| - `off` (default): do not post comments | ||
| - `best-effort`: post comments and continue on API/permission failures | ||
| - `strict`: fail release if comment posting fails | ||
| - comments are authored by the account that owns the configured token; see | ||
| [Comment and commit author identity](#comment-and-commit-author-identity) | ||
| to post them under a bot identity | ||
| - optional monorepo planning with `monorepo-mode` and `packages`: | ||
| - `independent` computes package bumps per path | ||
| - `fixed` computes one shared bump across configured package paths | ||
| - per-package `package-name` can override release identity (labels + tag base) | ||
| - per-package `changelog-file` writes package release notes to | ||
| `<package-path>/<changelog-file>` | ||
| - per-package `follows` declares an asymmetric version link to one or more | ||
| source packages: when any source bumps, the follower releases too, with | ||
| bump = `max(own bump, max(source bumps))`. The follower's changelog gets a | ||
| `### Dependencies` section listing the followed sources. Use it when one | ||
| package bundles another's artifact (e.g. an editor extension that ships | ||
| the CLI binary). Cycles, self-references, unknown source paths, and | ||
| combining `follows` with `monorepo-mode: "fixed"` are config errors. | ||
| `follows` is non-transitive: A follows B does not imply A follows what B | ||
| follows. | ||
| - per-package `exclude-paths` drops commits that only touch the listed paths | ||
| (relative to the package) from that package's bump and changelog. A | ||
| top-level `exclude-paths` applies to every package; the effective excludes | ||
| for a package are the union of the top-level list and the package's own | ||
| list. The top-level list also applies to a single-package (non-`packages`) | ||
| repository. | ||
| - per-package `allow-stable-major` overrides the top-level setting for that | ||
| package's own bump (including dependency-propagation and `follows`-driven | ||
| bumps), so a `0.y.z` package can transition to `1.0.0` on a breaking release | ||
| independently of its siblings. In `fixed` mode the single shared version is | ||
| governed by the top-level `allow-stable-major` only. | ||
| ```jsonc | ||
| // Editor extension that bundles the root CLI artifact | ||
| { | ||
| "version": 1, | ||
| "release-type": "rust", | ||
| "monorepo-mode": "independent", | ||
| "packages": { | ||
| ".": { "exclude-paths": ["editors"] }, | ||
| "editors/code": { | ||
| "release-type": "node", | ||
| "package-name": "panache-code", | ||
| "follows": ["."] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| Rust strategy examples: | ||
| ```jsonc | ||
| // Single crate | ||
| { | ||
| "release-type": "rust", | ||
| "version-file": "Cargo.toml" | ||
| } | ||
| ``` | ||
| ```jsonc | ||
| // 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 = true` by updating | ||
| `[workspace.package].version` in the owning workspace manifest | ||
| - updates internal workspace dependency versions when the dependency name | ||
| matches another targeted crate name | ||
| - refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists | ||
| 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. | ||
| ### Monorepo release names and tag naming | ||
| For independent monorepo targets, Versionary derives release tags as: | ||
| - root package (`"."`): `v<version>` | ||
| - non-root package: `<release-name>-v<version>` | ||
| `release-name` precedence is: | ||
| 1. `packages.<path>.package-name` (explicit override) | ||
| 2. strategy-native package name from version file: | ||
| - Node: `package.json` `name` | ||
| - Rust: `Cargo.toml` `[package].name` | ||
| - R: `DESCRIPTION` `Package:` | ||
| 3. 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. | ||
| ## Commit parsing and release analysis | ||
| 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 `!` and `BREAKING CHANGE` / `BREAKING-CHANGE` | ||
| footers | ||
| - 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!:` or `BREAKING 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 verify` | ||
| - `pnpm run` (default orchestration: no-op, create/update release PR, or publish | ||
| release based on context) | ||
| - `pnpm run -- --json` (machine-readable orchestration result) | ||
| - `pnpm plan` | ||
| - `pnpm changelog -- --write` | ||
| - `pnpm pr` | ||
| - `pnpm 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. | ||
| ### Moloch migration example (semantic-release -> versionary) | ||
| For LaTeX projects like `moloch`, use `release-type: "latex"` so Versionary: | ||
| - bumps `build.lua` version | ||
| - updates `src/**/*.dtx` `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` entries | ||
| with the release commit date (`git show --format=%cs <sha>`) | ||
| Example `versionary.jsonc` for `moloch`: | ||
| ```jsonc | ||
| { | ||
| "version": 1, | ||
| "review-mode": "pr", | ||
| "release-type": "latex", | ||
| "version-file": "build.lua", | ||
| "changelog-file": "CHANGELOG.md", | ||
| "release-branch": "versionary/release" | ||
| } | ||
| ``` | ||
| For first-run bootstrapping, set `bootstrap-sha` (similar to release-please). | ||
| Subsequent runs use the baseline state file. | ||
| ## Release retry and recovery behavior | ||
| 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. | ||
| ## SCM API model | ||
| Versionary currently uses a static internal SCM client model: | ||
| - `src/scm/types.ts` defines the `ScmClient` contract | ||
| - `src/scm/client.ts` returns the active provider client | ||
| - current provider is `github` via `src/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`. | ||
| ### GitHub integration: env, permissions, and flow | ||
| Required environment for the GitHub SCM provider: | ||
| - `GITHUB_REPOSITORY` (format: `owner/repo`) | ||
| - one token env var: `VERSIONARY_PR_TOKEN` or `GH_TOKEN` or `GITHUB_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; `review` is a backward-compatible alias): `pnpm run run` | ||
| prepares/updates the release branch and creates or | ||
| updates a release PR | ||
| - `direct`: `pnpm run run` prepares/updates the release branch and skips review | ||
| request creation | ||
| Concise GitHub Actions examples: | ||
| ```yaml | ||
| # 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 }} | ||
| ``` | ||
| ```yaml | ||
| # 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.) | ||
| #### Comment and commit author identity | ||
| Two distinct identities are at play; keep them apart. | ||
| **Release-reference comments, the GitHub Release, and the tag/branch push** are | ||
| attributed to the account that owns the token you provide. There is no GitHub | ||
| API to set a custom author independent of the token, so this identity always | ||
| follows the token's account: | ||
| - the workflow's default `GITHUB_TOKEN` acts as `github-actions[bot]` — the | ||
| common case, and what most `semantic-release` setups show | ||
| - a **personal access token (PAT)** acts as your own user | ||
| - a **dedicated bot user account** acts as that account (for example | ||
| `semantic-release`'s own `@semantic-release-bot`): create a separate GitHub | ||
| user, generate a PAT for it, and store it as the release token | ||
| - a **GitHub App installation token** (e.g. minted with | ||
| `actions/create-github-app-token`) acts as `<app-name>[bot]` | ||
| **The release commit's committer** comes from git's `user.name`/`user.email`, | ||
| not the token. When neither is configured (e.g. a bare CI runner), Versionary | ||
| defaults it to `github-actions[bot]`, so no `git config` step is needed in your | ||
| workflow; an existing identity (local, global, or the one the GitHub Action | ||
| wrapper sets) is left untouched. | ||
| The release-reference comment body itself is signed by Versionary regardless of | ||
| which account posts it. | ||
| Action outputs: | ||
| - `action`: `noop`, `pr-prepared`, `release-published`, `release-skipped` | ||
| - `message`: human-readable summary | ||
| - `release_created`: `"true"` when at least one release was published | ||
| - `tag_name`: first published tag (for single-target flows) | ||
| - `tag_names`: JSON array of published tags | ||
| - `review_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. | ||
| ## Install from GitHub | ||
@@ -495,2 +149,6 @@ | ||
| The package runs a `prepare` build during git installation so the `versionary` | ||
| CLI binary is available after `pnpm install`. | ||
| CLI binary is available after install. | ||
| ## License | ||
| [MIT](LICENSE) |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
6265
0.87%262421
-4.21%6
50%153
-69.09%