versionary 0.30.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 +94 -428
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +9 -0
- package/dist/release/artifact-rules.js +41 -6
- package/dist/release/plan.js +11 -3
- package/dist/release/pr.js +9 -0
- package/dist/release/semver.d.ts +8 -6
- package/dist/release/semver.js +16 -9
- package/dist/strategy/julia.d.ts +2 -0
- package/dist/strategy/julia.js +115 -0
- package/dist/strategy/resolve.js +2 -0
- package/dist/types/config.d.ts +2 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,474 +1,136 @@
|
|
|
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)
|
|
112
100
|
- `src/strategy/`: strategy contracts, resolver, and built-in implementations
|
|
113
|
-
(`simple`, `node`, `rust`, `r`, `latex`, `python`)
|
|
101
|
+
(`simple`, `node`, `rust`, `r`, `latex`, `python`, `julia`)
|
|
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` can also be an array of strategy names to compose them across
|
|
148
|
-
manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first
|
|
149
|
-
entry is the *primary* (drives `readVersion`, `readPackageName`, and consumes
|
|
150
|
-
any `version-file` override); each *secondary* writes its default manifest
|
|
151
|
-
with the same target version. Common combinations:
|
|
152
|
-
- `["python", "rust"]` — PyO3/maturin (`pyproject.toml` + `Cargo.toml` +
|
|
153
|
-
`Cargo.lock`)
|
|
154
|
-
- `["node", "rust"]` — napi-rs (`package.json` + `Cargo.toml` + `Cargo.lock`)
|
|
155
|
-
- `["r", "rust"]` — R packages with embedded Rust crates (note: nested
|
|
156
|
-
`src/rust/Cargo.toml` layouts are not supported by the array form yet —
|
|
157
|
-
use a single strategy until per-strategy `version-file` overrides land)
|
|
158
|
-
- simple/default strategy keeps `version.txt` as source of truth and does not
|
|
159
|
-
update `package.json`
|
|
160
|
-
- stable release branch (`release-branch`, default: `versionary/release`) so
|
|
161
|
-
release PRs are updated in-place
|
|
162
|
-
- `baseline-file` (default `.versionary-manifest.json`) tracks baseline SHA for
|
|
163
|
-
deterministic commit ranges independent of tags
|
|
164
|
-
- pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
|
|
165
|
-
changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
|
|
166
|
-
auto-transition to `1.0.0` on a breaking release
|
|
167
|
-
- review mode (`review-mode`): `pr` (PR/MR style) or `direct` (no review
|
|
168
|
-
request)
|
|
169
|
-
- `release-draft` (default `false`) publishes GitHub releases as drafts when
|
|
170
|
-
enabled
|
|
171
|
-
- `release-reference-comments` controls release comments on linked issues/PRs:
|
|
172
|
-
- `off` (default): do not post comments
|
|
173
|
-
- `best-effort`: post comments and continue on API/permission failures
|
|
174
|
-
- `strict`: fail release if comment posting fails
|
|
175
|
-
- comments are authored by the account that owns the configured token; see
|
|
176
|
-
[Comment and commit author identity](#comment-and-commit-author-identity)
|
|
177
|
-
to post them under a bot identity
|
|
178
|
-
- optional monorepo planning with `monorepo-mode` and `packages`:
|
|
179
|
-
- `independent` computes package bumps per path
|
|
180
|
-
- `fixed` computes one shared bump across configured package paths
|
|
181
|
-
- per-package `package-name` can override release identity (labels + tag base)
|
|
182
|
-
- per-package `changelog-file` writes package release notes to
|
|
183
|
-
`<package-path>/<changelog-file>`
|
|
184
|
-
- per-package `follows` declares an asymmetric version link to one or more
|
|
185
|
-
source packages: when any source bumps, the follower releases too, with
|
|
186
|
-
bump = `max(own bump, max(source bumps))`. The follower's changelog gets a
|
|
187
|
-
`### Dependencies` section listing the followed sources. Use it when one
|
|
188
|
-
package bundles another's artifact (e.g. an editor extension that ships
|
|
189
|
-
the CLI binary). Cycles, self-references, unknown source paths, and
|
|
190
|
-
combining `follows` with `monorepo-mode: "fixed"` are config errors.
|
|
191
|
-
`follows` is non-transitive: A follows B does not imply A follows what B
|
|
192
|
-
follows.
|
|
193
|
-
- per-package `exclude-paths` drops commits that only touch the listed paths
|
|
194
|
-
(relative to the package) from that package's bump and changelog. A
|
|
195
|
-
top-level `exclude-paths` applies to every package; the effective excludes
|
|
196
|
-
for a package are the union of the top-level list and the package's own
|
|
197
|
-
list. The top-level list also applies to a single-package (non-`packages`)
|
|
198
|
-
repository.
|
|
199
|
-
|
|
200
|
-
```jsonc
|
|
201
|
-
// Editor extension that bundles the root CLI artifact
|
|
202
|
-
{
|
|
203
|
-
"version": 1,
|
|
204
|
-
"release-type": "rust",
|
|
205
|
-
"monorepo-mode": "independent",
|
|
206
|
-
"packages": {
|
|
207
|
-
".": { "exclude-paths": ["editors"] },
|
|
208
|
-
"editors/code": {
|
|
209
|
-
"release-type": "node",
|
|
210
|
-
"package-name": "panache-code",
|
|
211
|
-
"follows": ["."]
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Rust strategy examples:
|
|
218
|
-
|
|
219
|
-
```jsonc
|
|
220
|
-
// Single crate
|
|
221
|
-
{
|
|
222
|
-
"release-type": "rust",
|
|
223
|
-
"version-file": "Cargo.toml"
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
```jsonc
|
|
228
|
-
// Workspace root (virtual or root crate + members)
|
|
229
|
-
{
|
|
230
|
-
"release-type": "rust",
|
|
231
|
-
"version-file": "Cargo.toml"
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Current rust auto-update behavior (phase scope):
|
|
236
|
-
|
|
237
|
-
- updates crate versions in each targeted crate `[package].version`
|
|
238
|
-
- supports targeted crates using `version.workspace = true` by updating
|
|
239
|
-
`[workspace.package].version` in the owning workspace manifest
|
|
240
|
-
- updates internal workspace dependency versions when the dependency name
|
|
241
|
-
matches another targeted crate name
|
|
242
|
-
- refreshes `Cargo.lock` via `cargo generate-lockfile` when `Cargo.lock` exists
|
|
243
|
-
in repo root
|
|
244
|
-
- applies dependency version rewrites in:
|
|
245
|
-
- `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
|
|
246
|
-
- `[target.*.dependencies]`, `[target.*.dev-dependencies]`,
|
|
247
|
-
`[target.*.build-dependencies]`
|
|
248
|
-
|
|
249
|
-
Current rust non-goals/limits:
|
|
250
|
-
|
|
251
|
-
- does not update external dependency versions
|
|
252
|
-
- does not update `workspace.dependencies`
|
|
253
|
-
- does not add missing `version = ...` fields to dependency inline tables
|
|
254
|
-
- does not perform Cargo publish/release to crates.io
|
|
255
|
-
|
|
256
|
-
If `Cargo.lock` exists, `cargo` must be available in PATH during PR preparation.
|
|
257
|
-
|
|
258
|
-
### Monorepo release names and tag naming
|
|
259
|
-
|
|
260
|
-
For independent monorepo targets, Versionary derives release tags as:
|
|
261
|
-
|
|
262
|
-
- root package (`"."`): `v<version>`
|
|
263
|
-
- non-root package: `<release-name>-v<version>`
|
|
264
|
-
|
|
265
|
-
`release-name` precedence is:
|
|
266
|
-
|
|
267
|
-
1. `packages.<path>.package-name` (explicit override)
|
|
268
|
-
2. strategy-native package name from version file:
|
|
269
|
-
- Node: `package.json` `name`
|
|
270
|
-
- Rust: `Cargo.toml` `[package].name`
|
|
271
|
-
- R: `DESCRIPTION` `Package:`
|
|
272
|
-
3. package path fallback
|
|
273
|
-
|
|
274
|
-
When multiple packages resolve to the same `<release-name>` and version, the run
|
|
275
|
-
fails fast with a duplicate-tag error and suggests setting unique
|
|
276
|
-
`package-name` values.
|
|
277
|
-
|
|
278
|
-
## Commit parsing and release analysis
|
|
279
|
-
|
|
280
|
-
Release planning is based on Conventional Commit parsing semantics:
|
|
281
|
-
|
|
282
|
-
- parses type/scope/description from commit headers
|
|
283
|
-
- exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
|
|
284
|
-
`description`, `notes`, `references`, `mentions`, `revert`)
|
|
285
|
-
- separates parser output from release policy mapping (`inferReleaseType*`)
|
|
286
|
-
- recognizes breaking changes from `!` and `BREAKING CHANGE` / `BREAKING-CHANGE`
|
|
287
|
-
footers
|
|
288
|
-
- maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
|
|
289
|
-
- treats `revert:` commits as patch-releasable by default (and major if marked
|
|
290
|
-
breaking, e.g. `revert!:` or `BREAKING CHANGE`)
|
|
291
|
-
- suppresses commits that are reverted within the analyzed release window so
|
|
292
|
-
they do not affect bump/changelog output
|
|
293
|
-
- emits parser diagnostics for malformed headers/footers/references and
|
|
294
|
-
ambiguous revert messages
|
|
295
|
-
|
|
296
|
-
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`.
|
|
297
110
|
|
|
298
|
-
|
|
299
|
-
- `pnpm run` (default orchestration: no-op, create/update release PR, or publish
|
|
300
|
-
release based on context)
|
|
301
|
-
- `pnpm run -- --json` (machine-readable orchestration result)
|
|
302
|
-
- `pnpm plan`
|
|
303
|
-
- `pnpm changelog -- --write`
|
|
304
|
-
- `pnpm pr`
|
|
305
|
-
- `pnpm release`
|
|
306
|
-
|
|
307
|
-
`pnpm pr` prepares release commit + branch and opens/updates a review request
|
|
308
|
-
through the SCM client. `pnpm run` is the recommended CI entrypoint and
|
|
309
|
-
auto-dispatches between PR/update and release publish.
|
|
310
|
-
|
|
311
|
-
### Moloch migration example (semantic-release -> versionary)
|
|
312
|
-
|
|
313
|
-
For LaTeX projects like `moloch`, use `release-type: "latex"` so Versionary:
|
|
314
|
-
|
|
315
|
-
- bumps `build.lua` version
|
|
316
|
-
- updates `src/**/*.dtx` `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` entries
|
|
317
|
-
with the release commit date (`git show --format=%cs <sha>`)
|
|
318
|
-
|
|
319
|
-
Example `versionary.jsonc` for `moloch`:
|
|
320
|
-
|
|
321
|
-
```jsonc
|
|
322
|
-
{
|
|
323
|
-
"version": 1,
|
|
324
|
-
"review-mode": "pr",
|
|
325
|
-
"release-type": "latex",
|
|
326
|
-
"version-file": "build.lua",
|
|
327
|
-
"changelog-file": "CHANGELOG.md",
|
|
328
|
-
"release-branch": "versionary/release"
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
333
|
-
Subsequent runs use the baseline state file.
|
|
334
|
-
|
|
335
|
-
## Release retry and recovery behavior
|
|
336
|
-
|
|
337
|
-
Release publish (`pnpm release` or the publish path in `pnpm run`) is idempotent
|
|
338
|
-
by target tag:
|
|
339
|
-
|
|
340
|
-
- if a tag already exists, Versionary reuses it rather than recreating it
|
|
341
|
-
- if release metadata already exists for the tag (e.g., GitHub Release), it is
|
|
342
|
-
reused
|
|
343
|
-
- if a prior run created/pushed the tag but failed before metadata creation, a
|
|
344
|
-
rerun creates the missing metadata and proceeds
|
|
345
|
-
|
|
346
|
-
Versionary fails fast when recovery is unsafe (for example, local and remote
|
|
347
|
-
tags with the same name point to different SHAs). In these cases, the error
|
|
348
|
-
message includes remediation guidance so CI logs are actionable.
|
|
349
|
-
|
|
350
|
-
## SCM API model
|
|
351
|
-
|
|
352
|
-
Versionary currently uses a static internal SCM client model:
|
|
353
|
-
|
|
354
|
-
- `src/scm/types.ts` defines the `ScmClient` contract
|
|
355
|
-
- `src/scm/client.ts` returns the active provider client
|
|
356
|
-
- current provider is `github` via `src/scm/github-plugin.ts`
|
|
357
|
-
|
|
358
|
-
There is no runtime discovery/loading of external SCM providers in the release
|
|
359
|
-
flow. Adding another provider is an internal extension: implement `ScmClient`
|
|
360
|
-
and wire provider selection in `src/scm/client.ts`.
|
|
361
|
-
|
|
362
|
-
### GitHub integration: env, permissions, and flow
|
|
363
|
-
|
|
364
|
-
Required environment for the GitHub SCM provider:
|
|
365
|
-
|
|
366
|
-
- `GITHUB_REPOSITORY` (format: `owner/repo`)
|
|
367
|
-
- one token env var: `VERSIONARY_PR_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN`
|
|
368
|
-
|
|
369
|
-
Token precedence is:
|
|
370
|
-
|
|
371
|
-
- `VERSIONARY_PR_TOKEN` > `GH_TOKEN` > `GITHUB_TOKEN`
|
|
372
|
-
|
|
373
|
-
Minimum GitHub token/repo permissions for Versionary-managed metadata:
|
|
374
|
-
|
|
375
|
-
- release PR create/update flow: `contents: write`, `pull-requests: write`
|
|
376
|
-
- release metadata flow (GitHub Release create/read): `contents: write`
|
|
377
|
-
|
|
378
|
-
`review-mode` behavior:
|
|
379
|
-
|
|
380
|
-
- `pr` (preferred; `review` is a backward-compatible alias): `pnpm run run`
|
|
381
|
-
prepares/updates the release branch and creates or
|
|
382
|
-
updates a release PR
|
|
383
|
-
- `direct`: `pnpm run run` prepares/updates the release branch and skips review
|
|
384
|
-
request creation
|
|
385
|
-
|
|
386
|
-
Concise GitHub Actions examples:
|
|
387
|
-
|
|
388
|
-
```yaml
|
|
389
|
-
# 1) Release PR / update flow (run on push to default branch)
|
|
390
|
-
permissions:
|
|
391
|
-
contents: write
|
|
392
|
-
pull-requests: write
|
|
393
|
-
|
|
394
|
-
steps:
|
|
395
|
-
- uses: actions/checkout@v6
|
|
396
|
-
with:
|
|
397
|
-
fetch-depth: 0
|
|
398
|
-
fetch-tags: true
|
|
399
|
-
- id: versionary
|
|
400
|
-
uses: jolars/versionary@v1
|
|
401
|
-
with:
|
|
402
|
-
token: ${{ secrets.RELEASE_TOKEN }}
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
```yaml
|
|
406
|
-
# 2) Release publish flow after merge (release commit context)
|
|
407
|
-
permissions:
|
|
408
|
-
contents: write
|
|
409
|
-
|
|
410
|
-
steps:
|
|
411
|
-
- uses: actions/checkout@v6
|
|
412
|
-
with:
|
|
413
|
-
fetch-depth: 0
|
|
414
|
-
fetch-tags: true
|
|
415
|
-
- id: versionary
|
|
416
|
-
uses: jolars/versionary@v1
|
|
417
|
-
with:
|
|
418
|
-
token: ${{ secrets.RELEASE_TOKEN }}
|
|
419
|
-
- if: ${{ steps.versionary.outputs.release_created == 'true' }}
|
|
420
|
-
run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
`token` is used for both GitHub API calls and git push authentication in
|
|
424
|
-
the composite action. This means release-branch force-pushes are attributed to
|
|
425
|
-
that token and can trigger downstream workflows when using a PAT/App token.
|
|
426
|
-
(`github-token` remains as a deprecated alias for backward compatibility.)
|
|
427
|
-
|
|
428
|
-
#### Comment and commit author identity
|
|
429
|
-
|
|
430
|
-
Two distinct identities are at play; keep them apart.
|
|
431
|
-
|
|
432
|
-
**Release-reference comments, the GitHub Release, and the tag/branch push** are
|
|
433
|
-
attributed to the account that owns the token you provide. There is no GitHub
|
|
434
|
-
API to set a custom author independent of the token, so this identity always
|
|
435
|
-
follows the token's account:
|
|
436
|
-
|
|
437
|
-
- the workflow's default `GITHUB_TOKEN` acts as `github-actions[bot]` — the
|
|
438
|
-
common case, and what most `semantic-release` setups show
|
|
439
|
-
- a **personal access token (PAT)** acts as your own user
|
|
440
|
-
- a **dedicated bot user account** acts as that account (for example
|
|
441
|
-
`semantic-release`'s own `@semantic-release-bot`): create a separate GitHub
|
|
442
|
-
user, generate a PAT for it, and store it as the release token
|
|
443
|
-
- a **GitHub App installation token** (e.g. minted with
|
|
444
|
-
`actions/create-github-app-token`) acts as `<app-name>[bot]`
|
|
445
|
-
|
|
446
|
-
**The release commit's committer** comes from git's `user.name`/`user.email`,
|
|
447
|
-
not the token. When neither is configured (e.g. a bare CI runner), Versionary
|
|
448
|
-
defaults it to `github-actions[bot]`, so no `git config` step is needed in your
|
|
449
|
-
workflow; an existing identity (local, global, or the one the GitHub Action
|
|
450
|
-
wrapper sets) is left untouched.
|
|
451
|
-
|
|
452
|
-
The release-reference comment body itself is signed by Versionary regardless of
|
|
453
|
-
which account posts it.
|
|
454
|
-
|
|
455
|
-
Action outputs:
|
|
111
|
+
## Adding a new release strategy
|
|
456
112
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
- `tag_name`: first published tag (for single-target flows)
|
|
461
|
-
- `tag_names`: JSON array of published tags
|
|
462
|
-
- `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`.
|
|
463
116
|
|
|
464
|
-
|
|
465
|
-
maintain a moving major tag (`v1`, `v2`, ...). A small release-triggered
|
|
466
|
-
workflow should update `v<major>` to the latest release tag so `uses:
|
|
467
|
-
jolars/versionary@v1` stays current without breaking major compatibility.
|
|
117
|
+
Checklist for new strategies:
|
|
468
118
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
472
134
|
|
|
473
135
|
## Install from GitHub
|
|
474
136
|
|
|
@@ -483,4 +145,8 @@ You can install directly from a git ref:
|
|
|
483
145
|
```
|
|
484
146
|
|
|
485
147
|
The package runs a `prepare` build during git installation so the `versionary`
|
|
486
|
-
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
|
@@ -40,6 +40,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
40
40
|
"markdown-changelog": "markdown-changelog";
|
|
41
41
|
"r-news": "r-news";
|
|
42
42
|
}>>;
|
|
43
|
+
"allow-stable-major": z.ZodOptional<z.ZodBoolean>;
|
|
43
44
|
"exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
44
45
|
"extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
45
46
|
type: z.ZodEnum<{
|
|
@@ -53,6 +54,7 @@ export declare const configSchema: z.ZodObject<{
|
|
|
53
54
|
"field-path": z.ZodOptional<z.ZodString>;
|
|
54
55
|
jsonpath: z.ZodOptional<z.ZodString>;
|
|
55
56
|
pattern: z.ZodOptional<z.ZodString>;
|
|
57
|
+
replacement: z.ZodOptional<z.ZodString>;
|
|
56
58
|
}, z.core.$strip>>>;
|
|
57
59
|
follows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
58
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,
|
|
@@ -57,6 +65,7 @@ const packageSchema = z
|
|
|
57
65
|
"package-name": z.string().optional(),
|
|
58
66
|
"changelog-file": z.string().optional(),
|
|
59
67
|
"changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
|
|
68
|
+
"allow-stable-major": z.boolean().optional(),
|
|
60
69
|
"exclude-paths": z.array(z.string()).optional(),
|
|
61
70
|
"extra-files": z.array(artifactRuleSchema).optional(),
|
|
62
71
|
follows: z.array(z.string().min(1)).optional(),
|
|
@@ -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/plan.js
CHANGED
|
@@ -50,6 +50,8 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
50
50
|
const baselineSha = readBaselineSha(cwd) ?? loaded.config["bootstrap-sha"] ?? null;
|
|
51
51
|
const releaseTargetByPath = new Map(readReleaseTargets(cwd).map((target) => [target.path, target]));
|
|
52
52
|
const allowStableMajor = loaded.config["allow-stable-major"] ?? false;
|
|
53
|
+
const allowStableMajorForPath = (packagePath) => loaded.config.packages?.[packagePath]?.["allow-stable-major"] ??
|
|
54
|
+
allowStableMajor;
|
|
53
55
|
const monorepoMode = getMode(loaded.config["monorepo-mode"]);
|
|
54
56
|
const buildPackagePlan = (pkg) => {
|
|
55
57
|
const packageContext = resolvePackageStrategyContext(loaded.config, pkg.path, pkg.config);
|
|
@@ -78,7 +80,9 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
78
80
|
const commits = effectiveCommits;
|
|
79
81
|
const releaseType = analyzeParsedCommits(parsedCommits);
|
|
80
82
|
const nextVersion = releaseType
|
|
81
|
-
? bumpVersion(packageCurrentVersion, releaseType, {
|
|
83
|
+
? bumpVersion(packageCurrentVersion, releaseType, {
|
|
84
|
+
allowStableMajor: allowStableMajorForPath(pkg.path),
|
|
85
|
+
})
|
|
82
86
|
: null;
|
|
83
87
|
return {
|
|
84
88
|
path: pkg.path,
|
|
@@ -198,7 +202,9 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
198
202
|
return {
|
|
199
203
|
...pkgPlan,
|
|
200
204
|
releaseType: "patch",
|
|
201
|
-
nextVersion: bumpVersion(current, "patch", {
|
|
205
|
+
nextVersion: bumpVersion(current, "patch", {
|
|
206
|
+
allowStableMajor: allowStableMajorForPath(pkgPlan.path),
|
|
207
|
+
}),
|
|
202
208
|
bumpReason: "dependency-propagation",
|
|
203
209
|
dependencySourcePaths,
|
|
204
210
|
};
|
|
@@ -234,7 +240,9 @@ export function createReleasePlan(cwd = process.cwd()) {
|
|
|
234
240
|
pkgPlan.bumpReason === undefined;
|
|
235
241
|
const baseVersion = packageCurrentVersionByPath[pkgPlan.path] ?? pkgPlan.currentVersion;
|
|
236
242
|
const nextVersion = combinedReleaseType
|
|
237
|
-
? bumpVersion(baseVersion, combinedReleaseType, {
|
|
243
|
+
? bumpVersion(baseVersion, combinedReleaseType, {
|
|
244
|
+
allowStableMajor: allowStableMajorForPath(pkgPlan.path),
|
|
245
|
+
})
|
|
238
246
|
: null;
|
|
239
247
|
return {
|
|
240
248
|
...pkgPlan,
|
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/release/semver.d.ts
CHANGED
|
@@ -14,14 +14,16 @@ export declare function parseVersion(version: string): ParsedVersion;
|
|
|
14
14
|
export declare function isValidVersion(version: string): boolean;
|
|
15
15
|
/**
|
|
16
16
|
* Decide whether a changelog heading denotes a released version (e.g.
|
|
17
|
-
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000
|
|
18
|
-
* rather than a manual-notes heading (e.g. `## Unreleased`,
|
|
17
|
+
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`,
|
|
18
|
+
* `# pkg 8.0`) rather than a manual-notes heading (e.g. `## Unreleased`,
|
|
19
|
+
* `## Upcoming`).
|
|
19
20
|
*
|
|
20
21
|
* Intentionally liberal: a heading counts as a version if it contains any
|
|
21
|
-
* version-like token that {@link isValidVersion} accepts
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* version-like token that {@link isValidVersion} accepts, or ends with a bare
|
|
23
|
+
* `major.minor` token (the R `NEWS.md` convention). This errs toward "it's a
|
|
24
|
+
* version", so a real release heading is never mistaken for a notes block (and
|
|
25
|
+
* therefore never stripped). Dates such as `2026-06-02` use dashes, not dots,
|
|
26
|
+
* so they never match either token.
|
|
25
27
|
*/
|
|
26
28
|
export declare function isVersionHeading(heading: string): boolean;
|
|
27
29
|
export declare function compareVersions(leftRaw: string, rightRaw: string): number;
|
package/dist/release/semver.js
CHANGED
|
@@ -39,23 +39,30 @@ export function isValidVersion(version) {
|
|
|
39
39
|
return SEMVER_PATTERN.test(normalizeVersionInput(version));
|
|
40
40
|
}
|
|
41
41
|
const VERSION_TOKEN_PATTERN = /v?(\d+\.\d+\.\d+(?:\.\d+)?)/u;
|
|
42
|
+
// R `NEWS.md` headings conventionally abbreviate to `major.minor` (e.g.
|
|
43
|
+
// `# pkg 8.0`). Accept a bare two-component token only when it is the trailing
|
|
44
|
+
// token of the heading, so genuine R release headings register as versions
|
|
45
|
+
// while prose like `## Notes for 2.0 milestone` does not.
|
|
46
|
+
const TRAILING_MAJOR_MINOR_PATTERN = /\bv?\d+\.\d+\s*$/u;
|
|
42
47
|
/**
|
|
43
48
|
* Decide whether a changelog heading denotes a released version (e.g.
|
|
44
|
-
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000
|
|
45
|
-
* rather than a manual-notes heading (e.g. `## Unreleased`,
|
|
49
|
+
* `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`,
|
|
50
|
+
* `# pkg 8.0`) rather than a manual-notes heading (e.g. `## Unreleased`,
|
|
51
|
+
* `## Upcoming`).
|
|
46
52
|
*
|
|
47
53
|
* Intentionally liberal: a heading counts as a version if it contains any
|
|
48
|
-
* version-like token that {@link isValidVersion} accepts
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
54
|
+
* version-like token that {@link isValidVersion} accepts, or ends with a bare
|
|
55
|
+
* `major.minor` token (the R `NEWS.md` convention). This errs toward "it's a
|
|
56
|
+
* version", so a real release heading is never mistaken for a notes block (and
|
|
57
|
+
* therefore never stripped). Dates such as `2026-06-02` use dashes, not dots,
|
|
58
|
+
* so they never match either token.
|
|
52
59
|
*/
|
|
53
60
|
export function isVersionHeading(heading) {
|
|
54
61
|
const match = heading.match(VERSION_TOKEN_PATTERN);
|
|
55
|
-
if (
|
|
56
|
-
return
|
|
62
|
+
if (match?.[1] && isValidVersion(match[1])) {
|
|
63
|
+
return true;
|
|
57
64
|
}
|
|
58
|
-
return
|
|
65
|
+
return TRAILING_MAJOR_MINOR_PATTERN.test(heading);
|
|
59
66
|
}
|
|
60
67
|
function isNumericIdentifier(identifier) {
|
|
61
68
|
return /^(0|[1-9]\d*)$/u.test(identifier);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
function parseProjectToml(content, versionFile) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = parseToml(content);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
throw new Error(`Failed to parse ${versionFile}: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
if (parsed && typeof parsed === "object") {
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Failed to parse ${versionFile}: not a TOML table.`);
|
|
17
|
+
}
|
|
18
|
+
function readProjectVersion(content, versionFile) {
|
|
19
|
+
const parsed = parseProjectToml(content, versionFile);
|
|
20
|
+
const version = parsed.version;
|
|
21
|
+
if (typeof version === "string" && version.trim().length > 0) {
|
|
22
|
+
return version.trim();
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`${versionFile} is missing a valid root "version" field required by release-type "julia".`);
|
|
25
|
+
}
|
|
26
|
+
function writeProjectVersion(rawContent, versionFile, version) {
|
|
27
|
+
const lineEnding = rawContent.includes("\r\n") ? "\r\n" : "\n";
|
|
28
|
+
const hasFinalLineEnding = rawContent.endsWith("\n") || rawContent.endsWith("\r\n");
|
|
29
|
+
const lines = rawContent.split(/\r?\n/u);
|
|
30
|
+
let activeTable = null;
|
|
31
|
+
let replaced = false;
|
|
32
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
33
|
+
const line = lines[index] ?? "";
|
|
34
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/u);
|
|
35
|
+
if (sectionMatch) {
|
|
36
|
+
activeTable = sectionMatch[1]?.trim() ?? null;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// The Julia version is a root key: only match before the first table header.
|
|
40
|
+
if (activeTable !== null) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const versionMatch = line.match(/^(\s*version\s*=\s*)(["'])([^"']*)(\2)(\s*(?:#.*)?)?$/u);
|
|
44
|
+
if (!versionMatch) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const [, prefix = "", quote = '"', , , suffix = ""] = versionMatch;
|
|
48
|
+
lines[index] = `${prefix}${quote}${version}${quote}${suffix}`;
|
|
49
|
+
replaced = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (!replaced) {
|
|
53
|
+
throw new Error(`${versionFile} is missing a valid root "version" field required by release-type "julia".`);
|
|
54
|
+
}
|
|
55
|
+
let updated = lines.join(lineEnding);
|
|
56
|
+
if (hasFinalLineEnding && !updated.endsWith(lineEnding)) {
|
|
57
|
+
updated += lineEnding;
|
|
58
|
+
}
|
|
59
|
+
if (!hasFinalLineEnding && updated.endsWith(lineEnding)) {
|
|
60
|
+
updated = updated.slice(0, -lineEnding.length);
|
|
61
|
+
}
|
|
62
|
+
return updated;
|
|
63
|
+
}
|
|
64
|
+
export const juliaVersionStrategy = {
|
|
65
|
+
name: "julia",
|
|
66
|
+
getVersionFile(config) {
|
|
67
|
+
return config["version-file"] ?? "Project.toml";
|
|
68
|
+
},
|
|
69
|
+
validateProject(cwd, config) {
|
|
70
|
+
const versionFile = this.getVersionFile(config);
|
|
71
|
+
const versionPath = path.join(cwd, versionFile);
|
|
72
|
+
if (!fs.existsSync(versionPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
readProjectVersion(fs.readFileSync(versionPath, "utf8"), versionFile);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return error instanceof Error ? error.message : String(error);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
readVersion(cwd, config) {
|
|
84
|
+
const versionFile = this.getVersionFile(config);
|
|
85
|
+
const versionPath = path.join(cwd, versionFile);
|
|
86
|
+
if (!fs.existsSync(versionPath)) {
|
|
87
|
+
throw new Error(`Versionary requires ${versionFile} to exist.`);
|
|
88
|
+
}
|
|
89
|
+
return readProjectVersion(fs.readFileSync(versionPath, "utf8"), versionFile);
|
|
90
|
+
},
|
|
91
|
+
writeVersion(cwd, config, version) {
|
|
92
|
+
const versionFile = this.getVersionFile(config);
|
|
93
|
+
const versionPath = path.join(cwd, versionFile);
|
|
94
|
+
if (!fs.existsSync(versionPath)) {
|
|
95
|
+
throw new Error(`Versionary requires ${versionFile} to exist.`);
|
|
96
|
+
}
|
|
97
|
+
const existing = fs.readFileSync(versionPath, "utf8");
|
|
98
|
+
const updated = writeProjectVersion(existing, versionFile, version);
|
|
99
|
+
fs.writeFileSync(versionPath, updated, "utf8");
|
|
100
|
+
return [versionFile];
|
|
101
|
+
},
|
|
102
|
+
readPackageName(cwd, config) {
|
|
103
|
+
const versionFile = this.getVersionFile(config);
|
|
104
|
+
const versionPath = path.join(cwd, versionFile);
|
|
105
|
+
if (!fs.existsSync(versionPath)) {
|
|
106
|
+
throw new Error(`Versionary requires ${versionFile} to exist.`);
|
|
107
|
+
}
|
|
108
|
+
const parsed = parseProjectToml(fs.readFileSync(versionPath, "utf8"), versionFile);
|
|
109
|
+
const name = parsed.name;
|
|
110
|
+
if (typeof name === "string" && name.trim().length > 0) {
|
|
111
|
+
return name.trim();
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
},
|
|
115
|
+
};
|
package/dist/strategy/resolve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compositeVersionStrategy } from "./composite.js";
|
|
2
|
+
import { juliaVersionStrategy } from "./julia.js";
|
|
2
3
|
import { latexVersionStrategy } from "./latex.js";
|
|
3
4
|
import { nodeVersionStrategy } from "./node.js";
|
|
4
5
|
import { pythonVersionStrategy } from "./python.js";
|
|
@@ -6,6 +7,7 @@ import { rVersionStrategy } from "./r.js";
|
|
|
6
7
|
import { rustVersionStrategy } from "./rust.js";
|
|
7
8
|
import { simpleVersionStrategy } from "./simple.js";
|
|
8
9
|
const strategyRegistry = {
|
|
10
|
+
julia: juliaVersionStrategy,
|
|
9
11
|
latex: latexVersionStrategy,
|
|
10
12
|
simple: simpleVersionStrategy,
|
|
11
13
|
node: nodeVersionStrategy,
|
package/dist/types/config.d.ts
CHANGED
|
@@ -7,12 +7,14 @@ export interface VersionaryArtifactRule {
|
|
|
7
7
|
"field-path"?: string;
|
|
8
8
|
jsonpath?: string;
|
|
9
9
|
pattern?: string;
|
|
10
|
+
replacement?: string;
|
|
10
11
|
}
|
|
11
12
|
export interface VersionaryPackage {
|
|
12
13
|
"release-type"?: string | string[];
|
|
13
14
|
"package-name"?: string;
|
|
14
15
|
"changelog-file"?: string;
|
|
15
16
|
"changelog-format"?: VersionaryChangelogFormat;
|
|
17
|
+
"allow-stable-major"?: boolean;
|
|
16
18
|
"exclude-paths"?: string[];
|
|
17
19
|
"extra-files"?: VersionaryArtifactRule[];
|
|
18
20
|
follows?: string[];
|
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,27 +27,33 @@
|
|
|
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": {
|
|
43
45
|
"build": "tsc -p tsconfig.json",
|
|
44
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"gen:schema": "tsx scripts/generate-schema.ts && biome format --write schemas/config.json",
|
|
45
48
|
"test": "vitest run",
|
|
46
49
|
"verify": "tsx src/cli/index.ts verify",
|
|
47
50
|
"run": "tsx src/cli/index.ts run",
|
|
48
51
|
"plan": "tsx src/cli/index.ts plan",
|
|
49
52
|
"changelog": "tsx src/cli/index.ts changelog",
|
|
50
53
|
"pr": "tsx src/cli/index.ts pr",
|
|
51
|
-
"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"
|
|
52
58
|
}
|
|
53
59
|
}
|