versionary 0.2.0 → 0.4.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 +231 -12
- package/dist/app/release/artifact-rules.d.ts +3 -0
- package/dist/app/release/artifact-rules.js +186 -0
- package/dist/app/release/pr.d.ts +23 -0
- package/dist/{simple → app/release}/pr.js +90 -79
- package/dist/app/release/recovery.d.ts +20 -0
- package/dist/app/release/recovery.js +101 -0
- package/dist/app/release/release.d.ts +19 -0
- package/dist/app/release/release.js +111 -0
- package/dist/app/release/state.d.ts +9 -0
- package/dist/app/release/state.js +93 -0
- package/dist/app/release/verify.d.ts +2 -0
- package/dist/app/release/verify.js +5 -0
- package/dist/cli/index.js +71 -15
- package/dist/config/load-config.js +2 -12
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +34 -1
- package/dist/domain/release/changelog.d.ts +12 -0
- package/dist/domain/release/changelog.js +123 -0
- package/dist/{simple → domain/release}/plan.d.ts +4 -3
- package/dist/{simple → domain/release}/plan.js +32 -15
- package/dist/domain/release/semver.d.ts +15 -0
- package/dist/domain/release/semver.js +111 -0
- package/dist/domain/strategy/node.d.ts +2 -0
- package/dist/domain/strategy/node.js +67 -0
- package/dist/domain/strategy/r.d.ts +2 -0
- package/dist/domain/strategy/r.js +46 -0
- package/dist/domain/strategy/resolve.d.ts +3 -0
- package/dist/domain/strategy/resolve.js +19 -0
- package/dist/domain/strategy/rust.d.ts +2 -0
- package/dist/domain/strategy/rust.js +369 -0
- package/dist/domain/strategy/simple.d.ts +2 -0
- package/dist/domain/strategy/simple.js +28 -0
- package/dist/domain/strategy/types.d.ts +7 -0
- package/dist/domain/strategy/types.js +2 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -1
- package/dist/infra/git/commits.d.ts +65 -0
- package/dist/infra/git/commits.js +436 -0
- package/dist/infra/scm/github/plugin.d.ts +1 -0
- package/dist/infra/scm/github/plugin.js +5 -0
- package/dist/infra/scm/runtime.d.ts +2 -0
- package/dist/infra/scm/runtime.js +24 -0
- package/dist/infra/scm/types.d.ts +1 -0
- package/dist/infra/scm/types.js +2 -0
- package/dist/plugins/runtime.d.ts +1 -2
- package/dist/plugins/runtime.js +3 -22
- package/dist/scm/github-plugin.js +133 -40
- package/dist/strategies/node.d.ts +1 -0
- package/dist/strategies/node.js +5 -0
- package/dist/strategies/resolve.d.ts +1 -0
- package/dist/strategies/resolve.js +5 -0
- package/dist/strategies/simple.d.ts +1 -0
- package/dist/strategies/simple.js +5 -0
- package/dist/strategies/types.d.ts +1 -0
- package/dist/strategies/types.js +2 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/plugins.d.ts +1 -0
- package/dist/verify/verify-project.js +3 -1
- package/package.json +3 -2
- package/dist/simple/changelog.d.ts +0 -3
- package/dist/simple/changelog.js +0 -46
- package/dist/simple/git.d.ts +0 -10
- package/dist/simple/git.js +0 -114
- package/dist/simple/pr.d.ts +0 -15
- package/dist/simple/release.d.ts +0 -1
- package/dist/simple/release.js +0 -77
- package/dist/simple/semver.d.ts +0 -8
- package/dist/simple/semver.js +0 -25
- package/dist/simple/state.d.ts +0 -3
- package/dist/simple/state.js +0 -47
- /package/dist/{simple → infra/git}/repo-url.d.ts +0 -0
- /package/dist/{simple → infra/git}/repo-url.js +0 -0
package/README.md
CHANGED
|
@@ -1,13 +1,78 @@
|
|
|
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
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
Versionary is designed as a practical middle ground between `semantic-release`
|
|
9
|
+
and `release-please`.
|
|
10
|
+
|
|
11
|
+
- Like `semantic-release`, it supports direct release execution.
|
|
12
|
+
- Like `release-please`, it supports a release PR workflow so maintainers can
|
|
13
|
+
preview and review changes before publication.
|
|
14
|
+
|
|
15
|
+
The core idea is to keep versioning, changelog generation, tagging, and SCM
|
|
16
|
+
release metadata in one tool, while leaving package publication (npm, crates.io,
|
|
17
|
+
etc.) to dedicated CI workflows triggered by tags or releases.
|
|
18
|
+
|
|
19
|
+
## Product direction
|
|
20
|
+
|
|
21
|
+
Versionary is being built to:
|
|
22
|
+
|
|
23
|
+
- support both direct releases and release-PR-gated releases
|
|
24
|
+
- work across repository types (Node, Rust, docs/LaTeX, etc.)
|
|
25
|
+
- stay SCM-agnostic at the core, with integrations via plugin capabilities
|
|
26
|
+
(GitHub first; GitLab/Codeberg later)
|
|
27
|
+
- keep a small, stable core with explicit extension points
|
|
28
|
+
- handle trunk-based development and monorepo workflows cleanly
|
|
29
|
+
|
|
30
|
+
## Scope and non-goals
|
|
31
|
+
|
|
32
|
+
In scope:
|
|
33
|
+
|
|
34
|
+
- semantic version planning from commits
|
|
35
|
+
- changelog generation
|
|
36
|
+
- release PR automation
|
|
37
|
+
- tags + SCM release metadata (e.g. GitHub Releases)
|
|
38
|
+
|
|
39
|
+
Out of scope (intentional):
|
|
40
|
+
|
|
41
|
+
- publishing artifacts to language registries
|
|
42
|
+
- replacing package-specific publish tooling
|
|
43
|
+
|
|
44
|
+
Use your CI/CD platform for registry publishing, triggered from a created
|
|
45
|
+
release/tag.
|
|
46
|
+
|
|
47
|
+
## Current status vs roadmap
|
|
48
|
+
|
|
49
|
+
Current implementation focuses on:
|
|
50
|
+
|
|
51
|
+
- strategy-based version updates (`simple`, `node`)
|
|
52
|
+
- release planning and changelog generation
|
|
53
|
+
- review-mode vs direct-mode release flow
|
|
54
|
+
- built-in GitHub SCM plugin capabilities
|
|
55
|
+
|
|
56
|
+
Planned/harder areas include deeper monorepo ergonomics, broader SCM coverage,
|
|
57
|
+
and stronger failure recovery around release steps.
|
|
58
|
+
|
|
59
|
+
## Architecture layout (current migration)
|
|
60
|
+
|
|
61
|
+
The repository is moving to explicit layered modules:
|
|
62
|
+
|
|
63
|
+
- `src/app/`: command-level application services and orchestration boundaries
|
|
64
|
+
- `src/domain/`: release and strategy domain logic/contracts
|
|
65
|
+
- `src/infra/`: platform integrations (SCM, git/runtime adapters)
|
|
66
|
+
|
|
67
|
+
Legacy `src/simple/` has been removed. Remaining compatibility paths are
|
|
68
|
+
`src/strategies` and `src/scm` while migration is finalized.
|
|
69
|
+
|
|
70
|
+
Configuration is loaded from `versionary.jsonc` by default (or
|
|
71
|
+
`versionary.json`).
|
|
7
72
|
|
|
8
73
|
Schema URL for editor support:
|
|
9
74
|
|
|
10
|
-
- `https://raw.githubusercontent.com/jolars/versionary/main/schemas/
|
|
75
|
+
- `https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json`
|
|
11
76
|
|
|
12
77
|
## Config (manifest style)
|
|
13
78
|
|
|
@@ -15,21 +80,85 @@ For a quick trial, use:
|
|
|
15
80
|
|
|
16
81
|
- `version-file` (default `version.txt`) as version source
|
|
17
82
|
- `changelog-file` (default `CHANGELOG.md`) as release notes output
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
- `
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
83
|
+
- `release-type: "node"` uses `package.json` as version source and updates it
|
|
84
|
+
during release PR prep
|
|
85
|
+
- `release-type: "r"` uses `DESCRIPTION` as version source and updates the
|
|
86
|
+
`Version:` field
|
|
87
|
+
- `release-type: "rust"` uses Cargo manifests (`Cargo.toml`) as version source;
|
|
88
|
+
`version-file` must point to a `Cargo.toml` (default: `Cargo.toml`)
|
|
89
|
+
- simple/default strategy keeps `version.txt` as source of truth and does not
|
|
90
|
+
update `package.json`
|
|
91
|
+
- stable release branch (`release-branch`, default: `versionary/release`) so
|
|
92
|
+
release PRs are updated in-place
|
|
93
|
+
- `baseline-file` (default `.versionary-manifest.json`) tracks baseline SHA for
|
|
94
|
+
deterministic commit ranges independent of tags
|
|
95
|
+
- pre-1.0 policy defaults to conservative major handling: for `0.y.z`, breaking
|
|
96
|
+
changes bump to `0.(y+1).0`; set `allow-stable-major: true` to allow explicit
|
|
97
|
+
auto-transition to `1.0.0` on a breaking release
|
|
98
|
+
- review mode (`review-mode`): `review` (PR/MR style) or `direct` (no review
|
|
99
|
+
request)
|
|
24
100
|
- optional monorepo planning with `monorepo-mode` and `packages`:
|
|
25
101
|
- `independent` computes package bumps per path
|
|
26
102
|
- `fixed` computes one shared bump across configured package paths
|
|
27
103
|
|
|
104
|
+
Rust strategy examples:
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
// Single crate
|
|
108
|
+
{
|
|
109
|
+
"release-type": "rust",
|
|
110
|
+
"version-file": "Cargo.toml"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
// Workspace root (virtual or root crate + members)
|
|
116
|
+
{
|
|
117
|
+
"release-type": "rust",
|
|
118
|
+
"version-file": "Cargo.toml"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Current rust auto-update behavior (phase scope):
|
|
123
|
+
|
|
124
|
+
- updates crate versions in each targeted crate `[package].version`
|
|
125
|
+
- updates internal workspace dependency versions when the dependency name
|
|
126
|
+
matches another targeted crate name
|
|
127
|
+
- applies dependency version rewrites in:
|
|
128
|
+
- `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`
|
|
129
|
+
- `[target.*.dependencies]`, `[target.*.dev-dependencies]`,
|
|
130
|
+
`[target.*.build-dependencies]`
|
|
131
|
+
|
|
132
|
+
Current rust non-goals/limits:
|
|
133
|
+
|
|
134
|
+
- does not update external dependency versions
|
|
135
|
+
- does not update `workspace.dependencies`
|
|
136
|
+
- does not add missing `version = ...` fields to dependency inline tables
|
|
137
|
+
- does not perform Cargo publish/release to crates.io
|
|
138
|
+
|
|
139
|
+
## Commit parsing and release analysis
|
|
140
|
+
|
|
141
|
+
Release planning is based on Conventional Commit parsing semantics:
|
|
142
|
+
|
|
143
|
+
- parses type/scope/description from commit headers
|
|
144
|
+
- exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
|
|
145
|
+
`description`, `notes`, `references`, `mentions`, `revert`)
|
|
146
|
+
- separates parser output from release policy mapping (`inferReleaseType*`)
|
|
147
|
+
- recognizes breaking changes from `!` and `BREAKING CHANGE` / `BREAKING-CHANGE`
|
|
148
|
+
footers
|
|
149
|
+
- maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
|
|
150
|
+
- treats `revert:` as non-releasable commits
|
|
151
|
+
- suppresses commits that are reverted within the analyzed release window so
|
|
152
|
+
they do not affect bump/changelog output
|
|
153
|
+
- emits parser diagnostics for malformed headers/footers/references and
|
|
154
|
+
ambiguous revert messages
|
|
155
|
+
|
|
28
156
|
Commands:
|
|
29
157
|
|
|
30
158
|
- `pnpm verify`
|
|
31
159
|
- `pnpm run` (default orchestration: no-op, create/update release PR, or publish
|
|
32
160
|
release based on context)
|
|
161
|
+
- `pnpm run -- --json` (machine-readable orchestration result)
|
|
33
162
|
- `pnpm plan`
|
|
34
163
|
- `pnpm changelog -- --write`
|
|
35
164
|
- `pnpm pr`
|
|
@@ -39,8 +168,23 @@ Commands:
|
|
|
39
168
|
via SCM plugin capability. `pnpm run` is the recommended CI entrypoint and
|
|
40
169
|
auto-dispatches between PR/update and release publish.
|
|
41
170
|
|
|
42
|
-
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
43
|
-
the baseline state file.
|
|
171
|
+
For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
|
|
172
|
+
Subsequent runs use the baseline state file.
|
|
173
|
+
|
|
174
|
+
## Release retry and recovery behavior
|
|
175
|
+
|
|
176
|
+
Release publish (`pnpm release` or the publish path in `pnpm run`) is idempotent
|
|
177
|
+
by target tag:
|
|
178
|
+
|
|
179
|
+
- if a tag already exists, Versionary reuses it rather than recreating it
|
|
180
|
+
- if release metadata already exists for the tag (e.g., GitHub Release), it is
|
|
181
|
+
reused
|
|
182
|
+
- if a prior run created/pushed the tag but failed before metadata creation, a
|
|
183
|
+
rerun creates the missing metadata and proceeds
|
|
184
|
+
|
|
185
|
+
Versionary fails fast when recovery is unsafe (for example, local and remote
|
|
186
|
+
tags with the same name point to different SHAs). In these cases, the error
|
|
187
|
+
message includes remediation guidance so CI logs are actionable.
|
|
44
188
|
|
|
45
189
|
## Built-in plugins
|
|
46
190
|
|
|
@@ -48,8 +192,83 @@ Versionary ships with built-in SCM plugin support:
|
|
|
48
192
|
|
|
49
193
|
- `github` (default): review request + release metadata
|
|
50
194
|
|
|
195
|
+
### GitHub integration: env, permissions, and flow
|
|
196
|
+
|
|
197
|
+
Required environment for the built-in GitHub plugin:
|
|
198
|
+
|
|
199
|
+
- `GITHUB_REPOSITORY` (format: `owner/repo`)
|
|
200
|
+
- one token env var: `VERSIONARY_PR_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN`
|
|
201
|
+
|
|
202
|
+
Token precedence is:
|
|
203
|
+
|
|
204
|
+
- `VERSIONARY_PR_TOKEN` > `GH_TOKEN` > `GITHUB_TOKEN`
|
|
205
|
+
|
|
206
|
+
Minimum GitHub token/repo permissions for Versionary-managed metadata:
|
|
207
|
+
|
|
208
|
+
- release PR create/update flow: `contents: write`, `pull-requests: write`
|
|
209
|
+
- release metadata flow (GitHub Release create/read): `contents: write`
|
|
210
|
+
|
|
211
|
+
`review-mode` behavior:
|
|
212
|
+
|
|
213
|
+
- `review`: `pnpm run run` prepares/updates the release branch and creates or
|
|
214
|
+
updates a release PR
|
|
215
|
+
- `direct`: `pnpm run run` prepares/updates the release branch and skips review
|
|
216
|
+
request creation
|
|
217
|
+
|
|
218
|
+
Concise GitHub Actions examples:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
# 1) Release PR / update flow (run on push to default branch)
|
|
222
|
+
permissions:
|
|
223
|
+
contents: write
|
|
224
|
+
pull-requests: write
|
|
225
|
+
|
|
226
|
+
steps:
|
|
227
|
+
- uses: actions/checkout@v6
|
|
228
|
+
with:
|
|
229
|
+
fetch-depth: 0
|
|
230
|
+
fetch-tags: true
|
|
231
|
+
- id: versionary
|
|
232
|
+
uses: jolars/versionary@v1
|
|
233
|
+
with:
|
|
234
|
+
github-token: ${{ secrets.RELEASE_TOKEN }}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
# 2) Release publish flow after merge (release commit context)
|
|
239
|
+
permissions:
|
|
240
|
+
contents: write
|
|
241
|
+
|
|
242
|
+
steps:
|
|
243
|
+
- uses: actions/checkout@v6
|
|
244
|
+
with:
|
|
245
|
+
fetch-depth: 0
|
|
246
|
+
fetch-tags: true
|
|
247
|
+
- id: versionary
|
|
248
|
+
uses: jolars/versionary@v1
|
|
249
|
+
with:
|
|
250
|
+
github-token: ${{ secrets.RELEASE_TOKEN }}
|
|
251
|
+
- if: ${{ steps.versionary.outputs.release_created == 'true' }}
|
|
252
|
+
run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Action outputs:
|
|
256
|
+
|
|
257
|
+
- `action`: `noop`, `pr-prepared`, `release-published`, `release-skipped`
|
|
258
|
+
- `message`: human-readable summary
|
|
259
|
+
- `release_created`: `"true"` when at least one release was published
|
|
260
|
+
- `tag_name`: first published tag (for single-target flows)
|
|
261
|
+
- `tag_names`: JSON array of published tags
|
|
262
|
+
- `review_url`: review request URL when PR flow runs
|
|
263
|
+
|
|
264
|
+
For GitHub Action consumers, publish immutable tags (for example `v1.2.3`) and
|
|
265
|
+
maintain a moving major tag (`v1`, `v2`, ...). A small release-triggered
|
|
266
|
+
workflow should update `v<major>` to the latest release tag so `uses:
|
|
267
|
+
jolars/versionary@v1` stays current without breaking major compatibility.
|
|
268
|
+
|
|
51
269
|
Package publication is intentionally out of scope in the current release flow.
|
|
52
|
-
Use separate CI workflows for publishing after Versionary has prepared/tagged
|
|
270
|
+
Use separate CI workflows for publishing after Versionary has prepared/tagged
|
|
271
|
+
the release.
|
|
53
272
|
|
|
54
273
|
## Install from GitHub
|
|
55
274
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.applyConfiguredArtifactRules = applyConfiguredArtifactRules;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const toml_1 = __importDefault(require("@iarna/toml"));
|
|
10
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function parseJsonPath(jsonpath) {
|
|
15
|
+
if (!jsonpath.startsWith("$")) {
|
|
16
|
+
throw new Error(`Invalid jsonpath "${jsonpath}". Must start with "$".`);
|
|
17
|
+
}
|
|
18
|
+
const tokens = [];
|
|
19
|
+
let index = 1;
|
|
20
|
+
while (index < jsonpath.length) {
|
|
21
|
+
const current = jsonpath[index];
|
|
22
|
+
if (current === ".") {
|
|
23
|
+
const keyMatch = jsonpath.slice(index + 1).match(/^[A-Za-z0-9_-]+/u);
|
|
24
|
+
if (!keyMatch) {
|
|
25
|
+
throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
|
|
26
|
+
}
|
|
27
|
+
tokens.push(keyMatch[0]);
|
|
28
|
+
index += 1 + keyMatch[0].length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (current === "[") {
|
|
32
|
+
const rest = jsonpath.slice(index + 1);
|
|
33
|
+
const numberMatch = rest.match(/^(\d+)\]/u);
|
|
34
|
+
if (numberMatch) {
|
|
35
|
+
tokens.push(Number(numberMatch[1]));
|
|
36
|
+
index += 2 + numberMatch[1].length;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const keyMatch = rest.match(/^"([^"]+)"\]/u);
|
|
40
|
+
if (keyMatch) {
|
|
41
|
+
tokens.push(keyMatch[1]);
|
|
42
|
+
index += 4 + keyMatch[1].length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Invalid jsonpath "${jsonpath}" near index ${index}.`);
|
|
48
|
+
}
|
|
49
|
+
if (tokens.length === 0) {
|
|
50
|
+
throw new Error(`Invalid jsonpath "${jsonpath}". Path must target a field.`);
|
|
51
|
+
}
|
|
52
|
+
return tokens;
|
|
53
|
+
}
|
|
54
|
+
function setVersionAtJsonPath(document, jsonpath, version) {
|
|
55
|
+
const tokens = parseJsonPath(jsonpath);
|
|
56
|
+
let cursor = document;
|
|
57
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
58
|
+
const token = tokens[index];
|
|
59
|
+
if (typeof token === "number") {
|
|
60
|
+
if (!Array.isArray(cursor) || token >= cursor.length) {
|
|
61
|
+
throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
|
|
62
|
+
}
|
|
63
|
+
cursor = cursor[token];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(cursor) || !(token in cursor)) {
|
|
67
|
+
throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
|
|
68
|
+
}
|
|
69
|
+
cursor = cursor[token];
|
|
70
|
+
}
|
|
71
|
+
const leaf = tokens.at(-1);
|
|
72
|
+
if (typeof leaf === "number") {
|
|
73
|
+
if (!Array.isArray(cursor) || leaf >= cursor.length) {
|
|
74
|
+
throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
|
|
75
|
+
}
|
|
76
|
+
const current = cursor[leaf];
|
|
77
|
+
if (typeof current !== "string" && typeof current !== "number") {
|
|
78
|
+
throw new Error(`jsonpath "${jsonpath}" must point to a string or number field for version updates.`);
|
|
79
|
+
}
|
|
80
|
+
cursor[leaf] = version;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord(cursor) || !(leaf in cursor)) {
|
|
84
|
+
throw new Error(`jsonpath "${jsonpath}" does not resolve to an existing field.`);
|
|
85
|
+
}
|
|
86
|
+
const current = cursor[leaf];
|
|
87
|
+
if (typeof current !== "string" && typeof current !== "number") {
|
|
88
|
+
throw new Error(`jsonpath "${jsonpath}" must point to a string or number field for version updates.`);
|
|
89
|
+
}
|
|
90
|
+
cursor[leaf] = version;
|
|
91
|
+
}
|
|
92
|
+
function parseRegexPattern(pattern) {
|
|
93
|
+
const slashPattern = pattern.match(/^\/((?:\\\/|[^/])+)\/([a-z]*)$/u);
|
|
94
|
+
if (slashPattern) {
|
|
95
|
+
return new RegExp(slashPattern[1], slashPattern[2]);
|
|
96
|
+
}
|
|
97
|
+
return new RegExp(pattern, "m");
|
|
98
|
+
}
|
|
99
|
+
function applyRegexRule(content, pattern, version) {
|
|
100
|
+
const regex = parseRegexPattern(pattern);
|
|
101
|
+
const matchFlags = regex.flags.includes("g")
|
|
102
|
+
? regex.flags
|
|
103
|
+
: `${regex.flags}g`;
|
|
104
|
+
const globalRegex = new RegExp(regex.source, matchFlags);
|
|
105
|
+
const matches = [...content.matchAll(globalRegex)];
|
|
106
|
+
if (matches.length !== 1) {
|
|
107
|
+
throw new Error(`Regex pattern must match exactly one occurrence; matched ${matches.length}.`);
|
|
108
|
+
}
|
|
109
|
+
const match = matches[0];
|
|
110
|
+
const start = match.index;
|
|
111
|
+
if (start === undefined) {
|
|
112
|
+
throw new Error("Regex match did not include an index.");
|
|
113
|
+
}
|
|
114
|
+
const full = match[0];
|
|
115
|
+
const groupOne = match[1];
|
|
116
|
+
const replacement = typeof groupOne === "string" ? full.replace(groupOne, version) : version;
|
|
117
|
+
return `${content.slice(0, start)}${replacement}${content.slice(start + full.length)}`;
|
|
118
|
+
}
|
|
119
|
+
function applyArtifactRuleToContent(content, rule, version) {
|
|
120
|
+
if (rule.type === "regex") {
|
|
121
|
+
return applyRegexRule(content, rule.pattern, version);
|
|
122
|
+
}
|
|
123
|
+
if (rule.type === "json") {
|
|
124
|
+
const parsed = JSON.parse(content);
|
|
125
|
+
setVersionAtJsonPath(parsed, rule.jsonpath, version);
|
|
126
|
+
return `${JSON.stringify(parsed, null, 2)}\n`;
|
|
127
|
+
}
|
|
128
|
+
if (rule.type === "toml") {
|
|
129
|
+
const parsed = toml_1.default.parse(content);
|
|
130
|
+
setVersionAtJsonPath(parsed, rule.jsonpath, version);
|
|
131
|
+
return `${toml_1.default.stringify(parsed)}\n`;
|
|
132
|
+
}
|
|
133
|
+
const parsed = yaml_1.default.parse(content);
|
|
134
|
+
setVersionAtJsonPath(parsed, rule.jsonpath, version);
|
|
135
|
+
return `${yaml_1.default.stringify(parsed)}`;
|
|
136
|
+
}
|
|
137
|
+
function normalizeRelative(base, target) {
|
|
138
|
+
return node_path_1.default.relative(base, target).replaceAll("\\", "/");
|
|
139
|
+
}
|
|
140
|
+
function applyArtifactRulesForPackage(cwd, packagePath, packageConfig, version) {
|
|
141
|
+
const rules = packageConfig["extra-files"] ?? [];
|
|
142
|
+
if (rules.length === 0) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const packageBase = node_path_1.default.join(cwd, packagePath);
|
|
146
|
+
const updated = [];
|
|
147
|
+
for (const rule of rules) {
|
|
148
|
+
const targetPath = node_path_1.default.join(packageBase, rule.path);
|
|
149
|
+
if (!node_fs_1.default.existsSync(targetPath)) {
|
|
150
|
+
throw new Error(`Artifact rule target missing for package "${packagePath}": ${rule.path}`);
|
|
151
|
+
}
|
|
152
|
+
const existing = node_fs_1.default.readFileSync(targetPath, "utf8");
|
|
153
|
+
let next;
|
|
154
|
+
try {
|
|
155
|
+
next = applyArtifactRuleToContent(existing, rule, version);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
throw new Error(`Failed applying artifact rule (${rule.type}) for package "${packagePath}" file "${rule.path}": ${message}`);
|
|
160
|
+
}
|
|
161
|
+
node_fs_1.default.writeFileSync(targetPath, next, "utf8");
|
|
162
|
+
updated.push(normalizeRelative(cwd, targetPath));
|
|
163
|
+
}
|
|
164
|
+
return updated;
|
|
165
|
+
}
|
|
166
|
+
function applyConfiguredArtifactRules(cwd, config, plan) {
|
|
167
|
+
const packageConfigs = config.packages ?? {};
|
|
168
|
+
if (!plan.packages || plan.packages.length === 0) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const updated = new Set();
|
|
172
|
+
for (const packagePlan of plan.packages) {
|
|
173
|
+
if (!packagePlan.nextVersion) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const packageConfig = packageConfigs[packagePlan.path];
|
|
177
|
+
if (!packageConfig?.["extra-files"]?.length) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const files = applyArtifactRulesForPackage(cwd, packagePlan.path, packageConfig, packagePlan.nextVersion);
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
updated.add(file);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return [...updated].sort((a, b) => a.localeCompare(b));
|
|
186
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SimplePlan } from "../../domain/release/plan.js";
|
|
2
|
+
import type { ParsedCommit } from "../../infra/git/commits.js";
|
|
3
|
+
import type { VersionaryPluginContext } from "../../types/plugins.js";
|
|
4
|
+
export declare function splitSafeDirtyFiles(files: string[]): {
|
|
5
|
+
ignored: string[];
|
|
6
|
+
blocking: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function prepareSimpleReleasePr(cwd?: string, options?: {
|
|
9
|
+
logger?: VersionaryPluginContext["logger"];
|
|
10
|
+
}): {
|
|
11
|
+
branch: string;
|
|
12
|
+
title: string;
|
|
13
|
+
version: string;
|
|
14
|
+
previousVersion: string;
|
|
15
|
+
commits: ParsedCommit[];
|
|
16
|
+
plan: SimplePlan;
|
|
17
|
+
};
|
|
18
|
+
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, cwd?: string): string;
|
|
19
|
+
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, options?: {
|
|
20
|
+
logger?: VersionaryPluginContext["logger"];
|
|
21
|
+
}): Promise<string>;
|
|
22
|
+
export declare function pushReleaseBranch(cwd: string, branch: string): void;
|
|
23
|
+
export declare function isReleaseCommitMessage(subject: string): boolean;
|