versionary 0.15.1 → 0.16.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 CHANGED
@@ -49,7 +49,7 @@ release/tag.
49
49
 
50
50
  Current implementation focuses on:
51
51
 
52
- - strategy-based version updates (`simple`, `node`, `rust`, `r`)
52
+ - strategy-based version updates (`simple`, `node`, `rust`, `r`, `latex`)
53
53
  - release planning and changelog generation
54
54
  - review-mode vs direct-mode release flow
55
55
  - a static internal SCM client (`github` provider today)
@@ -76,8 +76,8 @@ Checklist for new strategies (for example `python`):
76
76
  - optionally implement `propagateDependentPatchImpacts(cwd, packages)` if
77
77
  dependency updates in this ecosystem should trigger dependent package patch
78
78
  bumps
79
- - optionally implement `finalizeVersionWrites(cwd, writes)` for ecosystem
80
- post-processing after all target version files are written
79
+ - optionally implement `finalizeVersionWrites(cwd, writes, context)` for
80
+ ecosystem post-processing after all target version files are written
81
81
  - add focused strategy tests for ecosystem-specific behavior and edge cases
82
82
  - add/extend strategy contract tests in `tests/strategy-contract.test.ts`
83
83
  - update schema/docs for new `release-type` behavior and defaults
@@ -105,7 +105,7 @@ Current runtime code uses a flat `src/` layout with clear module boundaries:
105
105
  - `src/cli/`: command router (`run`, `verify`, `plan`, `changelog`, `pr`, `release`)
106
106
  - `src/release/`: release orchestration (plan/changelog/PR/release/state/recovery)
107
107
  - `src/strategy/`: strategy contracts, resolver, and built-in implementations
108
- (`simple`, `node`, `rust`, `r`)
108
+ (`simple`, `node`, `rust`, `r`, `latex`)
109
109
  - `src/scm/`: SCM client contracts and provider implementation(s)
110
110
  - `src/config/`: config loading and schema validation
111
111
  - `src/git/`: git commit/range and repository URL helpers
@@ -131,6 +131,9 @@ For a quick trial, use:
131
131
  `Version:` field
132
132
  - `release-type: "rust"` uses Cargo manifests (`Cargo.toml`) as version source;
133
133
  `version-file` must point to a `Cargo.toml` (default: `Cargo.toml`)
134
+ - `release-type: "latex"` uses `build.lua` as version source and updates LaTeX
135
+ `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` metadata in `src/**/*.dtx`
136
+ using the release commit date
134
137
  - simple/default strategy keeps `version.txt` as source of truth and does not
135
138
  update `package.json`
136
139
  - stable release branch (`release-branch`, default: `versionary/release`) so
@@ -249,6 +252,27 @@ Commands:
249
252
  through the SCM client. `pnpm run` is the recommended CI entrypoint and
250
253
  auto-dispatches between PR/update and release publish.
251
254
 
255
+ ### Moloch migration example (semantic-release -> versionary)
256
+
257
+ For LaTeX projects like `moloch`, use `release-type: "latex"` so Versionary:
258
+
259
+ - bumps `build.lua` version
260
+ - updates `src/**/*.dtx` `\ProvidesPackage{...}[YYYY-MM-DD vX.Y.Z ...]` entries
261
+ with the release commit date (`git show --format=%cs <sha>`)
262
+
263
+ Example `versionary.jsonc` for `moloch`:
264
+
265
+ ```jsonc
266
+ {
267
+ "version": 1,
268
+ "review-mode": "pr",
269
+ "release-type": "latex",
270
+ "version-file": "build.lua",
271
+ "changelog-file": "CHANGELOG.md",
272
+ "release-branch": "versionary/release"
273
+ }
274
+ ```
275
+
252
276
  For first-run bootstrapping, set `bootstrap-sha` (similar to release-please).
253
277
  Subsequent runs use the baseline state file.
254
278
 
@@ -270,12 +270,14 @@ function parseFooters(body) {
270
270
  extractReferences(`${footer.token}: ${footer.value}`, footer.token);
271
271
  }
272
272
  extractInlineSentenceReferences(body);
