versionary 0.15.1 → 0.17.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
 
@@ -8,6 +8,7 @@ exports.loadConfig = loadConfig;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const jsonc_parser_1 = require("jsonc-parser");
11
+ const resolve_js_1 = require("../strategy/resolve.js");
11
12
  const schema_js_1 = require("./schema.js");
12
13
  const SUPPORTED_FILES = [
13
14
  { file: "versionary.jsonc", format: "jsonc" },
@@ -22,6 +23,34 @@ function parseConfig(raw, format) {
22
23
  function isRecord(value) {
23
24
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
25
  }
26
+ function validateReleaseTypes(config) {
27
+ try {
28
+ (0, resolve_js_1.resolveVersionStrategy)(config);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error) {
32
+ throw error;
33
+ }
34
+ const known = (0, resolve_js_1.listKnownReleaseTypes)().join(", ");
35
+ throw new Error(`Unsupported release-type. Supported release types: ${known}.`);
36
+ }
37
+ for (const [packagePath, packageConfig] of Object.entries(config.packages ?? {})) {
38
+ const packageReleaseType = packageConfig["release-type"];
39
+ if (!packageReleaseType) {
40
+ continue;
41
+ }
42
+ try {
43
+ (0, resolve_js_1.resolveVersionStrategy)({ ...config, "release-type": packageReleaseType });
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error) {
47
+ throw new Error(`${error.message} (in packages["${packagePath}"])`);
48
+ }
49
+ const known = (0, resolve_js_1.listKnownReleaseTypes)().join(", ");
50
+ throw new Error(`Unsupported release-type in packages["${packagePath}"]. Supported release types: ${known}.`);
51
+ }
52
+ }
53
+ }
25
54
  function findConfigFile(cwd) {
26
55
  for (const candidate of SUPPORTED_FILES) {
27
56
  const candidatePath = node_path_1.default.join(cwd, candidate.file);
@@ -45,6 +74,7 @@ function loadConfig(cwd = process.cwd()) {
45
74
  throw new Error('The "plugins" config key is no longer supported. Versionary uses built-in integrations only.');
46
75
  }
47
76
  const validated = schema_js_1.configSchema.parse(parsed);
77
+ validateReleaseTypes(validated);
48
78
  return {
49
79
  path: found.path,
50
80
  format: found.format,
@@ -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,
@@ -12,8 +12,14 @@ export declare function renderSimpleReleaseNotes(input: {
12
12
  }>;
13
13
  }, options?: {
14
14
  includeFooter?: boolean;
15
+ headerLabel?: string;
16
+ }): string;
17
+ export declare function renderReviewRequestFooter(): string;
18
+ export declare function renderReleasePlanChangelog(plan: ReleasePlan, options?: {
19
+ headerLabel?: string;
20
+ includeFooter?: boolean;
21
+ cwd?: string;
15
22
  }): string;
16
- export declare function renderReleasePlanChangelog(plan: ReleasePlan): string;
17
23
  /** @deprecated Use renderReleasePlanChangelog. */
18
24
  export declare function renderSimpleChangelog(plan: SimplePlan): string;
19
25
  export declare function renderPackageChangelogSection(input: {
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.renderSimpleReleaseNotes = renderSimpleReleaseNotes;
7
+ exports.renderReviewRequestFooter = renderReviewRequestFooter;
7
8
  exports.renderReleasePlanChangelog = renderReleasePlanChangelog;
8
9
  exports.renderSimpleChangelog = renderSimpleChangelog;
9
10
  exports.renderPackageChangelogSection = renderPackageChangelogSection;
@@ -13,6 +14,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
13
14
  const node_path_1 = __importDefault(require("node:path"));
14
15
  const commits_js_1 = require("../git/commits.js");
15
16
  const repo_url_js_1 = require("../git/repo-url.js");
17
+ const REVIEW_REQUEST_FOOTER = "---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).";
16
18
  function formatDate() {
17
19
  return new Date().toISOString().slice(0, 10);
18
20
  }
@@ -31,17 +33,41 @@ function formatCommitReferences(commit, repoBaseUrl) {
31
33
  if (references.length === 0) {
32
34
  return "";
33
35
  }
34
- return references
35
- .map((reference) => {
36
+ const formatIssueList = (issues) => {
37
+ if (issues.length === 0) {
38
+ return "";
39
+ }
40
+ if (issues.length === 1) {
41
+ return issues[0] ?? "";
42
+ }
43
+ if (issues.length === 2) {
44
+ return `${issues[0] ?? ""} and ${issues[1] ?? ""}`;
45
+ }
46
+ return `${issues.slice(0, -1).join(", ")}, and ${issues[issues.length - 1] ?? ""}`;
47
+ };
48
+ const referencesByAction = new Map();
49
+ for (const reference of references) {
36
50
  const issue = reference.issue;
37
51
  if (!issue) {
38
- return "";
52
+ continue;
39
53
  }
40
54
  const action = (reference.action?.trim() || "refs").toLowerCase();
41
- if (!repoBaseUrl) {
42
- return `${action} #${issue}`;
55
+ const formattedIssue = repoBaseUrl
56
+ ? `[#${issue}](${repoBaseUrl}/issues/${issue})`
57
+ : `#${issue}`;
58
+ const existing = referencesByAction.get(action) ?? [];
59
+ if (!existing.includes(formattedIssue)) {
60
+ existing.push(formattedIssue);
61
+ referencesByAction.set(action, existing);
62
+ }
63
+ }
64
+ return [...referencesByAction.entries()]
65
+ .map(([action, issues]) => {
66
+ const list = formatIssueList(issues);
67
+ if (list.length === 0) {
68
+ return "";
43
69
  }
44
- return `${action} [#${issue}](${repoBaseUrl}/issues/${issue})`;
70
+ return `${action} ${list}`;
45
71
  })
46
72
  .filter((entry) => entry.length > 0)
47
73
  .join(", ");
@@ -112,9 +138,10 @@ function groupCommitLines(commits, repoUrl) {
112
138
  }
113
139
  function renderSimpleReleaseNotes(input, options = {}) {
114
140
  const repoUrl = (0, repo_url_js_1.resolveRepositoryWebBaseUrl)(input.cwd ?? process.cwd());
141
+ const headerLabel = options.headerLabel ?? input.nextVersion;
115
142
  const header = repoUrl
116
- ? `## [${input.nextVersion}](${repoUrl}/compare/v${input.currentVersion}...v${input.nextVersion}) (${formatDate()})`
117
- : `## ${input.nextVersion} (${formatDate()})`;
143
+ ? `## [${headerLabel}](${repoUrl}/compare/v${input.currentVersion}...v${input.nextVersion}) (${formatDate()})`
144
+ : `## ${headerLabel} (${formatDate()})`;
118
145
  const grouped = groupCommitLines(input.commits, repoUrl);
119
146
  const sections = [];
120
147
  if (grouped.breaking.length > 0) {
@@ -132,13 +159,16 @@ function renderSimpleReleaseNotes(input, options = {}) {
132
159
  if (input.dependencies && input.dependencies.length > 0) {
133
160
  sections.push("### Dependencies", ...input.dependencies.map((dependency) => `- updated ${dependency.name} to v${dependency.version}`), "");
134
161
  }
135
- const lines = [header, "", ...sections];
162
+ const body = [header, "", ...sections].join("\n").trimEnd();
136
163
  if (options.includeFooter) {
137
- lines.push("\n---\n\nThis PR was generated by [Versionary](https://github.com/jolars/versionary).");
164
+ return `${body}\n\n${renderReviewRequestFooter()}`;
138
165
  }
139
- return lines.join("\n");
166
+ return body;
167
+ }
168
+ function renderReviewRequestFooter() {
169
+ return REVIEW_REQUEST_FOOTER;
140
170
  }
141
- function renderReleasePlanChangelog(plan) {
171
+ function renderReleasePlanChangelog(plan, options = {}) {
142
172
  if (!plan.nextVersion) {
143
173
  return "";
144
174
  }
@@ -170,8 +200,11 @@ function renderReleasePlanChangelog(plan) {
170
200
  currentVersion: plan.currentVersion,
171
201
  nextVersion: plan.nextVersion,
172
202
  commits: dedupedCommits,
173
- cwd: process.cwd(),
203
+ cwd: options.cwd ?? process.cwd(),
174
204
  dependencies,
205
+ }, {
206
+ includeFooter: options.includeFooter,
207
+ headerLabel: options.headerLabel,
175
208
  });
176
209
  }
177
210
  /** @deprecated Use renderReleasePlanChangelog. */
@@ -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)
@@ -337,25 +348,15 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
337
348
  .sort((a, b) => a.name.localeCompare(b.name));
338
349
  return directSources;
339
350
  };
340
- const relabelSectionHeader = (notes, packageLabel, nextVersion) => {
341
- const linkedHeader = notes.match(/^##\s+\[([^\]]+)\]\(([^)]+)\)\s+\(([^)]+)\)/u);
342
- if (linkedHeader) {
343
- const [, , compareUrl, date] = linkedHeader;
344
- return notes.replace(/^##\s+\[[^\]]+\]\([^)]+\)\s+\([^)]+\)/u, `## [${packageLabel}: ${nextVersion}](${compareUrl}) (${date})`);
345
- }
346
- const plainHeader = notes.match(/^##\s+([^\s]+)\s+\(([^)]+)\)/u);
347
- if (plainHeader) {
348
- const [, , date] = plainHeader;
349
- return notes.replace(/^##\s+[^\s]+\s+\([^)]+\)/u, `## ${packageLabel}: ${nextVersion} (${date})`);
350
- }
351
- return notes;
352
- };
353
351
  if (plan?.packages && plan.packages.length > 1) {
354
352
  const sections = [];
355
353
  const rootPackage = plan.packages.find((pkg) => pkg.path === "." && pkg.nextVersion);
356
354
  if (rootPackage?.nextVersion) {
357
- const rootNotes = (0, changelog_js_1.renderReleasePlanChangelog)(plan);
358
- sections.push(relabelSectionHeader(rootNotes, formatPackageLabel("."), rootPackage.nextVersion));
355
+ const rootNotes = (0, changelog_js_1.renderReleasePlanChangelog)(plan, {
356
+ headerLabel: `${formatPackageLabel(".")}: ${rootPackage.nextVersion}`,
357
+ cwd,
358
+ });
359
+ sections.push(rootNotes);
359
360
  }
360
361
  const packageSections = plan.packages
361
362
  .filter((pkg) => pkg.path !== "." && pkg.nextVersion)
@@ -368,15 +369,18 @@ function renderSimpleReviewRequestBody(version, previousVersion, commits, plan =
368
369
  commits: pkg.commits,
369
370
  cwd,
370
371
  dependencies: propagatedDependencies,
371
- }, { includeFooter: false });
372
- return relabelSectionHeader(notes, packageLabel, pkg.nextVersion ?? "");
372
+ }, {
373
+ includeFooter: false,
374
+ headerLabel: `${packageLabel}: ${pkg.nextVersion ?? ""}`,
375
+ });
376
+ return notes;
373
377
  });
374
378
  sections.push(...packageSections);
375
379
  const bodySections = sections.join("\n\n");
376
380
  if (bodySections.length === 0) {
377
- return "This PR was generated by Versionary.";
381
+ return (0, changelog_js_1.renderReviewRequestFooter)();
378
382
  }
379
- return `${bodySections}\n\nThis PR was generated by Versionary.`;
383
+ return `${bodySections}\n\n${(0, changelog_js_1.renderReviewRequestFooter)()}`;
380
384
  }
381
385
  return (0, changelog_js_1.renderSimpleReleaseNotes)({
382
386
  currentVersion: previousVersion,
@@ -5,6 +5,7 @@ export interface ReleaseTargetInput {
5
5
  version: string;
6
6
  notes: string;
7
7
  draft?: boolean;
8
+ makeLatest?: "true" | "false" | "legacy";
8
9
  }
9
10
  export interface ReleaseExecutionContext {
10
11
  createReleaseMetadata: (input: ReleaseTargetInput) => Promise<VersionaryScmReleaseMetadataResult>;
@@ -183,6 +183,7 @@ async function runReleaseDetailed(cwd = process.cwd(), options = {}) {
183
183
  version: target.version,
184
184
  notes: releaseNotes,
185
185
  draft: loaded.config["release-draft"] ?? false,
186
+ makeLatest: target.path === "." ? "true" : "false",
186
187
  }, {
187
188
  createReleaseMetadata: (input) => scmClient.createReleaseMetadata(input, {
188
189
  cwd,
@@ -200,6 +200,7 @@ function createGitHubPlugin() {
200
200
  name: input.tag,
201
201
  body: input.notes,
202
202
  draft: input.draft ?? false,
203
+ make_latest: input.makeLatest,
203
204
  });
204
205
  data = response.data;
205
206
  }
@@ -25,6 +25,7 @@ export interface ScmReleaseMetadataInput {
25
25
  version: string;
26
26
  notes: string;
27
27
  draft?: boolean;
28
+ makeLatest?: "true" | "false" | "legacy";
28
29
  }
29
30
  export interface ScmReleaseMetadataResult {
30
31
  url: string;
@@ -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,
@@ -17,5 +19,10 @@ function listKnownReleaseTypes() {
17
19
  }
18
20
  function resolveVersionStrategy(config) {
19
21
  const releaseType = config["release-type"] ?? "simple";
20
- return strategyRegistry[releaseType] ?? simple_js_1.simpleVersionStrategy;
22
+ const strategy = strategyRegistry[releaseType];
23
+ if (!strategy) {
24
+ const known = listKnownReleaseTypes().join(", ");
25
+ throw new Error(`Unsupported release-type "${releaseType}". Supported release types: ${known}.`);
26
+ }
27
+ return strategy;
21
28
  }
@@ -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.17.0",
4
4
  "description": "Automatic release framework based on conventional commits and semantic versioning",
5
5
  "keywords": [
6
6
  "releasing",