versionary 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -435
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +8 -0
- package/dist/release/artifact-rules.js +41 -6
- package/dist/release/pr.js +9 -0
- package/dist/types/config.d.ts +1 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,111 +1,99 @@
|
|
|
1
1
|
# Versionary
|
|
2
2
|
|
|
3
3
|
Versionary is a software-agnostic automated release tool focused on SemVer,
|
|
4
|
-
|
|
4
|
+
Conventional Commits, release-PR workflows, and extensibility.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
📖 **Documentation: <https://versionary.dev/>**
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
and `release-please`.
|
|
8
|
+
## Why this exists
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
10
|
+
Versionary is a practical middle ground between
|
|
11
|
+
[`semantic-release`](https://github.com/semantic-release/semantic-release) and
|
|
12
|
+
[`release-please`](https://github.com/googleapis/release-please):
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
- like `semantic-release`, it supports **direct release execution**;
|
|
15
|
+
- like `release-please`, it supports a **release PR workflow** so maintainers
|
|
16
|
+
can preview and review changes before publication.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
It keeps versioning, changelog generation, tagging, and SCM release metadata in
|
|
19
|
+
one tool, while leaving package publication (npm, crates.io, etc.) to dedicated
|
|
20
|
+
CI workflows triggered by tags or releases.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
It is built to:
|
|
22
23
|
|
|
23
24
|
- support both direct releases and release-PR-gated releases
|
|
24
|
-
- work across
|
|
25
|
-
- stay SCM-agnostic at the core with built-in integration
|
|
26
|
-
|
|
27
|
-
- keep a small, stable core with explicit extension points
|
|
25
|
+
- work across ecosystems (Node, Rust, Python, R, Julia, LaTeX, …)
|
|
26
|
+
- stay SCM-agnostic at the core with built-in integration (GitHub first)
|
|
27
|
+
- keep a small, stable core with clear extension points
|
|
28
28
|
- handle trunk-based development and monorepo workflows cleanly
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
> **Status:** early, alpha-stage development. Breaking changes are expected
|
|
31
|
+
> before `1.0.0`.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
## Quick start
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
- changelog generation
|
|
36
|
-
- release PR automation
|
|
37
|
-
- tags + SCM release metadata (e.g. GitHub Releases)
|
|
35
|
+
Install:
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add -D versionary # or npm install --save-dev versionary
|
|
39
|
+
```
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
- replacing package-specific publish tooling
|
|
43
|
-
- external/user-provided plugin loading
|
|
41
|
+
Add a `versionary.jsonc` at the repo root:
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
```jsonc
|
|
44
|
+
{
|
|
45
|
+
"$schema": "https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json",
|
|
46
|
+
"version": 1,
|
|
47
|
+
"release-type": "node"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
Check it and preview the next release:
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
```bash
|
|
54
|
+
npx versionary verify
|
|
55
|
+
npx versionary plan
|
|
56
|
+
```
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
- a static internal SCM client (`github` provider today)
|
|
58
|
+
Then automate it in CI—see the
|
|
59
|
+
[Getting started](https://versionary.dev/guide/getting-started) and
|
|
60
|
+
[GitHub Actions](https://versionary.dev/guide/github-actions)
|
|
61
|
+
guides.
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
and stronger failure recovery around release steps.
|
|
63
|
+
## Documentation
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
The full documentation lives at <https://versionary.dev/>:
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
- [Getting started](https://versionary.dev/guide/getting-started)
|
|
68
|
+
- [GitHub Actions setup](https://versionary.dev/guide/github-actions)
|
|
69
|
+
(including tokens and permissions)
|
|
70
|
+
- [Release workflows](https://versionary.dev/guide/workflows)
|
|
71
|
+
- [Monorepos](https://versionary.dev/guide/monorepos)
|
|
72
|
+
- [Conventional Commits](https://versionary.dev/guide/conventional-commits)
|
|
73
|
+
and [Versioning](https://versionary.dev/guide/versioning)
|
|
74
|
+
- Reference:
|
|
75
|
+
[CLI](https://versionary.dev/reference/cli),
|
|
76
|
+
[Configuration](https://versionary.dev/reference/configuration),
|
|
77
|
+
[Strategies](https://versionary.dev/reference/strategies)
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
## Scope and non-goals
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- implement `readVersion(cwd, config)` with explicit malformed-file errors
|
|
73
|
-
- implement `writeVersion(cwd, config, version)` returning deterministic updated
|
|
74
|
-
file paths
|
|
75
|
-
- optionally implement `readPackageName(cwd, config)` so monorepo release tags
|
|
76
|
-
can derive from language metadata (similar to Node/Rust/R)
|
|
77
|
-
- optionally implement `propagateDependentPatchImpacts(cwd, packages)` if
|
|
78
|
-
dependency updates in this ecosystem should trigger dependent package patch
|
|
79
|
-
bumps
|
|
80
|
-
- optionally implement `finalizeVersionWrites(cwd, writes, context)` for
|
|
81
|
-
ecosystem post-processing after all target version files are written
|
|
82
|
-
- add focused strategy tests for ecosystem-specific behavior and edge cases
|
|
83
|
-
- add/extend strategy contract tests in `tests/strategy-contract.test.ts`
|
|
84
|
-
- update schema/docs for new `release-type` behavior and defaults
|
|
81
|
+
In scope: semantic version planning from commits, changelog generation, release
|
|
82
|
+
PR automation, and tags + SCM release metadata (e.g. GitHub Releases).
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- Node strategy updates root `package-lock.json`/`npm-shrinkwrap.json` when
|
|
96
|
-
present
|
|
97
|
-
- Rust release PR prep refreshes all discovered `Cargo.lock` files
|
|
98
|
-
- Python strategy refreshes any `poetry.lock`/`uv.lock`/`pdm.lock` at the
|
|
99
|
-
package root by shelling out to the matching tool (`poetry lock
|
|
100
|
-
--no-update`, `uv lock`, `pdm lock --update-reuse`); the corresponding
|
|
101
|
-
binary must be on `PATH`
|
|
102
|
-
- workspace/inheritance:
|
|
103
|
-
- Rust supports `version.workspace = true` via `[workspace.package].version`
|
|
104
|
-
- other strategies should document equivalent inheritance behavior explicitly
|
|
84
|
+
Out of scope (intentional): publishing artifacts to language registries,
|
|
85
|
+
replacing package-specific publish tooling, and external/user-provided plugin
|
|
86
|
+
loading. Use your CI/CD platform for registry publishing, triggered from a
|
|
87
|
+
created release/tag.
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
|
|
91
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup, commands, and
|
|
92
|
+
commit conventions.
|
|
105
93
|
|
|
106
94
|
## Architecture layout (canonical)
|
|
107
95
|
|
|
108
|
-
|
|
96
|
+
Runtime code uses a flat `src/` layout with clear module boundaries:
|
|
109
97
|
|
|
110
98
|
- `src/cli/`: command router (`run`, `verify`, `plan`, `changelog`, `pr`, `release`)
|
|
111
99
|
- `src/release/`: release orchestration (plan/changelog/PR/release/state/recovery)
|
|
@@ -114,369 +102,35 @@ Current runtime code uses a flat `src/` layout with clear module boundaries:
|
|
|
114
102
|
- `src/scm/`: SCM client contracts and provider implementation(s)
|
|
115
103
|
- `src/config/`: config loading and schema validation
|
|
116
104
|
- `src/git/`: git commit/range and repository URL helpers
|
|
117
|
-
- `src/verify/`: repository/config verification
|
|
118
105
|
- `src/types/`: shared config/plugin-facing types
|
|
119
106
|
|
|
120
107
|
Configuration is loaded from `versionary.jsonc` by default (or
|
|
121
|
-
`versionary.json`).
|
|
122
|
-
|
|
123
|
-
Schema URL for editor support:
|
|
124
|
-
|
|
125
|
-
- `https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json`
|
|
126
|
-
|
|
127
|
-
## Config (manifest style)
|
|
128
|
-
|
|
129
|
-
For a quick trial, use:
|
|
130
|
-
|
|
131
|
-
- `version-file` (default `version.txt`) as version source
|
|
132
|
-
- `changelog-file` (default `CHANGELOG.md`) as release notes output
|
|
133
|
-
- `release-type: "node"` uses `package.json` as version source and updates it
|
|
134
|
-
during release PR prep
|
|
135
|
-
- `release-type: "r"` uses `DESCRIPTION` as version source and updates the
|
|
136
|
-
`Version:` field
|
|
137
|
-
- `release-type: "rust"` uses Cargo manifests (`Cargo.toml`) as version source;
|
|
138
|
-
`version-file` must point to a `Cargo.toml` (default: `Cargo.toml`)
|
|
139
|
-
- `release-type: "latex"` uses `build.lua` as version source and updates LaTeX
|
|
140
|
-
`\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` metadata in `src/**/*.dtx`
|
|
141
|
-
using the release commit date
|
|
142
|
-
- `release-type: "python"` uses `pyproject.toml` (default) and updates
|
|
143
|
-
`[project].version` and/or `[tool.poetry].version`; point `version-file` at a
|
|
144
|
-
Python source file (e.g. `src/<pkg>/__init__.py`) to update a `__version__`
|
|
145
|
-
assignment instead. Refreshes `poetry.lock`/`uv.lock`/`pdm.lock` at the
|
|
146
|
-
package root if present
|
|
147
|
-
- `release-type: "julia"` uses `Project.toml` (default) as version source and
|
|
148
|
-
updates the top-level `version` field (Julia keeps `version`/`name` as root
|
|
149
|
-
keys, not under a section)
|
|
150
|
-
- `release-type` can also be an array of strategy names to compose them across
|
|
151
|
-
manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first
|
|
152
|
-
entry is the *primary* (drives `readVersion`, `readPackageName`, and consumes
|
|
153
|
-
any `version-file` override); each *secondary* writes its default manifest
|
|
154
|
-
with the same target version. Common combinations:
|
|
155
|
-
- `["python", "rust"]` — PyO3/maturin (`pyproject.toml` + `Cargo.toml` +
|
|
156
|
-
`Cargo.lock`)
|
|
157
|
-
- `["node", "rust"]` — napi-rs (`package.json` + `Cargo.toml` + `Cargo.lock`)
|
|
158
|
-
- `["r", "rust"]` — R packages with embedded Rust crates (note: nested
|
|
159
|
-
`src/rust/Cargo.toml` layouts are not supported by the array form yet —
|
|
160
|
-
use a single strategy until per-strategy `version-file` overrides land)
|
|
161
|
-
- simple/default strategy keeps `version.txt` as source of truth and does not
|
|
162
|
-
update `package.json`
|
|
163
|
-
- stable release branch (`release-branch`, default: `versionary/release`) so
|
|
164
|
-
release PRs are updated in-place
|
|
165
|
-
- `baseline-file` (default `.versionary-manifest.json`) tracks baseline SHA for
|
|
166
|
-
deterministic commit ranges independent of tags
|
|
167
|
-
- pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
|
|
168
|
-
changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
|
|
169
|
-
auto-transition to `1.0.0` on a breaking release
|
|
170
|
-
- review mode (`review-mode`): `pr` (PR/MR style) or `direct` (no review
|
|
171
|
-
request)
|
|
172
|
-
- `release-draft` (default `false`) publishes GitHub releases as drafts when
|
|
173
|
-
enabled
|
|
174
|
-
- `release-reference-comments` controls release comments on linked issues/PRs:
|
|
175
|
-
- `off` (default): do not post comments
|
|
176
|
-
- `best-effort`: post comments and continue on API/permission failures
|
|
177
|
-
- `strict`: fail release if comment posting fails
|
|
178
|
-
- comments are authored by the account that owns the configured token; see
|
|
179
|
-
[Comment and commit author identity](#comment-and-commit-author-identity)
|
|
180
|
-
to post them under a bot identity
|
|
181
|
-
- optional monorepo planning with `monorepo-mode` and `packages`:
|
|
182
|
-
- `independent` computes package bumps per path
|
|
183
|
-
- `fixed` computes one shared bump across configured package paths
|
|
184
|
-
- per-package `package-name` can override release identity (labels + tag base)
|
|
185
|
-
- per-package `changelog-file` writes package release notes to
|
|
186
|
-
`<package-path>/<changelog-file>`
|
|
187
|
-
- per-package `follows` declares an asymmetric version link to one or more
|
|
188
|
-
source packages: when any source bumps, the follower releases too, with
|
|
189
|
-
bump = `max(own bump, max(source bumps))`. The follower's changelog gets a
|
|
190
|
-
`### Dependencies` section listing the followed sources. Use it when one
|
|
191
|
-
package bundles another's artifact (e.g. an editor extension that ships
|
|
192
|
-
the CLI binary). Cycles, self-references, unknown source paths, and
|
|
193
|
-
combining `follows` with `monorepo-mode: "fixed"` are config errors.
|
|
194
|
-
`follows` is non-transitive: A follows B does not imply A follows what B
|
|
195
|
-
follows.
|
|
196
|
-
- per-package `exclude-paths` drops commits that only touch the listed paths
|
|
197
|
-
(relative to the package) from that package's bump and changelog. A
|
|
198
|
-
top-level `exclude-paths` applies to every package; the effective excludes
|
|
199
|
-
for a package are the union of the top-level list and the package's own
|
|
200
|
-
list. The top-level list also applies to a single-package (non-`packages`)
|
|
201
|
-
repository.
|
|
202
|
-
- per-package `allow-stable-major` overrides the top-level setting for that
|
|
203
|
-
package's own bump (including dependency-propagation and `follows`-driven
|
|
204
|
-
bumps), so a `0.y.z` package can transition to `1.0.0` on a breaking release
|
|
205
|
-
independently of its siblings. In `fixed` mode the single shared version is
|
|
206
|
-
governed by the top-level `allow-stable-major` only.
|
|
207
|
-
|
|
208
|
-
```jsonc
|
|
209
|
-
// Editor extension that bundles the root CLI artifact
|
|
210
|
-
{
|
|
211
|
-
"version": 1,
|
|
212
|
-
"release-type": "rust",
|
|
213
|
-
"monorepo-mode": "independent",
|
|
214
|
-
"packages": {
|
|
215
|
-
".": { "exclude-paths": ["editors"] },
|
|
216
|
-
"editors/code": {
|
|
217
|
-
"release-type": "node",
|
|
218
|
-
"package-name": "panache-code",
|
|
219
|
-
"follows": ["."]
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Rust strategy examples:
|
|
226
|
-
|
|
227
|
-
```jsonc
|
|
228
|
-
// Single crate
|
|
229
|
-
{
|
|
230
|
-
"release-type": "rust",
|
|
231
|
-
"version-file": "Cargo.toml"
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
```jsonc
|
|
236
|
-
// Workspace root (virtual or root crate + members)
|
|
237
|
-
{
|
|
238
|
-
"release-type": "rust",
|
|
239
|
-
"version-file": "Cargo.toml"
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
Current rust auto-update behavior (phase scope):
|
|
244
|
-
|
|
245
|
-
- updates crate versions in each targeted crate `[package].version`
|
|
246
|
-
- supports targeted crates using `version.workspace = true` by updating
|
|
247
|
-
`[workspace.package].version` in the owning workspace manifest
|
|
248
|
-
- updates internal workspace dependency versions when the dependency name
|
|
249
|
-
matches another targeted crate name
|
|
250
|
-
- refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
|
|
251
|
-
in repo root
|
|
252
|
-
- applies dependency version rewrites in:
|
|
253
|
-
- `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
|
|
254
|
-
- `[target.*.dependencies]`, `[target.*.dev-dependencies]`,
|
|
255
|
-
`[target.*.build-dependencies]`
|
|
256
|
-
|
|
257
|
-
Current rust non-goals/limits:
|
|
258
|
-
|
|
259
|
-
- does not update external dependency versions
|
|
260
|
-
- does not update `workspace.dependencies`
|
|
261
|
-
- does not add missing `version = ...` fields to dependency inline tables
|
|
262
|
-
- does not perform Cargo publish/release to crates.io
|
|
263
|
-
|
|
264
|
-
If `Cargo.lock` exists, `cargo` must be available in PATH during PR preparation.
|
|
265
|
-
|
|
266
|
-
### Monorepo release names and tag naming
|
|
267
|
-
|
|
268
|
-
For independent monorepo targets, Versionary derives release tags as:
|
|
269
|
-
|
|
270
|
-
- root package (`"."`): `v<version>`
|
|
271
|
-
- non-root package: `<release-name>-v<version>`
|
|
272
|
-
|
|
273
|
-
`release-name` precedence is:
|
|
274
|
-
|
|
275
|
-
1. `packages.<path>.package-name` (explicit override)
|
|
276
|
-
2. strategy-native package name from version file:
|
|
277
|
-
- Node: `package.json` `name`
|
|
278
|
-
- Rust: `Cargo.toml` `[package].name`
|
|
279
|
-
- R: `DESCRIPTION` `Package:`
|
|
280
|
-
3. package path fallback
|
|
281
|
-
|
|
282
|
-
When multiple packages resolve to the same `<release-name>` and version, the run
|
|
283
|
-
fails fast with a duplicate-tag error and suggests setting unique
|
|
284
|
-
`package-name` values.
|
|
285
|
-
|
|
286
|
-
## Commit parsing and release analysis
|
|
287
|
-
|
|
288
|
-
Release planning is based on Conventional Commit parsing semantics:
|
|
289
|
-
|
|
290
|
-
- parses type/scope/description from commit headers
|
|
291
|
-
- exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
|
|
292
|
-
`description`, `notes`, `references`, `mentions`, `revert`)
|
|
293
|
-
- separates parser output from release policy mapping (`inferReleaseType*`)
|
|
294
|
-
- recognizes breaking changes from `!` and `BREAKING CHANGE` / `BREAKING-CHANGE`
|
|
295
|
-
footers
|
|
296
|
-
- maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
|
|
297
|
-
- treats `revert:` commits as patch-releasable by default (and major if marked
|
|
298
|
-
breaking, e.g. `revert!:` or `BREAKING CHANGE`)
|
|
299
|
-
- suppresses commits that are reverted within the analyzed release window so
|
|
300
|
-
they do not affect bump/changelog output
|
|
301
|
-
- emits parser diagnostics for malformed headers/footers/references and
|
|
302
|
-
ambiguous revert messages
|
|
303
|
-
|
|
304
|
-
Commands:
|
|
108
|
+
`versionary.json`). The config schema lives in `src/config/schema.ts`; the
|
|
109
|
+
editor-facing `schemas/config.json` is generated via `pnpm gen:schema`.
|
|
305
110
|
|
|
306
|
-
|
|
307
|
-
- `pnpm run` (default orchestration: no-op, create/update release PR, or publish
|
|
308
|
-
release based on context)
|
|
309
|
-
- `pnpm run -- --json` (machine-readable orchestration result)
|
|
310
|
-
- `pnpm plan`
|
|
311
|
-
- `pnpm changelog -- --write`
|
|
312
|
-
- `pnpm pr`
|
|
313
|
-
- `pnpm release`
|
|
314
|
-
|
|
315
|
-
`pnpm pr` prepares release commit + branch and opens/updates a review request
|
|
316
|
-
through the SCM client. `pnpm run` is the recommended CI entrypoint and
|
|
317
|
-
auto-dispatches between PR/update and release publish.
|
|
318
|
-
|
|
319
|
-
### Moloch migration example (semantic-release -> versionary)
|
|
320
|
-
|
|
321
|
-
For LaTeX projects like `moloch`, use `release-type: "latex"` so Versionary:
|
|
322
|
-
|
|
323
|
-
- bumps `build.lua` version
|
|
324
|
-
- updates `src/**/*.dtx` `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` entries
|
|
325
|
-
with the release commit date (`git show --format=%cs <sha>`)
|
|
326
|
-
|
|
327
|
-
Example `versionary.jsonc` for `moloch`:
|
|
328
|
-
|
|
329
|
-
```jsonc
|
|
330
|
-
{
|
|
331
|
-
"version": 1,
|
|
332
|
-
"review-mode": "pr",
|
|
333
|
-
"release-type": "latex",
|
|
334
|
-
"version-file": "build.lua",
|
|
335
|
-
"changelog-file": "CHANGELOG.md",
|
|
336
|
-
"release-branch": "versionary/release"
|
|
337
|
-
}
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
341
|
-
Subsequent runs use the baseline state file.
|
|
342
|
-
|
|
343
|
-
## Release retry and recovery behavior
|
|
344
|
-
|
|
345
|
-
Release publish (`pnpm release` or the publish path in `pnpm run`) is idempotent
|
|
346
|
-
by target tag:
|
|
347
|
-
|
|
348
|
-
- if a tag already exists, Versionary reuses it rather than recreating it
|
|
349
|
-
- if release metadata already exists for the tag (e.g., GitHub Release), it is
|
|
350
|
-
reused
|
|
351
|
-
- if a prior run created/pushed the tag but failed before metadata creation, a
|
|
352
|
-
rerun creates the missing metadata and proceeds
|
|
353
|
-
|
|
354
|
-
Versionary fails fast when recovery is unsafe (for example, local and remote
|
|
355
|
-
tags with the same name point to different SHAs). In these cases, the error
|
|
356
|
-
message includes remediation guidance so CI logs are actionable.
|
|
357
|
-
|
|
358
|
-
## SCM API model
|
|
359
|
-
|
|
360
|
-
Versionary currently uses a static internal SCM client model:
|
|
361
|
-
|
|
362
|
-
- `src/scm/types.ts` defines the `ScmClient` contract
|
|
363
|
-
- `src/scm/client.ts` returns the active provider client
|
|
364
|
-
- current provider is `github` via `src/scm/github-plugin.ts`
|
|
365
|
-
|
|
366
|
-
There is no runtime discovery/loading of external SCM providers in the release
|
|
367
|
-
flow. Adding another provider is an internal extension: implement `ScmClient`
|
|
368
|
-
and wire provider selection in `src/scm/client.ts`.
|
|
369
|
-
|
|
370
|
-
### GitHub integration: env, permissions, and flow
|
|
371
|
-
|
|
372
|
-
Required environment for the GitHub SCM provider:
|
|
373
|
-
|
|
374
|
-
- `GITHUB_REPOSITORY` (format: `owner/repo`)
|
|
375
|
-
- one token env var: `VERSIONARY_PR_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN`
|
|
376
|
-
|
|
377
|
-
Token precedence is:
|
|
378
|
-
|
|
379
|
-
- `VERSIONARY_PR_TOKEN` > `GH_TOKEN` > `GITHUB_TOKEN`
|
|
380
|
-
|
|
381
|
-
Minimum GitHub token/repo permissions for Versionary-managed metadata:
|
|
382
|
-
|
|
383
|
-
- release PR create/update flow: `contents: write`, `pull-requests: write`
|
|
384
|
-
- release metadata flow (GitHub Release create/read): `contents: write`
|
|
385
|
-
|
|
386
|
-
`review-mode` behavior:
|
|
387
|
-
|
|
388
|
-
- `pr` (preferred; `review` is a backward-compatible alias): `pnpm run run`
|
|
389
|
-
prepares/updates the release branch and creates or
|
|
390
|
-
updates a release PR
|
|
391
|
-
- `direct`: `pnpm run run` prepares/updates the release branch and skips review
|
|
392
|
-
request creation
|
|
393
|
-
|
|
394
|
-
Concise GitHub Actions examples:
|
|
395
|
-
|
|
396
|
-
```yaml
|
|
397
|
-
# 1) Release PR / update flow (run on push to default branch)
|
|
398
|
-
permissions:
|
|
399
|
-
contents: write
|
|
400
|
-
pull-requests: write
|
|
401
|
-
|
|
402
|
-
steps:
|
|
403
|
-
- uses: actions/checkout@v6
|
|
404
|
-
with:
|
|
405
|
-
fetch-depth: 0
|
|
406
|
-
fetch-tags: true
|
|
407
|
-
- id: versionary
|
|
408
|
-
uses: jolars/versionary@v1
|
|
409
|
-
with:
|
|
410
|
-
token: ${{ secrets.RELEASE_TOKEN }}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
```yaml
|
|
414
|
-
# 2) Release publish flow after merge (release commit context)
|
|
415
|
-
permissions:
|
|
416
|
-
contents: write
|
|
417
|
-
|
|
418
|
-
steps:
|
|
419
|
-
- uses: actions/checkout@v6
|
|
420
|
-
with:
|
|
421
|
-
fetch-depth: 0
|
|
422
|
-
fetch-tags: true
|
|
423
|
-
- id: versionary
|
|
424
|
-
uses: jolars/versionary@v1
|
|
425
|
-
with:
|
|
426
|
-
token: ${{ secrets.RELEASE_TOKEN }}
|
|
427
|
-
- if: ${{ steps.versionary.outputs.release_created == 'true' }}
|
|
428
|
-
run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
`token` is used for both GitHub API calls and git push authentication in
|
|
432
|
-
the composite action. This means release-branch force-pushes are attributed to
|
|
433
|
-
that token and can trigger downstream workflows when using a PAT/App token.
|
|
434
|
-
(`github-token` remains as a deprecated alias for backward compatibility.)
|
|
435
|
-
|
|
436
|
-
#### Comment and commit author identity
|
|
437
|
-
|
|
438
|
-
Two distinct identities are at play; keep them apart.
|
|
439
|
-
|
|
440
|
-
**Release-reference comments, the GitHub Release, and the tag/branch push** are
|
|
441
|
-
attributed to the account that owns the token you provide. There is no GitHub
|
|
442
|
-
API to set a custom author independent of the token, so this identity always
|
|
443
|
-
follows the token's account:
|
|
444
|
-
|
|
445
|
-
- the workflow's default `GITHUB_TOKEN` acts as `github-actions[bot]` — the
|
|
446
|
-
common case, and what most `semantic-release` setups show
|
|
447
|
-
- a **personal access token (PAT)** acts as your own user
|
|
448
|
-
- a **dedicated bot user account** acts as that account (for example
|
|
449
|
-
`semantic-release`'s own `@semantic-release-bot`): create a separate GitHub
|
|
450
|
-
user, generate a PAT for it, and store it as the release token
|
|
451
|
-
- a **GitHub App installation token** (e.g. minted with
|
|
452
|
-
`actions/create-github-app-token`) acts as `<app-name>[bot]`
|
|
453
|
-
|
|
454
|
-
**The release commit's committer** comes from git's `user.name`/`user.email`,
|
|
455
|
-
not the token. When neither is configured (e.g. a bare CI runner), Versionary
|
|
456
|
-
defaults it to `github-actions[bot]`, so no `git config` step is needed in your
|
|
457
|
-
workflow; an existing identity (local, global, or the one the GitHub Action
|
|
458
|
-
wrapper sets) is left untouched.
|
|
459
|
-
|
|
460
|
-
The release-reference comment body itself is signed by Versionary regardless of
|
|
461
|
-
which account posts it.
|
|
462
|
-
|
|
463
|
-
Action outputs:
|
|
111
|
+
## Adding a new release strategy
|
|
464
112
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
- `tag_name`: first published tag (for single-target flows)
|
|
469
|
-
- `tag_names`: JSON array of published tags
|
|
470
|
-
- `review_url`: review request URL when PR flow runs
|
|
113
|
+
New language strategies can be added internally without changing release
|
|
114
|
+
orchestration. A new strategy should implement the `VersionStrategy` contract in
|
|
115
|
+
`src/strategy/types.ts` and be wired in `src/strategy/resolve.ts`.
|
|
471
116
|
|
|
472
|
-
|
|
473
|
-
maintain a moving major tag (`v1`, `v2`, ...). A small release-triggered
|
|
474
|
-
workflow should update `v<major>` to the latest release tag so `uses:
|
|
475
|
-
jolars/versionary@v1` stays current without breaking major compatibility.
|
|
117
|
+
Checklist for new strategies:
|
|
476
118
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
119
|
+
- define strategy `name`
|
|
120
|
+
- define `getVersionFile(config)` defaults and config override behavior
|
|
121
|
+
- implement `readVersion(cwd, config)` with explicit malformed-file errors
|
|
122
|
+
- implement `writeVersion(cwd, config, version)` returning deterministic updated
|
|
123
|
+
file paths
|
|
124
|
+
- optionally implement `readPackageName(cwd, config)` so monorepo release tags
|
|
125
|
+
can derive from language metadata (similar to Node/Rust/R)
|
|
126
|
+
- optionally implement `propagateDependentPatchImpacts(cwd, packages)` if
|
|
127
|
+
dependency updates in this ecosystem should trigger dependent package patch
|
|
128
|
+
bumps
|
|
129
|
+
- optionally implement `finalizeVersionWrites(cwd, writes, context)` for
|
|
130
|
+
ecosystem post-processing after all target version files are written
|
|
131
|
+
- add focused strategy tests for ecosystem-specific behavior and edge cases
|
|
132
|
+
- add/extend strategy contract tests in `tests/strategy-contract.test.ts`
|
|
133
|
+
- update schema/docs for new `release-type` behavior and defaults
|
|
480
134
|
|
|
481
135
|
## Install from GitHub
|
|
482
136
|
|
|
@@ -491,4 +145,8 @@ You can install directly from a git ref:
|
|
|
491
145
|
```
|
|
492
146
|
|
|
493
147
|
The package runs a `prepare` build during git installation so the `versionary`
|
|
494
|
-
CLI binary is available after
|
|
148
|
+
CLI binary is available after install.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
[MIT](LICENSE)
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
54
54
|
"field-path": z.ZodOptional<z.ZodString>;
|
|
55
55
|
jsonpath: z.ZodOptional<z.ZodString>;
|
|
56
56
|
pattern: z.ZodOptional<z.ZodString>;
|
|
57
|
+
replacement: z.ZodOptional<z.ZodString>;
|
|
57
58
|
}, z.core.$strip>>>;
|
|
58
59
|
follows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
60
|
}, z.core.$strict>>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -6,6 +6,7 @@ const artifactRuleSchema = z
|
|
|
6
6
|
"field-path": z.string().optional(),
|
|
7
7
|
jsonpath: z.string().optional(),
|
|
8
8
|
pattern: z.string().optional(),
|
|
9
|
+
replacement: z.string().optional(),
|
|
9
10
|
})
|
|
10
11
|
.superRefine((value, ctx) => {
|
|
11
12
|
const needsJsonPath = value.type === "json" ||
|
|
@@ -27,6 +28,13 @@ const artifactRuleSchema = z
|
|
|
27
28
|
path: ["pattern"],
|
|
28
29
|
});
|
|
29
30
|
}
|
|
31
|
+
if (needsJsonPath && value.replacement) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: z.ZodIssueCode.custom,
|
|
34
|
+
message: `${value.type} artifact rules do not support "replacement".`,
|
|
35
|
+
path: ["replacement"],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
30
38
|
if (value["field-path"] && value.jsonpath) {
|
|
31
39
|
ctx.addIssue({
|
|
32
40
|
code: z.ZodIssueCode.custom,
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
+
import { parseVersion } from "./semver.js";
|
|
5
6
|
const WILDCARD = Symbol("wildcard");
|
|
6
7
|
function isRecord(value) {
|
|
7
8
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -160,12 +161,34 @@ function parseRegexPattern(pattern) {
|
|
|
160
161
|
}
|
|
161
162
|
return new RegExp(pattern, "m");
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
const REPLACEMENT_TOKEN_PATTERN = /\{\{\s*([A-Za-z]+)\s*\}\}/gu;
|
|
165
|
+
function renderReplacementTemplate(template, version) {
|
|
166
|
+
const parsed = parseVersion(version);
|
|
167
|
+
const tokens = {
|
|
168
|
+
version,
|
|
169
|
+
major: String(parsed.major),
|
|
170
|
+
minor: String(parsed.minor),
|
|
171
|
+
patch: String(parsed.patch),
|
|
172
|
+
prerelease: parsed.prerelease.join("."),
|
|
173
|
+
build: parsed.build.join("."),
|
|
174
|
+
};
|
|
175
|
+
return template.replace(REPLACEMENT_TOKEN_PATTERN, (_whole, name) => {
|
|
176
|
+
const key = name.toLowerCase();
|
|
177
|
+
const value = tokens[key];
|
|
178
|
+
if (value === undefined) {
|
|
179
|
+
throw new Error(`Unknown replacement token "{{${name}}}". Supported tokens: ${Object.keys(tokens)
|
|
180
|
+
.map((token) => `{{${token}}}`)
|
|
181
|
+
.join(", ")}.`);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function applyRegexRule(content, pattern, version, replacementTemplate) {
|
|
164
187
|
const regex = parseRegexPattern(pattern);
|
|
165
188
|
const matchFlags = regex.flags.includes("g")
|
|
166
189
|
? regex.flags
|
|
167
190
|
: `${regex.flags}g`;
|
|
168
|
-
const globalRegex = new RegExp(regex.source, matchFlags);
|
|
191
|
+
const globalRegex = new RegExp(regex.source, matchFlags.includes("d") ? matchFlags : `${matchFlags}d`);
|
|
169
192
|
const matches = [...content.matchAll(globalRegex)];
|
|
170
193
|
if (matches.length !== 1) {
|
|
171
194
|
throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
|
|
@@ -179,9 +202,21 @@ function applyRegexRule(content, pattern, version) {
|
|
|
179
202
|
throw new Error("Regex match did not include an index.");
|
|
180
203
|
}
|
|
181
204
|
const full = match[0];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
// With a replacement template, render it and replace the entire match.
|
|
206
|
+
if (replacementTemplate !== undefined) {
|
|
207
|
+
const rendered = renderReplacementTemplate(replacementTemplate, version);
|
|
208
|
+
return `${content.slice(0, start)}${rendered}${content.slice(start + full.length)}`;
|
|
209
|
+
}
|
|
210
|
+
// Legacy behavior: substitute the full version into the first capture group,
|
|
211
|
+
// leaving the rest of the match intact. Splice by group indices rather than
|
|
212
|
+
// `String.replace` so literal `$` sequences and repeated group content are
|
|
213
|
+
// handled correctly.
|
|
214
|
+
const groupIndices = match.indices?.[1];
|
|
215
|
+
if (!groupIndices) {
|
|
216
|
+
return `${content.slice(0, start)}${version}${content.slice(start + full.length)}`;
|
|
217
|
+
}
|
|
218
|
+
const [groupStart, groupEnd] = groupIndices;
|
|
219
|
+
return `${content.slice(0, groupStart)}${version}${content.slice(groupEnd)}`;
|
|
185
220
|
}
|
|
186
221
|
function applyTomlRulePreservingFormatting(content, fieldPath, version) {
|
|
187
222
|
const simplePath = fieldPath.match(/^\$\.([A-Za-z0-9_-]+)$/u);
|
|
@@ -368,7 +403,7 @@ function applyArtifactRuleToContent(content, rule, version) {
|
|
|
368
403
|
if (!rule.pattern) {
|
|
369
404
|
throw new Error('regex artifact rules require "pattern".');
|
|
370
405
|
}
|
|
371
|
-
return applyRegexRule(content, rule.pattern, version);
|
|
406
|
+
return applyRegexRule(content, rule.pattern, version, rule.replacement);
|
|
372
407
|
}
|
|
373
408
|
if (rule.type === "json") {
|
|
374
409
|
const parsed = JSON.parse(content);
|
package/dist/release/pr.js
CHANGED
|
@@ -435,6 +435,15 @@ export function renderSimpleReviewRequestBody(version, previousVersion, commits,
|
|
|
435
435
|
}
|
|
436
436
|
return `${bodySections}\n\n${renderReviewRequestFooter()}`;
|
|
437
437
|
}
|
|
438
|
+
// Honor the plan's changelog format (e.g. r-news) so manual notes, which are
|
|
439
|
+
// authored/extracted relative to that format's heading depth, render at the
|
|
440
|
+
// right level. Falling through to renderReleaseNotesSection would always use
|
|
441
|
+
// the markdown-changelog convention and leave r-news highlights one level too
|
|
442
|
+
// high relative to the auto-generated sections.
|
|
443
|
+
if (plan && plan.changelogFormat === "r-news") {
|
|
444
|
+
const notes = renderReleasePlanChangelog(plan, { cwd, highlights });
|
|
445
|
+
return `${notes}\n\n${renderReviewRequestFooter()}`;
|
|
446
|
+
}
|
|
438
447
|
return renderReleaseNotesSection({
|
|
439
448
|
currentVersion: previousVersion,
|
|
440
449
|
nextVersion: version,
|
package/dist/types/config.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "versionary",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Automatic release framework based on conventional commits and semantic versioning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"releasing",
|
|
@@ -27,16 +27,18 @@
|
|
|
27
27
|
"main": "dist/index.js",
|
|
28
28
|
"types": "dist/index.d.ts",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"smol-toml": "^1.4.2",
|
|
31
30
|
"@octokit/rest": "^22.0.0",
|
|
32
31
|
"jsonc-parser": "^3.3.1",
|
|
32
|
+
"smol-toml": "^1.4.2",
|
|
33
33
|
"yaml": "^2.8.3",
|
|
34
34
|
"zod": "^4.1.12"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^
|
|
37
|
+
"@types/node": "^26.0.1",
|
|
38
38
|
"tsx": "^4.20.6",
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
+
"vite": "^8.0.0",
|
|
41
|
+
"vitepress": "^1.6.4",
|
|
40
42
|
"vitest": "^4.1.4"
|
|
41
43
|
},
|
|
42
44
|
"scripts": {
|
|
@@ -49,6 +51,9 @@
|
|
|
49
51
|
"plan": "tsx src/cli/index.ts plan",
|
|
50
52
|
"changelog": "tsx src/cli/index.ts changelog",
|
|
51
53
|
"pr": "tsx src/cli/index.ts pr",
|
|
52
|
-
"release": "tsx src/cli/index.ts release"
|
|
54
|
+
"release": "tsx src/cli/index.ts release",
|
|
55
|
+
"docs:dev": "vitepress dev docs",
|
|
56
|
+
"docs:build": "vitepress build docs",
|
|
57
|
+
"docs:preview": "vitepress preview docs"
|
|
53
58
|
}
|
|
54
59
|
}
|