versionary
Advanced tools
| import type { VersionStrategy } from "./types.js"; | ||
| export declare const juliaVersionStrategy: VersionStrategy; |
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { parse as parseToml } from "smol-toml"; | ||
| function parseProjectToml(content, versionFile) { | ||
| let parsed; | ||
| try { | ||
| parsed = parseToml(content); | ||
| } | ||
| catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| throw new Error(`Failed to parse ${versionFile}: ${message}`); | ||
| } | ||
| if (parsed && typeof parsed === "object") { | ||
| return parsed; | ||
| } | ||
| throw new Error(`Failed to parse ${versionFile}: not a TOML table.`); | ||
| } | ||
| function readProjectVersion(content, versionFile) { | ||
| const parsed = parseProjectToml(content, versionFile); | ||
| const version = parsed.version; | ||
| if (typeof version === "string" && version.trim().length > 0) { | ||
| return version.trim(); | ||
| } | ||
| throw new Error(`${versionFile} is missing a valid root "version" field required by release-type "julia".`); | ||
| } | ||
| function writeProjectVersion(rawContent, versionFile, version) { | ||
| const lineEnding = rawContent.includes("\r\n") ? "\r\n" : "\n"; | ||
| const hasFinalLineEnding = rawContent.endsWith("\n") || rawContent.endsWith("\r\n"); | ||
| const lines = rawContent.split(/\r?\n/u); | ||
| let activeTable = null; | ||
| let replaced = false; | ||
| for (let index = 0; index < lines.length; index += 1) { | ||
| const line = lines[index] ?? ""; | ||
| const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/u); | ||
| if (sectionMatch) { | ||
| activeTable = sectionMatch[1]?.trim() ?? null; | ||
| continue; | ||
| } | ||
| // The Julia version is a root key: only match before the first table header. | ||
| if (activeTable !== null) { | ||
| continue; | ||
| } | ||
| const versionMatch = line.match(/^(\s*version\s*=\s*)(["'])([^"']*)(\2)(\s*(?:#.*)?)?$/u); | ||
| if (!versionMatch) { | ||
| continue; | ||
| } | ||
| const [, prefix = "", quote = '"', , , suffix = ""] = versionMatch; | ||
| lines[index] = `${prefix}${quote}${version}${quote}${suffix}`; | ||
| replaced = true; | ||
| break; | ||
| } | ||
| if (!replaced) { | ||
| throw new Error(`${versionFile} is missing a valid root "version" field required by release-type "julia".`); | ||
| } | ||
| let updated = lines.join(lineEnding); | ||
| if (hasFinalLineEnding && !updated.endsWith(lineEnding)) { | ||
| updated += lineEnding; | ||
| } | ||
| if (!hasFinalLineEnding && updated.endsWith(lineEnding)) { | ||
| updated = updated.slice(0, -lineEnding.length); | ||
| } | ||
| return updated; | ||
| } | ||
| export const juliaVersionStrategy = { | ||
| name: "julia", | ||
| getVersionFile(config) { | ||
| return config["version-file"] ?? "Project.toml"; | ||
| }, | ||
| validateProject(cwd, config) { | ||
| const versionFile = this.getVersionFile(config); | ||
| const versionPath = path.join(cwd, versionFile); | ||
| if (!fs.existsSync(versionPath)) { | ||
| return null; | ||
| } | ||
| try { | ||
| readProjectVersion(fs.readFileSync(versionPath, "utf8"), versionFile); | ||
| return null; | ||
| } | ||
| catch (error) { | ||
| return error instanceof Error ? error.message : String(error); | ||
| } | ||
| }, | ||
| readVersion(cwd, config) { | ||
| const versionFile = this.getVersionFile(config); | ||
| const versionPath = path.join(cwd, versionFile); | ||
| if (!fs.existsSync(versionPath)) { | ||
| throw new Error(`Versionary requires ${versionFile} to exist.`); | ||
| } | ||
| return readProjectVersion(fs.readFileSync(versionPath, "utf8"), versionFile); | ||
| }, | ||
| writeVersion(cwd, config, version) { | ||
| const versionFile = this.getVersionFile(config); | ||
| const versionPath = path.join(cwd, versionFile); | ||
| if (!fs.existsSync(versionPath)) { | ||
| throw new Error(`Versionary requires ${versionFile} to exist.`); | ||
| } | ||
| const existing = fs.readFileSync(versionPath, "utf8"); | ||
| const updated = writeProjectVersion(existing, versionFile, version); | ||
| fs.writeFileSync(versionPath, updated, "utf8"); | ||
| return [versionFile]; | ||
| }, | ||
| readPackageName(cwd, config) { | ||
| const versionFile = this.getVersionFile(config); | ||
| const versionPath = path.join(cwd, versionFile); | ||
| if (!fs.existsSync(versionPath)) { | ||
| throw new Error(`Versionary requires ${versionFile} to exist.`); | ||
| } | ||
| const parsed = parseProjectToml(fs.readFileSync(versionPath, "utf8"), versionFile); | ||
| const name = parsed.name; | ||
| if (typeof name === "string" && name.trim().length > 0) { | ||
| return name.trim(); | ||
| } | ||
| return null; | ||
| }, | ||
| }; |
@@ -43,2 +43,3 @@ import { z } from "zod"; | ||
| }>>; | ||
| "allow-stable-major": z.ZodOptional<z.ZodBoolean>; | ||
| "exclude-paths": z.ZodOptional<z.ZodArray<z.ZodString>>; | ||
@@ -45,0 +46,0 @@ "extra-files": z.ZodOptional<z.ZodArray<z.ZodObject<{ |
@@ -60,2 +60,3 @@ import { z } from "zod"; | ||
| "changelog-format": z.enum(["markdown-changelog", "r-news"]).optional(), | ||
| "allow-stable-major": z.boolean().optional(), | ||
| "exclude-paths": z.array(z.string()).optional(), | ||
@@ -62,0 +63,0 @@ "extra-files": z.array(artifactRuleSchema).optional(), |
+11
-3
@@ -53,2 +53,4 @@ import fs from "node:fs"; | ||
| const allowStableMajor = loaded.config["allow-stable-major"] ?? false; | ||
| const allowStableMajorForPath = (packagePath) => loaded.config.packages?.[packagePath]?.["allow-stable-major"] ?? | ||
| allowStableMajor; | ||
| const monorepoMode = getMode(loaded.config["monorepo-mode"]); | ||
@@ -82,3 +84,5 @@ const buildPackagePlan = (pkg) => { | ||
| const nextVersion = releaseType | ||
| ? bumpVersion(packageCurrentVersion, releaseType, { allowStableMajor }) | ||
| ? bumpVersion(packageCurrentVersion, releaseType, { | ||
| allowStableMajor: allowStableMajorForPath(pkg.path), | ||
| }) | ||
| : null; | ||
@@ -203,3 +207,5 @@ return { | ||
| releaseType: "patch", | ||
| nextVersion: bumpVersion(current, "patch", { allowStableMajor }), | ||
| nextVersion: bumpVersion(current, "patch", { | ||
| allowStableMajor: allowStableMajorForPath(pkgPlan.path), | ||
| }), | ||
| bumpReason: "dependency-propagation", | ||
@@ -240,3 +246,5 @@ dependencySourcePaths, | ||
| const nextVersion = combinedReleaseType | ||
| ? bumpVersion(baseVersion, combinedReleaseType, { allowStableMajor }) | ||
| ? bumpVersion(baseVersion, combinedReleaseType, { | ||
| allowStableMajor: allowStableMajorForPath(pkgPlan.path), | ||
| }) | ||
| : null; | ||
@@ -243,0 +251,0 @@ return { |
@@ -17,10 +17,12 @@ export type ReleaseType = "major" | "minor" | "patch" | null; | ||
| * Decide whether a changelog heading denotes a released version (e.g. | ||
| * `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`) | ||
| * rather than a manual-notes heading (e.g. `## Unreleased`, `## Upcoming`). | ||
| * `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`, | ||
| * `# pkg 8.0`) rather than a manual-notes heading (e.g. `## Unreleased`, | ||
| * `## Upcoming`). | ||
| * | ||
| * Intentionally liberal: a heading counts as a version if it contains any | ||
| * version-like token that {@link isValidVersion} accepts. This errs toward | ||
| * "it's a version", so a real release heading is never mistaken for a notes | ||
| * block (and therefore never stripped). Dates such as `2026-06-02` use dashes, | ||
| * not dots, so they never match the three-component token. | ||
| * version-like token that {@link isValidVersion} accepts, or ends with a bare | ||
| * `major.minor` token (the R `NEWS.md` convention). This errs toward "it's a | ||
| * version", so a real release heading is never mistaken for a notes block (and | ||
| * therefore never stripped). Dates such as `2026-06-02` use dashes, not dots, | ||
| * so they never match either token. | ||
| */ | ||
@@ -27,0 +29,0 @@ export declare function isVersionHeading(heading: string): boolean; |
@@ -42,19 +42,26 @@ export function maxReleaseType(types) { | ||
| const VERSION_TOKEN_PATTERN = /v?(\d+\.\d+\.\d+(?:\.\d+)?)/u; | ||
| // R `NEWS.md` headings conventionally abbreviate to `major.minor` (e.g. | ||
| // `# pkg 8.0`). Accept a bare two-component token only when it is the trailing | ||
| // token of the heading, so genuine R release headings register as versions | ||
| // while prose like `## Notes for 2.0 milestone` does not. | ||
| const TRAILING_MAJOR_MINOR_PATTERN = /\bv?\d+\.\d+\s*$/u; | ||
| /** | ||
| * Decide whether a changelog heading denotes a released version (e.g. | ||
| * `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`) | ||
| * rather than a manual-notes heading (e.g. `## Unreleased`, `## Upcoming`). | ||
| * `## [0.28.2](url) (2026-06-02)`, `## 1.2.3`, `## v1.2.3`, `## 0.28.2.9000`, | ||
| * `# pkg 8.0`) rather than a manual-notes heading (e.g. `## Unreleased`, | ||
| * `## Upcoming`). | ||
| * | ||
| * Intentionally liberal: a heading counts as a version if it contains any | ||
| * version-like token that {@link isValidVersion} accepts. This errs toward | ||
| * "it's a version", so a real release heading is never mistaken for a notes | ||
| * block (and therefore never stripped). Dates such as `2026-06-02` use dashes, | ||
| * not dots, so they never match the three-component token. | ||
| * version-like token that {@link isValidVersion} accepts, or ends with a bare | ||
| * `major.minor` token (the R `NEWS.md` convention). This errs toward "it's a | ||
| * version", so a real release heading is never mistaken for a notes block (and | ||
| * therefore never stripped). Dates such as `2026-06-02` use dashes, not dots, | ||
| * so they never match either token. | ||
| */ | ||
| export function isVersionHeading(heading) { | ||
| const match = heading.match(VERSION_TOKEN_PATTERN); | ||
| if (!match?.[1]) { | ||
| return false; | ||
| if (match?.[1] && isValidVersion(match[1])) { | ||
| return true; | ||
| } | ||
| return isValidVersion(match[1]); | ||
| return TRAILING_MAJOR_MINOR_PATTERN.test(heading); | ||
| } | ||
@@ -61,0 +68,0 @@ function isNumericIdentifier(identifier) { |
| import { compositeVersionStrategy } from "./composite.js"; | ||
| import { juliaVersionStrategy } from "./julia.js"; | ||
| import { latexVersionStrategy } from "./latex.js"; | ||
@@ -9,2 +10,3 @@ import { nodeVersionStrategy } from "./node.js"; | ||
| const strategyRegistry = { | ||
| julia: juliaVersionStrategy, | ||
| latex: latexVersionStrategy, | ||
@@ -11,0 +13,0 @@ simple: simpleVersionStrategy, |
@@ -16,2 +16,3 @@ export type ConfigFileFormat = "jsonc" | "json" | "toml" | "js"; | ||
| "changelog-format"?: VersionaryChangelogFormat; | ||
| "allow-stable-major"?: boolean; | ||
| "exclude-paths"?: string[]; | ||
@@ -18,0 +19,0 @@ "extra-files"?: VersionaryArtifactRule[]; |
+2
-1
| { | ||
| "name": "versionary", | ||
| "version": "0.30.0", | ||
| "version": "0.31.0", | ||
| "description": "Automatic release framework based on conventional commits and semantic versioning", | ||
@@ -45,2 +45,3 @@ "keywords": [ | ||
| "typecheck": "tsc -p tsconfig.json --noEmit", | ||
| "gen:schema": "tsx scripts/generate-schema.ts && biome format --write schemas/config.json", | ||
| "test": "vitest run", | ||
@@ -47,0 +48,0 @@ "verify": "tsx src/cli/index.ts verify", |
+10
-2
@@ -53,3 +53,3 @@ # Versionary | ||
| - strategy-based version updates (`simple`, `node`, `rust`, `r`, `latex`, | ||
| `python`) | ||
| `python`, `julia`) | ||
| - release planning and changelog generation | ||
@@ -114,3 +114,3 @@ - review-mode vs direct-mode release flow | ||
| - `src/strategy/`: strategy contracts, resolver, and built-in implementations | ||
| (`simple`, `node`, `rust`, `r`, `latex`, `python`) | ||
| (`simple`, `node`, `rust`, `r`, `latex`, `python`, `julia`) | ||
| - `src/scm/`: SCM client contracts and provider implementation(s) | ||
@@ -149,2 +149,5 @@ - `src/config/`: config loading and schema validation | ||
| package root if present | ||
| - `release-type: "julia"` uses `Project.toml` (default) as version source and | ||
| updates the top-level `version` field (Julia keeps `version`/`name` as root | ||
| keys, not under a section) | ||
| - `release-type` can also be an array of strategy names to compose them across | ||
@@ -202,2 +205,7 @@ manifests, e.g. `["python", "rust"]` for a PyO3/maturin project: the first | ||
| repository. | ||
| - per-package `allow-stable-major` overrides the top-level setting for that | ||
| package's own bump (including dependency-propagation and `follows`-driven | ||
| bumps), so a `0.y.z` package can transition to `1.0.0` on a breaking release | ||
| independently of its siblings. In `fixed` mode the single shared version is | ||
| governed by the top-level `allow-stable-major` only. | ||
@@ -204,0 +212,0 @@ ```jsonc |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
273953
2.4%71
2.9%6211
2.29%495
1.64%39
2.63%