273
- const dedupedReferences = [
274
- ...new Map(references.map((reference) => [
275
- `${reference.action ?? ""}:${reference.owner ?? ""}/${reference.repository ?? ""}:${reference.prefix}:${reference.issue ?? ""}`,
276
- reference,
277
- ])).values(),
278
- ];
273
+ const dedupedReferenceMap = new Map();
274
+ for (const reference of references) {
275
+ const key = `${reference.action?.trim().toLowerCase() ?? ""}:${reference.owner?.trim().toLowerCase() ?? ""}/${reference.repository?.trim().toLowerCase() ?? ""}:${reference.prefix}:${reference.issue ?? ""}`;
276
+ if (!dedupedReferenceMap.has(key)) {
277
+ dedupedReferenceMap.set(key, reference);
278
+ }
279
+ }
280
+ const dedupedReferences = [...dedupedReferenceMap.values()];
279
281
  return {
280
282
  bodyText: bodyLines.join("\n").trim() || "",
281
283
  footerText: footerLines.length > 0 ? footerLines.join("\n").trim() : null,
@@ -31,17 +31,41 @@ function formatCommitReferences(commit, repoBaseUrl) {
31
31
  if (references.length === 0) {
32
32
  return "";
33
33
  }
34
- return references
35
- .map((reference) => {
34
+ const formatIssueList = (issues) => {
35
+ if (issues.length === 0) {
36
+ return "";
37
+ }
38
+ if (issues.length === 1) {
39
+ return issues[0] ?? "";
40
+ }
41
+ if (issues.length === 2) {
42
+ return `${issues[0] ?? ""} and ${issues[1] ?? ""}`;
43
+ }
44
+ return `${issues.slice(0, -1).join(", ")}, and ${issues[issues.length - 1] ?? ""}`;
45
+ };
46
+ const referencesByAction = new Map();
47
+ for (const reference of references) {
36
48
  const issue = reference.issue;
37
49
  if (!issue) {
38
- return "";
50
+ continue;
39
51
  }
40
52
  const action = (reference.action?.trim() || "refs").toLowerCase();
41
- if (!repoBaseUrl) {
42
- return `${action} #${issue}`;
53
+ const formattedIssue = repoBaseUrl
54
+ ? `[#${issue}](${repoBaseUrl}/issues/${issue})`
55
+ : `#${issue}`;
56
+ const existing = referencesByAction.get(action) ?? [];
57
+ if (!existing.includes(formattedIssue)) {
58
+ existing.push(formattedIssue);
59
+ referencesByAction.set(action, existing);
60
+ }
61
+ }
62
+ return [...referencesByAction.entries()]
63
+ .map(([action, issues]) => {
64
+ const list = formatIssueList(issues);
65
+ if (list.length === 0) {
66
+ return "";
43
67
  }
44
- return `${action} [#${issue}](${repoBaseUrl}/issues/${issue})`;
68
+ return `${action} ${list}`;
45
69
  })
46
70
  .filter((entry) => entry.length > 0)
47
71
  .join(", ");
@@ -82,6 +82,13 @@ function getCommitTreeSha(cwd, revision) {
82
82
  stdio: ["ignore", "pipe", "ignore"],
83
83
  }).trim();
84
84
  }
85
+ function resolveCommitDate(cwd, revision) {
86
+ return (0, node_child_process_1.execFileSync)("git", ["show", "-s", "--format=%cs", revision], {
87
+ cwd,
88
+ encoding: "utf8",
89
+ stdio: ["ignore", "pipe", "ignore"],
90
+ }).trim();
91
+ }
85
92
  function hasOriginRemote(cwd) {
86
93
  const remotes = (0, node_child_process_1.execFileSync)("git", ["remote"], {
87
94
  cwd,
@@ -191,6 +198,15 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
191
198
  throw new Error("No releasable commits found. Nothing to open a release PR for.");
192
199
  }
193
200
  ensureCleanWorktree(cwd, options.logger);
201
+ const releaseBaselineSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
202
+ cwd,
203
+ encoding: "utf8",
204
+ stdio: ["ignore", "pipe", "ignore"],
205
+ }).trim();
206
+ const finalizeContext = {
207
+ releaseCommitSha: releaseBaselineSha,
208
+ releaseDate: resolveCommitDate(cwd, releaseBaselineSha),
209
+ };
194
210
  const updatedVersionFiles = [];
195
211
  const writesByStrategy = new Map();
196
212
  const addStrategyWrite = (strategy, write) => {
@@ -230,7 +246,7 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
230
246
  });
231
247
  }
232
248
  for (const strategyGroup of writesByStrategy.values()) {
233
- updatedVersionFiles.push(...(strategyGroup.strategy.finalizeVersionWrites?.(cwd, strategyGroup.writes) ?? []));
249
+ updatedVersionFiles.push(...(strategyGroup.strategy.finalizeVersionWrites?.(cwd, strategyGroup.writes, finalizeContext) ?? []));
234
250
  }
235
251
  const updatedArtifactFiles = (0, artifact_rules_js_1.applyConfiguredArtifactRules)(cwd, loaded.config, plan);
236
252
  const packageReleaseMetadata = buildPackageReleaseMetadata(cwd, plan, loaded.config);
@@ -266,11 +282,6 @@ function prepareReleasePr(cwd = process.cwd(), options = {}) {
266
282
  const releaseTargets = buildReleaseTargets(cwd, plan, loaded.config);
267
283
  const branch = plan.releaseBranchPrefix;
268
284
  const title = formatReleaseCommitTitle(releaseTargets);
269
- const releaseBaselineSha = (0, node_child_process_1.execFileSync)("git", ["rev-parse", "HEAD"], {
270
- cwd,
271
- encoding: "utf8",
272
- stdio: ["ignore", "pipe", "ignore"],
273
- }).trim();
274
285
  const hasRemoteReleaseBranch = remoteReleaseBranchExists(cwd, branch);
275
286
  const remoteReleaseRef = hasRemoteReleaseBranch
276
287
  ? fetchRemoteReleaseBranch(cwd, branch)
@@ -0,0 +1,2 @@
1
+ import type { VersionStrategy } from "./types.js";
2
+ export declare const latexVersionStrategy: VersionStrategy;
@@ -0,0 +1,96 @@
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.latexVersionStrategy = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const BUILD_LUA_VERSION_PATTERN = /^(\s*version\s*=\s*")([^"]+)(")/mu;
10
+ const PROVIDES_PACKAGE_PATTERN = /\\ProvidesPackage\{([^}]+)\}\[\d{4}-\d{2}-\d{2} v([0-9]+\.[0-9]+\.[0-9]+) ([^\]]+)\]/gu;
11
+ function normalizeRelative(base, target) {
12
+ return node_path_1.default.relative(base, target).replaceAll("\\", "/");
13
+ }
14
+ function collectDtxFiles(dir) {
15
+ if (!node_fs_1.default.existsSync(dir)) {
16
+ return [];
17
+ }
18
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
19
+ const files = [];
20
+ for (const entry of entries) {
21
+ const target = node_path_1.default.join(dir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ files.push(...collectDtxFiles(target));
24
+ continue;
25
+ }
26
+ if (entry.isFile() && target.endsWith(".dtx")) {
27
+ files.push(target);
28
+ }
29
+ }
30
+ return files.sort((a, b) => a.localeCompare(b));
31
+ }
32
+ function replaceBuildLuaVersion(content, version, versionFile) {
33
+ const match = content.match(BUILD_LUA_VERSION_PATTERN);
34
+ if (!match) {
35
+ throw new Error(`${versionFile} is missing a valid version field required by release-type "latex".`);
36
+ }
37
+ return content.replace(BUILD_LUA_VERSION_PATTERN, `$1${version}$3`);
38
+ }
39
+ function replaceProvidesPackageMetadata(content, version, releaseDate, relativePath) {
40
+ const matches = [...content.matchAll(PROVIDES_PACKAGE_PATTERN)];
41
+ if (matches.length !== 1) {
42
+ throw new Error(`${relativePath} must contain exactly one \\ProvidesPackage metadata entry; matched ${matches.length}.`);
43
+ }
44
+ return content.replace(PROVIDES_PACKAGE_PATTERN, (_full, pkg, _prevVersion, desc) => `\\ProvidesPackage{${pkg}}[${releaseDate} v${version} ${desc}]`);
45
+ }
46
+ exports.latexVersionStrategy = {
47
+ name: "latex",
48
+ getVersionFile(config) {
49
+ return config["version-file"] ?? "build.lua";
50
+ },
51
+ readVersion(cwd, config) {
52
+ const versionFile = this.getVersionFile(config);
53
+ const versionPath = node_path_1.default.join(cwd, versionFile);
54
+ if (!node_fs_1.default.existsSync(versionPath)) {
55
+ throw new Error(`Versionary requires ${versionFile} to exist.`);
56
+ }
57
+ const content = node_fs_1.default.readFileSync(versionPath, "utf8");
58
+ const match = content.match(BUILD_LUA_VERSION_PATTERN);
59
+ if (!match?.[2]) {
60
+ throw new Error(`${versionFile} is missing a valid version field required by release-type "latex".`);
61
+ }
62
+ return match[2].trim();
63
+ },
64
+ writeVersion(cwd, config, version) {
65
+ const versionFile = this.getVersionFile(config);
66
+ const versionPath = node_path_1.default.join(cwd, versionFile);
67
+ if (!node_fs_1.default.existsSync(versionPath)) {
68
+ throw new Error(`Versionary requires ${versionFile} to exist.`);
69
+ }
70
+ const next = replaceBuildLuaVersion(node_fs_1.default.readFileSync(versionPath, "utf8"), version, versionFile);
71
+ node_fs_1.default.writeFileSync(versionPath, next, "utf8");
72
+ return [versionFile];
73
+ },
74
+ finalizeVersionWrites(cwd, writes, context) {
75
+ const updated = new Set();
76
+ for (const write of writes) {
77
+ const packageRoot = write.packagePath === "." ? cwd : node_path_1.default.join(cwd, write.packagePath);
78
+ const srcDir = node_path_1.default.join(packageRoot, "src");
79
+ const dtxFiles = collectDtxFiles(srcDir);
80
+ if (dtxFiles.length === 0) {
81
+ throw new Error(`release-type "latex" requires at least one .dtx file under ${normalizeRelative(cwd, srcDir)}.`);
82
+ }
83
+ for (const dtxPath of dtxFiles) {
84
+ const relativePath = normalizeRelative(cwd, dtxPath);
85
+ const existing = node_fs_1.default.readFileSync(dtxPath, "utf8");
86
+ const next = replaceProvidesPackageMetadata(existing, write.version, context.releaseDate, relativePath);
87
+ if (next === existing) {
88
+ continue;
89
+ }
90
+ node_fs_1.default.writeFileSync(dtxPath, next, "utf8");
91
+ updated.add(relativePath);
92
+ }
93
+ }
94
+ return [...updated].sort((a, b) => a.localeCompare(b));
95
+ },
96
+ };
@@ -2,11 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listKnownReleaseTypes = listKnownReleaseTypes;
4
4
  exports.resolveVersionStrategy = resolveVersionStrategy;
