versionary 1.0.1 → 1.2.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.
@@ -0,0 +1,283 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const CMAKE_VERSION_PATTERN = /^(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*)){2,3}$/u;
4
+ function bracketDelimiterAt(content, index) {
5
+ const match = content.slice(index).match(/^\[(=*)\[/u);
6
+ if (!match) {
7
+ return null;
8
+ }
9
+ const equals = match[1] ?? "";
10
+ return { openLength: equals.length + 2, close: `]${equals}]` };
11
+ }
12
+ function skipBracket(content, index) {
13
+ const delimiter = bracketDelimiterAt(content, index);
14
+ if (!delimiter) {
15
+ return null;
16
+ }
17
+ const closeIndex = content.indexOf(delimiter.close, index + delimiter.openLength);
18
+ return closeIndex === -1
19
+ ? content.length
20
+ : closeIndex + delimiter.close.length;
21
+ }
22
+ function skipComment(content, index) {
23
+ const bracketEnd = skipBracket(content, index + 1);
24
+ if (bracketEnd !== null) {
25
+ return bracketEnd;
26
+ }
27
+ const newline = content.indexOf("\n", index + 1);
28
+ return newline === -1 ? content.length : newline;
29
+ }
30
+ function skipQuoted(content, index) {
31
+ let cursor = index + 1;
32
+ while (cursor < content.length) {
33
+ if (content[cursor] === "\\") {
34
+ cursor += 2;
35
+ continue;
36
+ }
37
+ if (content[cursor] === '"') {
38
+ return cursor + 1;
39
+ }
40
+ cursor += 1;
41
+ }
42
+ return content.length;
43
+ }
44
+ function skipTrivia(content, index) {
45
+ let cursor = index;
46
+ while (cursor < content.length) {
47
+ if (/\s/u.test(content[cursor] ?? "")) {
48
+ cursor += 1;
49
+ continue;
50
+ }
51
+ if (content[cursor] === "#") {
52
+ cursor = skipComment(content, cursor);
53
+ continue;
54
+ }
55
+ break;
56
+ }
57
+ return cursor;
58
+ }
59
+ function readQuotedArgument(content, start) {
60
+ const end = skipQuoted(content, start);
61
+ const closed = end <= content.length && content[end - 1] === '"';
62
+ const valueEnd = closed ? end - 1 : end;
63
+ const value = content.slice(start + 1, valueEnd);
64
+ return {
65
+ value,
66
+ start,
67
+ end,
68
+ valueStart: start + 1,
69
+ valueEnd,
70
+ literal: closed && !value.includes("$"),
71
+ };
72
+ }
73
+ function readBracketArgument(content, start) {
74
+ const delimiter = bracketDelimiterAt(content, start);
75
+ if (!delimiter) {
76
+ return null;
77
+ }
78
+ const end = skipBracket(content, start) ?? content.length;
79
+ const closed = end < content.length || content.endsWith(delimiter.close);
80
+ const valueEnd = closed ? end - delimiter.close.length : end;
81
+ return {
82
+ value: content.slice(start + delimiter.openLength, valueEnd),
83
+ start,
84
+ end,
85
+ valueStart: start + delimiter.openLength,
86
+ valueEnd,
87
+ literal: closed,
88
+ };
89
+ }
90
+ function readCommand(content, name, openParen) {
91
+ const args = [];
92
+ let cursor = openParen + 1;
93
+ let depth = 1;
94
+ while (cursor < content.length) {
95
+ cursor = skipTrivia(content, cursor);
96
+ if (cursor >= content.length) {
97
+ break;
98
+ }
99
+ const current = content[cursor];
100
+ if (current === ")") {
101
+ depth -= 1;
102
+ cursor += 1;
103
+ if (depth === 0) {
104
+ break;
105
+ }
106
+ continue;
107
+ }
108
+ if (current === "(") {
109
+ depth += 1;
110
+ cursor += 1;
111
+ continue;
112
+ }
113
+ if (current === '"') {
114
+ const argument = readQuotedArgument(content, cursor);
115
+ if (depth === 1) {
116
+ args.push(argument);
117
+ }
118
+ cursor = argument.end;
119
+ continue;
120
+ }
121
+ if (current === "[") {
122
+ const argument = readBracketArgument(content, cursor);
123
+ if (argument) {
124
+ if (depth === 1) {
125
+ args.push(argument);
126
+ }
127
+ cursor = argument.end;
128
+ continue;
129
+ }
130
+ }
131
+ const start = cursor;
132
+ while (cursor < content.length) {
133
+ const character = content[cursor];
134
+ if (/\s/u.test(character ?? "") ||
135
+ character === "#" ||
136
+ character === "(" ||
137
+ character === ")") {
138
+ break;
139
+ }
140
+ cursor += 1;
141
+ }
142
+ if (cursor === start) {
143
+ cursor += 1;
144
+ continue;
145
+ }
146
+ if (depth === 1) {
147
+ const value = content.slice(start, cursor);
148
+ args.push({
149
+ value,
150
+ start,
151
+ end: cursor,
152
+ valueStart: start,
153
+ valueEnd: cursor,
154
+ literal: !value.includes("$"),
155
+ });
156
+ }
157
+ }
158
+ return { name, arguments: args, end: cursor };
159
+ }
160
+ function findCommands(content) {
161
+ const commands = [];
162
+ let cursor = 0;
163
+ while (cursor < content.length) {
164
+ const current = content[cursor];
165
+ if (current === "#") {
166
+ cursor = skipComment(content, cursor);
167
+ continue;
168
+ }
169
+ if (current === '"') {
170
+ cursor = skipQuoted(content, cursor);
171
+ continue;
172
+ }
173
+ if (current === "[") {
174
+ const bracketEnd = skipBracket(content, cursor);
175
+ if (bracketEnd !== null) {
176
+ cursor = bracketEnd;
177
+ continue;
178
+ }
179
+ }
180
+ if (!/[A-Za-z_]/u.test(current ?? "")) {
181
+ cursor += 1;
182
+ continue;
183
+ }
184
+ const nameStart = cursor;
185
+ cursor += 1;
186
+ while (/[A-Za-z0-9_]/u.test(content[cursor] ?? "")) {
187
+ cursor += 1;
188
+ }
189
+ const name = content.slice(nameStart, cursor);
190
+ const openParen = skipTrivia(content, cursor);
191
+ if (content[openParen] !== "(") {
192
+ continue;
193
+ }
194
+ const command = readCommand(content, name, openParen);
195
+ commands.push(command);
196
+ cursor = command.end;
197
+ }
198
+ return commands;
199
+ }
200
+ function parseProjectMetadata(content, versionFile) {
201
+ const projects = findCommands(content).filter((command) => command.name.toLowerCase() === "project");
202
+ const versionedProjects = projects.filter((command) => command.arguments.some((argument) => argument.value.toUpperCase() === "VERSION"));
203
+ if (versionedProjects.length !== 1) {
204
+ if (versionedProjects.length > 1) {
205
+ throw new Error(`${versionFile} must contain exactly one project() declaration with a VERSION argument required by release-type "cmake"; found ${versionedProjects.length}.`);
206
+ }
207
+ throw new Error(`${versionFile} is missing a literal VERSION argument in project() required by release-type "cmake".`);
208
+ }
209
+ const project = versionedProjects[0];
210
+ if (!project) {
211
+ throw new Error(`Failed to parse ${versionFile}.`);
212
+ }
213
+ const versionIndexes = project.arguments.flatMap((argument, index) => argument.value.toUpperCase() === "VERSION" ? [index] : []);
214
+ if (versionIndexes.length !== 1) {
215
+ throw new Error(`${versionFile} must contain exactly one VERSION argument in project() required by release-type "cmake".`);
216
+ }
217
+ const versionArgument = project.arguments[(versionIndexes[0] ?? -1) + 1];
218
+ if (!versionArgument?.literal ||
219
+ !CMAKE_VERSION_PATTERN.test(versionArgument.value)) {
220
+ throw new Error(`${versionFile} is missing a valid literal VERSION argument in project() required by release-type "cmake"; expected X.Y.Z or X.Y.Z.W.`);
221
+ }
222
+ const nameArgument = project.arguments[0];
223
+ const name = nameArgument?.literal && nameArgument.value.trim().length > 0
224
+ ? nameArgument.value.trim()
225
+ : null;
226
+ return {
227
+ name,
228
+ version: versionArgument.value,
229
+ versionStart: versionArgument.valueStart,
230
+ versionEnd: versionArgument.valueEnd,
231
+ };
232
+ }
233
+ export const cmakeVersionStrategy = {
234
+ name: "cmake",
235
+ getVersionFile(config) {
236
+ return config["version-file"] ?? "CMakeLists.txt";
237
+ },
238
+ validateProject(cwd, config) {
239
+ const versionFile = this.getVersionFile(config);
240
+ const versionPath = path.join(cwd, versionFile);
241
+ if (!fs.existsSync(versionPath)) {
242
+ return null;
243
+ }
244
+ try {
245
+ parseProjectMetadata(fs.readFileSync(versionPath, "utf8"), versionFile);
246
+ return null;
247
+ }
248
+ catch (error) {
249
+ return error instanceof Error ? error.message : String(error);
250
+ }
251
+ },
252
+ readVersion(cwd, config) {
253
+ const versionFile = this.getVersionFile(config);
254
+ const versionPath = path.join(cwd, versionFile);
255
+ if (!fs.existsSync(versionPath)) {
256
+ throw new Error(`Versionary requires ${versionFile} to exist.`);
257
+ }
258
+ return parseProjectMetadata(fs.readFileSync(versionPath, "utf8"), versionFile).version;
259
+ },
260
+ writeVersion(cwd, config, version) {
261
+ if (!CMAKE_VERSION_PATTERN.test(version)) {
262
+ throw new Error(`release-type "cmake" cannot write version "${version}"; CMake project() requires X.Y.Z or X.Y.Z.W without prerelease or build metadata.`);
263
+ }
264
+ const versionFile = this.getVersionFile(config);
265
+ const versionPath = path.join(cwd, versionFile);
266
+ if (!fs.existsSync(versionPath)) {
267
+ throw new Error(`Versionary requires ${versionFile} to exist.`);
268
+ }
269
+ const existing = fs.readFileSync(versionPath, "utf8");
270
+ const metadata = parseProjectMetadata(existing, versionFile);
271
+ const updated = `${existing.slice(0, metadata.versionStart)}${version}${existing.slice(metadata.versionEnd)}`;
272
+ fs.writeFileSync(versionPath, updated, "utf8");
273
+ return [versionFile];
274
+ },
275
+ readPackageName(cwd, config) {
276
+ const versionFile = this.getVersionFile(config);
277
+ const versionPath = path.join(cwd, versionFile);
278
+ if (!fs.existsSync(versionPath)) {
279
+ throw new Error(`Versionary requires ${versionFile} to exist.`);
280
+ }
281
+ return parseProjectMetadata(fs.readFileSync(versionPath, "utf8"), versionFile).name;
282
+ },
283
+ };
@@ -1,3 +1,4 @@
1
+ import { cmakeVersionStrategy } from "./cmake.js";
1
2
  import { compositeVersionStrategy } from "./composite.js";
2
3
  import { juliaVersionStrategy } from "./julia.js";
3
4
  import { latexVersionStrategy } from "./latex.js";
@@ -7,6 +8,7 @@ import { rVersionStrategy } from "./r.js";
7
8
  import { rustVersionStrategy } from "./rust.js";
8
9
  import { simpleVersionStrategy } from "./simple.js";
9
10
  const strategyRegistry = {
11
+ cmake: cmakeVersionStrategy,
10
12
  julia: juliaVersionStrategy,
11
13
  latex: latexVersionStrategy,
12
14
  simple: simpleVersionStrategy,
@@ -7,6 +7,7 @@ export interface VersionaryArtifactRule {
7
7
  "field-path"?: string;
8
8
  pattern?: string;
9
9
  replacement?: string;
10
+ "expected-matches"?: number;
10
11
  }
11
12
  export interface VersionaryPackage {
12
13
  "release-type"?: string | string[];
@@ -27,6 +28,7 @@ export interface VersionaryConfig {
27
28
  "release-draft"?: boolean;
28
29
  "release-reference-comments"?: ReleaseReferenceCommentsMode;
29
30
  "release-branch"?: string;
31
+ "separate-release-prs"?: boolean;
30
32
  "baseline-file"?: string;
31
33
  "bootstrap-sha"?: string;
32
34
  "monorepo-mode"?: "independent" | "fixed";
@@ -1,7 +1,9 @@
1
- import type { ScmClientContext, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult } from "../scm/types.js";
1
+ import type { ScmClientContext, ScmListReviewRequestsInput, ScmProvider, ScmReleaseMetadataInput, ScmReleaseMetadataResult, ScmReleaseReferenceCommentsInput, ScmReleaseReferenceCommentsResult, ScmReviewRequestInput, ScmReviewRequestResult, ScmReviewRequestSummary } from "../scm/types.js";
2
2
  export type VersionaryPluginCapability = "scm.reviewRequest" | "scm.releaseMetadata" | "scm.releaseReferenceComments";
3
3
  export type VersionaryScmReviewRequestInput = ScmReviewRequestInput;
4
4
  export type VersionaryScmReviewRequestResult = ScmReviewRequestResult;
5
+ export type VersionaryScmListReviewRequestsInput = ScmListReviewRequestsInput;
6
+ export type VersionaryScmReviewRequestSummary = ScmReviewRequestSummary;
5
7
  export type VersionaryScmReleaseMetadataInput = ScmReleaseMetadataInput;
6
8
  export type VersionaryScmReleaseMetadataResult = ScmReleaseMetadataResult;
7
9
  export type VersionaryScmReleaseReferenceCommentsInput = ScmReleaseReferenceCommentsInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",
@@ -44,6 +44,8 @@
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "lint": "biome ci .",
48
+ "gen:action": "tsx scripts/generate-action.ts",
47
49
  "gen:schema": "tsx scripts/generate-schema.ts && biome format --write schemas/config.json",
48
50
  "test": "vitest run",
49
51
  "verify": "tsx src/cli/index.ts verify",