versionary 0.3.0 → 0.5.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 +71 -28
- 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 +7 -2
- package/dist/app/release/pr.js +38 -20
- package/dist/app/release/recovery.d.ts +3 -0
- package/dist/app/release/recovery.js +1 -1
- package/dist/app/release/release.d.ts +18 -0
- package/dist/app/release/release.js +28 -7
- package/dist/app/release/state.d.ts +0 -1
- package/dist/app/release/state.js +36 -2
- package/dist/cli/index.js +60 -6
- package/dist/config/schema.js +33 -1
- package/dist/domain/release/changelog.js +1 -1
- package/dist/domain/release/plan.js +62 -16
- package/dist/domain/strategy/node.js +41 -11
- package/dist/domain/strategy/package-context.d.ts +7 -0
- package/dist/domain/strategy/package-context.js +49 -0
- package/dist/domain/strategy/r.d.ts +2 -0
- package/dist/domain/strategy/r.js +46 -0
- package/dist/domain/strategy/resolve.js +8 -0
- package/dist/domain/strategy/rust.d.ts +5 -0
- package/dist/domain/strategy/rust.js +477 -0
- package/dist/verify/verify-project.js +14 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Configuration is loaded from `versionary.jsonc` by default (or
|
|
|
72
72
|
|
|
73
73
|
Schema URL for editor support:
|
|
74
74
|
|
|
75
|
-
- `https://raw.githubusercontent.com/jolars/versionary/main/schemas/
|
|
75
|
+
- `https://raw.githubusercontent.com/jolars/versionary/main/schemas/config.json`
|
|
76
76
|
|
|
77
77
|
## Config (manifest style)
|
|
78
78
|
|
|
@@ -82,6 +82,10 @@ For a quick trial, use:
|
|
|
82
82
|
- `changelog-file` (default `CHANGELOG.md`) as release notes output
|
|
83
83
|
- `release-type: "node"` uses `package.json` as version source and updates it
|
|
84
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`)
|
|
85
89
|
- simple/default strategy keeps `version.txt` as source of truth and does not
|
|
86
90
|
update `package.json`
|
|
87
91
|
- stable release branch (`release-branch`, default: `versionary/release`) so
|
|
@@ -97,6 +101,41 @@ For a quick trial, use:
|
|
|
97
101
|
- `independent` computes package bumps per path
|
|
98
102
|
- `fixed` computes one shared bump across configured package paths
|
|
99
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
|
+
|
|
100
139
|
## Commit parsing and release analysis
|
|
101
140
|
|
|
102
141
|
Release planning is based on Conventional Commit parsing semantics:
|
|
@@ -105,20 +144,21 @@ Release planning is based on Conventional Commit parsing semantics:
|
|
|
105
144
|
- exposes structured parsed fields (`header`, `body`, `footer`, `type`, `scope`,
|
|
106
145
|
`description`, `notes`, `references`, `mentions`, `revert`)
|
|
107
146
|
- separates parser output from release policy mapping (`inferReleaseType*`)
|
|
108
|
-
- recognizes breaking changes from `!` and `BREAKING CHANGE` /
|
|
109
|
-
|
|
147
|
+
- recognizes breaking changes from `!` and `BREAKING CHANGE` / `BREAKING-CHANGE`
|
|
148
|
+
footers
|
|
110
149
|
- maps release impact as `feat => minor`, `fix|perf => patch`, breaking => major
|
|
111
150
|
- treats `revert:` as non-releasable commits
|
|
112
|
-
- suppresses commits that are reverted within the analyzed release window so
|
|
113
|
-
do not affect bump/changelog output
|
|
114
|
-
- emits parser diagnostics for malformed headers/footers/references and
|
|
115
|
-
revert messages
|
|
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
|
|
116
155
|
|
|
117
156
|
Commands:
|
|
118
157
|
|
|
119
158
|
- `pnpm verify`
|
|
120
159
|
- `pnpm run` (default orchestration: no-op, create/update release PR, or publish
|
|
121
160
|
release based on context)
|
|
161
|
+
- `pnpm run -- --json` (machine-readable orchestration result)
|
|
122
162
|
- `pnpm plan`
|
|
123
163
|
- `pnpm changelog -- --write`
|
|
124
164
|
- `pnpm pr`
|
|
@@ -133,8 +173,8 @@ Subsequent runs use the baseline state file.
|
|
|
133
173
|
|
|
134
174
|
## Release retry and recovery behavior
|
|
135
175
|
|
|
136
|
-
Release publish (`pnpm release` or the publish path in `pnpm run`) is
|
|
137
|
-
|
|
176
|
+
Release publish (`pnpm release` or the publish path in `pnpm run`) is idempotent
|
|
177
|
+
by target tag:
|
|
138
178
|
|
|
139
179
|
- if a tag already exists, Versionary reuses it rather than recreating it
|
|
140
180
|
- if release metadata already exists for the tag (e.g., GitHub Release), it is
|
|
@@ -188,18 +228,10 @@ steps:
|
|
|
188
228
|
with:
|
|
189
229
|
fetch-depth: 0
|
|
190
230
|
fetch-tags: true
|
|
191
|
-
-
|
|
192
|
-
|
|
231
|
+
- id: versionary
|
|
232
|
+
uses: jolars/versionary@v1
|
|
193
233
|
with:
|
|
194
|
-
|
|
195
|
-
cache: pnpm
|
|
196
|
-
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
197
|
-
- run: |
|
|
198
|
-
git config user.name "github-actions[bot]"
|
|
199
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
200
|
-
pnpm run run
|
|
201
|
-
env:
|
|
202
|
-
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
234
|
+
github-token: ${{ secrets.RELEASE_TOKEN }}
|
|
203
235
|
```
|
|
204
236
|
|
|
205
237
|
```yaml
|
|
@@ -212,17 +244,28 @@ steps:
|
|
|
212
244
|
with:
|
|
213
245
|
fetch-depth: 0
|
|
214
246
|
fetch-tags: true
|
|
215
|
-
-
|
|
216
|
-
|
|
247
|
+
- id: versionary
|
|
248
|
+
uses: jolars/versionary@v1
|
|
217
249
|
with:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
- run: pnpm run run
|
|
222
|
-
env:
|
|
223
|
-
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
250
|
+
github-token: ${{ secrets.RELEASE_TOKEN }}
|
|
251
|
+
- if: ${{ steps.versionary.outputs.release_created == 'true' }}
|
|
252
|
+
run: echo "Released ${{ steps.versionary.outputs.tag_name }}"
|
|
224
253
|
```
|
|
225
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
|
+
|
|
226
269
|
Package publication is intentionally out of scope in the current release flow.
|
|
227
270
|
Use separate CI workflows for publishing after Versionary has prepared/tagged
|
|
228
271
|
the release.
|
|
@@ -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
|
+
}
|
package/dist/app/release/pr.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type SimplePlan } from "../../domain/release/plan.js";
|
|
2
2
|
import type { ParsedCommit } from "../../infra/git/commits.js";
|
|
3
|
+
import type { VersionaryPluginContext } from "../../types/plugins.js";
|
|
3
4
|
export declare function splitSafeDirtyFiles(files: string[]): {
|
|
4
5
|
ignored: string[];
|
|
5
6
|
blocking: string[];
|
|
6
7
|
};
|
|
7
|
-
export declare function prepareSimpleReleasePr(cwd?: string
|
|
8
|
+
export declare function prepareSimpleReleasePr(cwd?: string, options?: {
|
|
9
|
+
logger?: VersionaryPluginContext["logger"];
|
|
10
|
+
}): {
|
|
8
11
|
branch: string;
|
|
9
12
|
title: string;
|
|
10
13
|
version: string;
|
|
@@ -13,6 +16,8 @@ export declare function prepareSimpleReleasePr(cwd?: string): {
|
|
|
13
16
|
plan: SimplePlan;
|
|
14
17
|
};
|
|
15
18
|
export declare function renderSimpleReviewRequestBody(version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null, cwd?: string): string;
|
|
16
|
-
export declare function openOrUpdateSimpleReviewRequest(cwd: string, branch: string, title: string, version: string, previousVersion: string, commits: ParsedCommit[], plan?: SimplePlan | null
|
|
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>;
|
|
17
22
|
export declare function pushReleaseBranch(cwd: string, branch: string): void;
|
|
18
23
|
export declare function isReleaseCommitMessage(subject: string): boolean;
|
package/dist/app/release/pr.js
CHANGED
|
@@ -14,9 +14,12 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const load_config_js_1 = require("../../config/load-config.js");
|
|
15
15
|
const changelog_js_1 = require("../../domain/release/changelog.js");
|
|
16
16
|
const plan_js_1 = require("../../domain/release/plan.js");
|
|
17
|
+
const package_context_js_1 = require("../../domain/strategy/package-context.js");
|
|
17
18
|
const resolve_js_1 = require("../../domain/strategy/resolve.js");
|
|
19
|
+
const rust_js_1 = require("../../domain/strategy/rust.js");
|
|
18
20
|
const capabilities_js_1 = require("../../plugins/capabilities.js");
|
|
19
21
|
const runtime_js_1 = require("../../plugins/runtime.js");
|
|
22
|
+
const artifact_rules_js_1 = require("./artifact-rules.js");
|
|
20
23
|
const state_js_1 = require("./state.js");
|
|
21
24
|
const SAFE_DIRTY_FILES = new Set([
|
|
22
25
|
"pnpm-lock.yaml",
|
|
@@ -55,25 +58,46 @@ function splitSafeDirtyFiles(files) {
|
|
|
55
58
|
}
|
|
56
59
|
return { ignored, blocking };
|
|
57
60
|
}
|
|
58
|
-
function ensureCleanWorktree(cwd) {
|
|
61
|
+
function ensureCleanWorktree(cwd, logger) {
|
|
59
62
|
const dirtyFiles = listTrackedDirtyFiles(cwd);
|
|
60
63
|
const { ignored, blocking } = splitSafeDirtyFiles(dirtyFiles);
|
|
61
64
|
if (blocking.length > 0) {
|
|
62
65
|
throw new Error(`Working tree has tracked modifications before versionary pr:\n${blocking.join("\n")}\nCommit/stash tracked changes first.`);
|
|
63
66
|
}
|
|
64
67
|
if (ignored.length > 0) {
|
|
65
|
-
|
|
68
|
+
logger?.warn(`Ignoring safe tracked changes before versionary pr:\n${ignored.join("\n")}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
|
-
function prepareSimpleReleasePr(cwd = process.cwd()) {
|
|
71
|
+
function prepareSimpleReleasePr(cwd = process.cwd(), options = {}) {
|
|
69
72
|
const plan = (0, plan_js_1.createSimplePlan)(cwd);
|
|
70
73
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
71
74
|
const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
|
|
72
75
|
if (!plan.nextVersion) {
|
|
73
76
|
throw new Error("No releasable commits found. Nothing to open a release PR for.");
|
|
74
77
|
}
|
|
75
|
-
ensureCleanWorktree(cwd);
|
|
76
|
-
const updatedVersionFiles =
|
|
78
|
+
ensureCleanWorktree(cwd, options.logger);
|
|
79
|
+
const updatedVersionFiles = [];
|
|
80
|
+
const rustManifestVersionTargets = {};
|
|
81
|
+
if (plan.packages && plan.packages.length > 0) {
|
|
82
|
+
for (const packagePlan of plan.packages) {
|
|
83
|
+
if (!packagePlan.nextVersion) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const packageConfig = loaded.config.packages?.[packagePlan.path] ?? {};
|
|
87
|
+
const packageContext = (0, package_context_js_1.resolvePackageStrategyContext)(loaded.config, packagePlan.path, packageConfig);
|
|
88
|
+
const packageUpdated = packageContext.strategy.writeVersion(cwd, packageContext.config, packagePlan.nextVersion);
|
|
89
|
+
updatedVersionFiles.push(...packageUpdated);
|
|
90
|
+
if (packageContext.strategy.name === rust_js_1.rustVersionStrategy.name) {
|
|
91
|
+
rustManifestVersionTargets[packageContext.versionFile] =
|
|
92
|
+
packagePlan.nextVersion;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
updatedVersionFiles.push(...(0, rust_js_1.applyRustWorkspaceDependencyUpdates)(cwd, rustManifestVersionTargets));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
updatedVersionFiles.push(...strategy.writeVersion(cwd, loaded.config, plan.nextVersion));
|
|
99
|
+
}
|
|
100
|
+
const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
|
|
77
101
|
const section = (0, changelog_js_1.renderSimpleChangelog)(plan);
|
|
78
102
|
(0, changelog_js_1.prependChangelog)(cwd, plan.changelogFile, section);
|
|
79
103
|
const branch = plan.releaseBranchPrefix;
|
|
@@ -82,7 +106,13 @@ function prepareSimpleReleasePr(cwd = process.cwd()) {
|
|
|
82
106
|
cwd,
|
|
83
107
|
stdio: ["ignore", "pipe", "ignore"],
|
|
84
108
|
});
|
|
85
|
-
const filesToAdd = [
|
|
109
|
+
const filesToAdd = [
|
|
110
|
+
...new Set([
|
|
111
|
+
...updatedVersionFiles,
|
|
112
|
+
...updatedArtifactFiles,
|
|
113
|
+
plan.changelogFile,
|
|
114
|
+
]),
|
|
115
|
+
];
|
|
86
116
|
(0, node_child_process_1.execFileSync)("git", ["add", ...filesToAdd], {
|
|
87
117
|
cwd,
|
|
88
118
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -100,24 +130,12 @@ function prepareSimpleReleasePr(cwd = process.cwd()) {
|
|
|
100
130
|
tag: pkg.path === "."
|
|
101
131
|
? `v${pkg.nextVersion ?? ""}`
|
|
102
132
|
: `${pkg.path.replaceAll("/", "-")}-v${pkg.nextVersion ?? ""}`,
|
|
103
|
-
notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
104
|
-
currentVersion: pkg.currentVersion,
|
|
105
|
-
nextVersion: pkg.nextVersion ?? "",
|
|
106
|
-
commits: pkg.commits,
|
|
107
|
-
cwd,
|
|
108
|
-
}, { includeFooter: false }),
|
|
109
133
|
}))
|
|
110
134
|
: [
|
|
111
135
|
{
|
|
112
136
|
path: ".",
|
|
113
137
|
version: plan.nextVersion,
|
|
114
138
|
tag: `v${plan.nextVersion}`,
|
|
115
|
-
notes: (0, changelog_js_1.renderSimpleReleaseNotes)({
|
|
116
|
-
currentVersion: plan.currentVersion,
|
|
117
|
-
nextVersion: plan.nextVersion,
|
|
118
|
-
commits: plan.commits,
|
|
119
|
-
cwd,
|
|
120
|
-
}, { includeFooter: false }),
|
|
121
139
|
},
|
|
122
140
|
];
|
|
123
141
|
(0, state_js_1.writeBaselineSha)(cwd, undefined, releaseTargets);
|
|
@@ -171,7 +189,7 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
|
|
|
171
189
|
cwd,
|
|
172
190
|
}, { includeFooter: true });
|
|
173
191
|
}
|
|
174
|
-
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null) {
|
|
192
|
+
async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, previousVersion, commits, plan = null, options = {}) {
|
|
175
193
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
176
194
|
const releaseFlow = loaded.config["review-mode"] ?? "direct";
|
|
177
195
|
if (releaseFlow !== "review") {
|
|
@@ -194,7 +212,7 @@ async function openOrUpdateSimpleReviewRequest(cwd, branch, title, version, prev
|
|
|
194
212
|
labels: ["release"],
|
|
195
213
|
}, {
|
|
196
214
|
cwd,
|
|
197
|
-
logger:
|
|
215
|
+
logger: options.logger,
|
|
198
216
|
});
|
|
199
217
|
return result.url;
|
|
200
218
|
}
|
|
@@ -7,6 +7,9 @@ export interface ReleaseTargetInput {
|
|
|
7
7
|
}
|
|
8
8
|
export interface ReleaseExecutionContext {
|
|
9
9
|
createReleaseMetadata: (input: ReleaseTargetInput) => Promise<VersionaryScmReleaseMetadataResult>;
|
|
10
|
+
logger?: {
|
|
11
|
+
info: (message: string) => void;
|
|
12
|
+
};
|
|
10
13
|
}
|
|
11
14
|
export interface ReleaseTargetOutcome {
|
|
12
15
|
tag: string;
|
|
@@ -87,7 +87,7 @@ async function executeIdempotentReleaseTarget(cwd, target, context) {
|
|
|
87
87
|
const metadata = await context.createReleaseMetadata(target);
|
|
88
88
|
const metadataStatus = metadata.status ?? "created";
|
|
89
89
|
if (tagStatus === "exists" && metadataStatus === "created") {
|
|
90
|
-
|
|
90
|
+
context.logger?.info(`Recovered drift for ${target.tag}: tag already existed and release metadata has now been created.`);
|
|
91
91
|
}
|
|
92
92
|
if (!metadata.url) {
|
|
93
93
|
throw new Error(`Release metadata for "${target.tag}" did not return a URL. Verify SCM API permissions and retry.`);
|
|
@@ -1 +1,19 @@
|
|
|
1
|
+
import type { VersionaryPluginContext } from "../../types/plugins.js";
|
|
1
2
|
export declare function runSimpleRelease(cwd?: string): Promise<string>;
|
|
3
|
+
export type SimpleRunReleaseResult = {
|
|
4
|
+
action: "release-skipped";
|
|
5
|
+
reason: string;
|
|
6
|
+
} | {
|
|
7
|
+
action: "release-published";
|
|
8
|
+
message: string;
|
|
9
|
+
releases: {
|
|
10
|
+
tag: string;
|
|
11
|
+
url: string;
|
|
12
|
+
tagStatus: "created" | "exists";
|
|
13
|
+
metadataStatus: "created" | "exists";
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export interface RunSimpleReleaseOptions {
|
|
17
|
+
logger?: VersionaryPluginContext["logger"];
|
|
18
|
+
}
|
|
19
|
+
export declare function runSimpleReleaseDetailed(cwd?: string, options?: RunSimpleReleaseOptions): Promise<SimpleRunReleaseResult>;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runSimpleRelease = runSimpleRelease;
|
|
7
|
+
exports.runSimpleReleaseDetailed = runSimpleReleaseDetailed;
|
|
7
8
|
const node_child_process_1 = require("node:child_process");
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -46,9 +47,19 @@ function readReleaseNotes(cwd, version, changelogFile) {
|
|
|
46
47
|
return notes.length > 0 ? notes : `Automated release for v${version}`;
|
|
47
48
|
}
|
|
48
49
|
async function runSimpleRelease(cwd = process.cwd()) {
|
|
50
|
+
const result = await runSimpleReleaseDetailed(cwd, { logger: console });
|
|
51
|
+
if (result.action === "release-skipped") {
|
|
52
|
+
return result.reason;
|
|
53
|
+
}
|
|
54
|
+
return result.message;
|
|
55
|
+
}
|
|
56
|
+
async function runSimpleReleaseDetailed(cwd = process.cwd(), options = {}) {
|
|
49
57
|
const subject = getHeadCommitSubject(cwd);
|
|
50
58
|
if (!(0, pr_js_1.isReleaseCommitMessage)(subject)) {
|
|
51
|
-
return
|
|
59
|
+
return {
|
|
60
|
+
action: "release-skipped",
|
|
61
|
+
reason: "No release commit context detected; skipping release stage.",
|
|
62
|
+
};
|
|
52
63
|
}
|
|
53
64
|
const loaded = (0, load_config_js_1.loadConfig)(cwd);
|
|
54
65
|
const strategy = (0, resolve_js_1.resolveVersionStrategy)(loaded.config);
|
|
@@ -72,19 +83,29 @@ async function runSimpleRelease(cwd = process.cwd()) {
|
|
|
72
83
|
path: ".",
|
|
73
84
|
version,
|
|
74
85
|
tag: defaultTag,
|
|
75
|
-
notes: readReleaseNotes(cwd, version, changelogFile),
|
|
76
86
|
},
|
|
77
87
|
];
|
|
78
|
-
const
|
|
88
|
+
const releases = [];
|
|
79
89
|
for (const target of targets) {
|
|
80
90
|
const outcome = await (0, recovery_js_1.executeIdempotentReleaseTarget)(cwd, {
|
|
81
91
|
tag: target.tag,
|
|
82
92
|
version: target.version,
|
|
83
|
-
notes:
|
|
93
|
+
notes: readReleaseNotes(cwd, target.version, changelogFile),
|
|
84
94
|
}, {
|
|
85
|
-
createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger:
|
|
95
|
+
createReleaseMetadata: (input) => plugin.createReleaseMetadata(input, { cwd, logger: options.logger }),
|
|
96
|
+
logger: options.logger,
|
|
97
|
+
});
|
|
98
|
+
releases.push({
|
|
99
|
+
tag: outcome.tag,
|
|
100
|
+
url: outcome.url,
|
|
101
|
+
tagStatus: outcome.tagStatus,
|
|
102
|
+
metadataStatus: outcome.metadataStatus,
|
|
86
103
|
});
|
|
87
|
-
published.push(`${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
|
|
88
104
|
}
|
|
89
|
-
|
|
105
|
+
const published = releases.map((outcome) => `${outcome.tag}: ${outcome.url} (tag=${outcome.tagStatus}, metadata=${outcome.metadataStatus})`);
|
|
106
|
+
return {
|
|
107
|
+
action: "release-published",
|
|
108
|
+
releases,
|
|
109
|
+
message: `Published releases ${published.join(", ")}`,
|
|
110
|
+
};
|
|
90
111
|
}
|