5
+ const latex_js_1 = require("./latex.js");
5
6
  const node_js_1 = require("./node.js");
6
7
  const r_js_1 = require("./r.js");
7
8
  const rust_js_1 = require("./rust.js");
8
9
  const simple_js_1 = require("./simple.js");
9
10
  const strategyRegistry = {
11
+ latex: latex_js_1.latexVersionStrategy,
10
12
  simple: simple_js_1.simpleVersionStrategy,
11
13
  node: node_js_1.nodeVersionStrategy,
12
14
  rust: rust_js_1.rustVersionStrategy,
@@ -721,7 +721,7 @@ exports.rustVersionStrategy = {
721
721
  .filter((pkgPath) => Boolean(pkgPath))
722
722
  .sort((a, b) => a.localeCompare(b));
723
723
  },
724
- finalizeVersionWrites(cwd, writes) {
724
+ finalizeVersionWrites(cwd, writes, _context) {
725
725
  const manifestToVersion = {};
726
726
  for (const write of writes) {
727
727
  manifestToVersion[write.versionFile] = write.version;
@@ -10,6 +10,10 @@ export interface StrategyVersionWriteContext {
10
10
  versionFile: string;
11
11
  version: string;
12
12
  }
13
+ export interface StrategyFinalizeContext {
14
+ releaseCommitSha: string;
15
+ releaseDate: string;
16
+ }
13
17
  export interface VersionStrategy {
14
18
  name: string;
15
19
  getVersionFile(config: VersionaryConfig): string;
@@ -19,5 +23,5 @@ export interface VersionStrategy {
19
23
  validateProject?(cwd: string, config: VersionaryConfig): string | null;
20
24
  readPackageName?(cwd: string, config: VersionaryConfig): string | null;
21
25
  propagateDependentPatchImpacts?(cwd: string, packages: StrategyPackagePlanContext[]): string[];
22
- finalizeVersionWrites?(cwd: string, writes: StrategyVersionWriteContext[]): string[];
26
+ finalizeVersionWrites?(cwd: string, writes: StrategyVersionWriteContext[], context: StrategyFinalizeContext): string[];
23
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "versionary",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",