versionary 0.30.0 → 0.31.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 +10 -2
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +1 -0
- package/dist/release/plan.js +11 -3
- 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 +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ release/tag.
|
|
|
50
50
|
Current implementation focuses on:
|
|
51
51
|
|
|
52
52
|
- strategy-based version updates (`simple`, `node`, `rust`, `r`, `latex`,
|
|
53
|
-
`python`)
|
|
53
|
+
`python`, `julia`)
|
|
54
54
|
- release planning and changelog generation
|
|
55
55
|
- review-mode vs direct-mode release flow
|
|
56
56
|
- a static internal SCM client (`github` provider today)
|
|
@@ -110,7 +110,7 @@ Current runtime code uses a flat `src/` layout with clear module boundaries:
|
|
|
110
110
|
- `src/cli/`: command router (`run`, `verify`, `plan`, `changelog`, `pr`, `release`)
|
|
111
111
|
- `src/release/`: release orchestration (plan/changelog/PR/release/state/recovery)
|
|
112
112
|
- `src/strategy/`: strategy contracts, resolver, and built-in implementations
|
|
113
|
-
(`simple`, `node`, `rust`, `r`, `latex`, `python`)
|
|
113
|
+
(`simple`, `node`, `rust`, `r`, `latex`, `python`, `julia`)
|
|
114
114
|
- `src/scm/`: SCM client contracts and provider implementation(s)
|
|
115
115
|
- `src/config/`: config loading and schema validation
|
|
116
116
|
- `src/git/`: git commit/range and repository URL helpers
|
|
@@ -144,6 +144,9 @@ For a quick trial, use:
|
|
|
144
144
|
Python source file (e.g. `src/<pkg>/__init__.py`) to update a `__version__`
|
|
145
145
|
assignment instead. Refreshes `poetry.lock`/`uv.lock`/`pdm.lock` at the
|
|
146
146
|
package root if present
|
|
147
|
+
- `release-type: "julia"` uses `Project.toml` (default) as version source and
|
|
148
|
+
updates the top-level `version` field (Julia keeps `version`/`name` as root
|
|
149
|
+
keys, not under a section)
|
|
147
150
|
- `release-type` can also be an array of strategy names to compose them across
|
|
148
151
|
manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first
|
|
149
152
|
entry is the *primary* (drives `readVersion`, `readPackageName`, and consumes
|
|
@@ -196,6 +199,11 @@ For a quick trial, use:
|
|
|
196
199
|
for a package are the union of the top-level list and the package's own
|
|
197
200
|
list. The top-level list also applies to a single-package (non-`packages`)
|
|
198
201
|
repository.
|
|
202
|
+
- per-package `allow-stable-major` overrides the top-level setting for that
|
|
203
|
+
package's own bump (including dependency-propagation and `follows`-driven
|
|
204
|
+
bumps), so a `0.y.z` package can transition to `1.0.0` on a breaking release
|
|
205
|
+
independently of its siblings. In `fixed` mode the single shared version is
|
|
206
|
+
governed by the top-level `allow-stable-major` only.
|
|
199
207
|
|
|
200
208
|
```jsonc
|
|
201
209
|
// Editor extension that bundles the root CLI artifact
|
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<{
|
package/dist/config/schema.js
CHANGED
|
@@ -57,6 +57,7 @@ const packageSchema = z
|
|
|
57
57
|
"package-name": z.string().optional(),
|
|
58
58
|
"changelog-file": z.string().optional(),
|
|
59
59
|
"changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(),
|
|
60
|
+
"allow-stable-major": z.boolean().optional(),
|
|
60
61
|
"exclude-paths": z.array(z.string()).optional(),
|
|
61
62
|
"extra-files": z.array(artifactRuleSchema).optional(),
|
|
62
63
|
follows: z.array(z.string().min(1)).optional(),
|
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/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
|
@@ -13,6 +13,7 @@ export interface VersionaryPackage {
|
|
|
13
13
|
"package-name"?: string;
|
|
14
14
|
"changelog-file"?: string;
|
|
15
15
|
"changelog-format"?: VersionaryChangelogFormat;
|
|
16
|
+
"allow-stable-major"?: boolean;
|
|
16
17
|
"exclude-paths"?: string[];
|
|
17
18
|
"extra-files"?: VersionaryArtifactRule[];
|
|
18
19
|
follows?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "versionary",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Automatic release framework based on conventional commits and semantic versioning",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"releasing",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.json",
|
|
44
44
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"gen:schema": "tsx scripts/generate-schema.ts && biome format --write schemas/config.json",
|
|
45
46
|
"test": "vitest run",
|
|
46
47
|
"verify": "tsx src/cli/index.ts verify",
|
|
47
48
|
"run": "tsx src/cli/index.ts run",
|