genbumppush 0.0.1 → 0.0.3

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,881 @@
1
+ import { createDefineConfig, loadConfig } from "c12";
2
+ import { determineSemverChange, generateMarkDown, getGitDiff, loadChangelogConfig, parseCommits, resolveRepoConfig } from "changelogen";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { readFile, readdir, realpath, unlink, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { execFileSync, spawnSync } from "node:child_process";
8
+ //#region src/config.ts
9
+ const defineConfig = createDefineConfig();
10
+ const defaults = {
11
+ changelog: "CHANGELOG.md",
12
+ excludeDependencyCommits: true,
13
+ recursive: false,
14
+ git: {
15
+ remote: "origin",
16
+ push: true,
17
+ sign: false,
18
+ requireClean: true,
19
+ requireUpstream: true,
20
+ commitMessage: "chore(release): v{{version}}",
21
+ tagName: "v{{version}}",
22
+ tagMessage: "v{{version}}"
23
+ }
24
+ };
25
+ async function loadReleaseConfig(cwd, configFile, overrides) {
26
+ return (await loadConfig({
27
+ name: "genbumppush",
28
+ cwd,
29
+ configFile,
30
+ packageJson: "genbumppush",
31
+ defaults,
32
+ overrides,
33
+ rcFile: false,
34
+ globalRc: false
35
+ })).config;
36
+ }
37
+ //#endregion
38
+ //#region src/error.ts
39
+ var ReleaseError = class extends Error {
40
+ code;
41
+ constructor(code, message, options) {
42
+ super(message, options);
43
+ this.name = "ReleaseError";
44
+ this.code = code;
45
+ }
46
+ };
47
+ //#endregion
48
+ //#region src/git.ts
49
+ function git(args, cwd) {
50
+ try {
51
+ return execFileSync("git", args, {
52
+ cwd,
53
+ encoding: "utf8",
54
+ stdio: [
55
+ "ignore",
56
+ "pipe",
57
+ "pipe"
58
+ ]
59
+ }).trim();
60
+ } catch (error) {
61
+ throw new ReleaseError("GIT_COMMAND_FAILED", `git ${args.join(" ")} failed.`, { cause: error });
62
+ }
63
+ }
64
+ function tagExists(cwd, tag) {
65
+ return spawnSync("git", [
66
+ "rev-parse",
67
+ "--verify",
68
+ "--quiet",
69
+ `refs/tags/${tag}`
70
+ ], {
71
+ cwd,
72
+ stdio: "ignore"
73
+ }).status === 0;
74
+ }
75
+ function isGitRepository(cwd) {
76
+ const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
77
+ cwd,
78
+ encoding: "utf8",
79
+ stdio: [
80
+ "ignore",
81
+ "pipe",
82
+ "ignore"
83
+ ]
84
+ });
85
+ if (result.error || result.status !== 0 || typeof result.stdout !== "string") return false;
86
+ return result.stdout.trim() === "true";
87
+ }
88
+ function remoteTagExists(cwd, remote, tag) {
89
+ return git([
90
+ "ls-remote",
91
+ "--tags",
92
+ remote,
93
+ `refs/tags/${tag}`
94
+ ], cwd).length > 0;
95
+ }
96
+ function runHook(command, cwd) {
97
+ if (spawnSync(command, {
98
+ cwd,
99
+ shell: true,
100
+ stdio: "inherit"
101
+ }).status !== 0) throw new ReleaseError("HOOK_FAILED", `Hook failed: ${command}`);
102
+ }
103
+ //#endregion
104
+ //#region src/github.ts
105
+ function normalizeHost(host) {
106
+ return host.replace(/^https?:\/\//, "").replace(/\/$/, "");
107
+ }
108
+ function apiBase(host) {
109
+ const normalized = normalizeHost(host);
110
+ if (normalized === "github.com" || normalized === "api.github.com") return "https://api.github.com";
111
+ return `https://${normalized}/api/v3`;
112
+ }
113
+ function encodeRepoPath(repo) {
114
+ return repo.split("/").map((segment) => encodeURIComponent(segment)).join("/");
115
+ }
116
+ function resolveGitHubToken(tokenEnv) {
117
+ if (tokenEnv !== void 0) {
118
+ const token = process.env[tokenEnv];
119
+ return token !== void 0 && token.length > 0 ? token : void 0;
120
+ }
121
+ for (const name of [
122
+ "GITHUB_TOKEN",
123
+ "GH_TOKEN",
124
+ "CHANGELOGEN_TOKENS_GITHUB"
125
+ ]) {
126
+ const token = process.env[name];
127
+ if (token !== void 0 && token.length > 0) return token;
128
+ }
129
+ }
130
+ async function resolveGitHubRepo(cwd, source) {
131
+ const host = normalizeHost(source.host ?? process.env.GITHUB_API_URL ?? "github.com");
132
+ const explicit = source.repo ?? process.env.GITHUB_REPOSITORY;
133
+ if (explicit !== void 0 && explicit.length > 0) return {
134
+ host,
135
+ repo: explicit
136
+ };
137
+ const resolved = await resolveRepoConfig(cwd);
138
+ if (resolved?.provider === "github" && resolved.repo) return {
139
+ host: normalizeHost(resolved.domain ?? host),
140
+ repo: resolved.repo
141
+ };
142
+ throw new ReleaseError("GITHUB_RELEASE_FAILED", "Set github.repo or GITHUB_REPOSITORY to create a GitHub release.");
143
+ }
144
+ async function githubFetch(options, path, init) {
145
+ const url = `${apiBase(options.host)}/repos/${encodeRepoPath(options.repo)}${path}`;
146
+ const headers = new Headers({
147
+ Accept: "application/vnd.github+json",
148
+ "X-GitHub-Api-Version": "2022-11-28",
149
+ Authorization: `Bearer ${options.token}`,
150
+ "Content-Type": "application/json"
151
+ });
152
+ if (init?.headers) new Headers(init.headers).forEach((value, key) => {
153
+ headers.set(key, value);
154
+ });
155
+ let response;
156
+ try {
157
+ response = await fetch(url, {
158
+ method: init?.method,
159
+ body: init?.body,
160
+ headers,
161
+ signal: init?.signal ?? AbortSignal.timeout(3e4)
162
+ });
163
+ } catch (error) {
164
+ throw new ReleaseError("GITHUB_RELEASE_FAILED", "Could not send the GitHub release request.", { cause: error });
165
+ }
166
+ return response;
167
+ }
168
+ function parseReleaseId(body) {
169
+ if (typeof body !== "object" || body === null || !("id" in body)) return void 0;
170
+ const id = body.id;
171
+ return typeof id === "number" ? id : void 0;
172
+ }
173
+ /**
174
+ * Create or update a GitHub (or GHES) release for an existing tag.
175
+ * Uses the exact tag string so custom genbumppush tag templates stay valid.
176
+ */
177
+ async function createGitHubRelease(options) {
178
+ const existing = await githubFetch(options, `/releases/tags/${encodeURIComponent(options.tag)}`, { method: "GET" });
179
+ let existingId;
180
+ if (existing.ok) existingId = parseReleaseId(await existing.json().catch(() => ({})));
181
+ else if (existing.status !== 404) {
182
+ const text = await existing.text().catch(() => "");
183
+ throw new ReleaseError("GITHUB_RELEASE_FAILED", `GitHub release lookup failed with ${existing.status}: ${text || existing.statusText}`);
184
+ }
185
+ const payload = JSON.stringify({
186
+ tag_name: options.tag,
187
+ name: options.name,
188
+ body: options.description
189
+ });
190
+ const response = existingId === void 0 ? await githubFetch(options, "/releases", {
191
+ method: "POST",
192
+ body: payload
193
+ }) : await githubFetch(options, `/releases/${existingId}`, {
194
+ method: "PATCH",
195
+ body: payload
196
+ });
197
+ if (!response.ok) {
198
+ const text = await response.text().catch(() => "");
199
+ throw new ReleaseError("GITHUB_RELEASE_FAILED", `GitHub release creation failed with ${response.status}: ${text || response.statusText}`);
200
+ }
201
+ }
202
+ //#endregion
203
+ //#region src/gitlab.ts
204
+ function releaseNotes(changelog, tag) {
205
+ const lines = changelog.split("\n");
206
+ const heading = `## ${tag}`;
207
+ const start = lines.findIndex((line) => line === heading || line.startsWith(`${heading} `));
208
+ if (start < 0) return "See CHANGELOG.md for release notes.";
209
+ const notes = [];
210
+ for (const line of lines.slice(start + 1)) {
211
+ if (line.startsWith("## ")) break;
212
+ notes.push(line);
213
+ }
214
+ return notes.join("\n").trim() || "See CHANGELOG.md for release notes.";
215
+ }
216
+ const PROVIDER_FETCH_TIMEOUT_MS = 3e4;
217
+ async function createGitLabRelease(options) {
218
+ const url = `${options.host.replace(/\/$/, "")}/api/v4/projects/${encodeURIComponent(options.project)}/releases`;
219
+ let response;
220
+ try {
221
+ response = await fetch(url, {
222
+ method: "POST",
223
+ signal: AbortSignal.timeout(PROVIDER_FETCH_TIMEOUT_MS),
224
+ headers: {
225
+ Authorization: `Bearer ${options.token}`,
226
+ "Content-Type": "application/json"
227
+ },
228
+ body: JSON.stringify({
229
+ tag_name: options.tag,
230
+ name: options.name,
231
+ description: options.description
232
+ })
233
+ });
234
+ } catch (error) {
235
+ throw new ReleaseError("GITLAB_RELEASE_FAILED", "Could not send the GitLab release request.", { cause: error });
236
+ }
237
+ if (!response.ok) {
238
+ const body = await response.text().catch(() => "");
239
+ throw new ReleaseError("GITLAB_RELEASE_FAILED", `GitLab release creation failed with ${response.status}: ${body || response.statusText}`);
240
+ }
241
+ }
242
+ //#endregion
243
+ //#region src/version-files.ts
244
+ const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
245
+ ".git",
246
+ "node_modules",
247
+ "dist",
248
+ "target",
249
+ ".output"
250
+ ]);
251
+ function isObject$1(value) {
252
+ return value !== null && Object(value) === value && !Array.isArray(value);
253
+ }
254
+ function isString$1(value) {
255
+ return Object.prototype.toString.call(value) === "[object String]";
256
+ }
257
+ function isPackageManifest(value) {
258
+ return isObject$1(value) && (!("version" in value) || value.version === void 0 || isString$1(value.version));
259
+ }
260
+ function isPackageMap(value) {
261
+ if (!isObject$1(value)) return false;
262
+ const root = "" in value ? value[""] : void 0;
263
+ return root === void 0 || isPackageManifest(root);
264
+ }
265
+ function isPackageLock(value) {
266
+ return isPackageManifest(value) && (!("packages" in value) || value.packages === void 0 || isPackageMap(value.packages));
267
+ }
268
+ function isInside(root, path) {
269
+ const fromRoot = relative(root, path);
270
+ return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
271
+ }
272
+ async function resolveRepositoryPath(cwd, configuredPath) {
273
+ const root = resolve(cwd);
274
+ const canonicalRoot = await realpath(root);
275
+ const path = resolve(root, configuredPath);
276
+ if (!isInside(root, path)) throw new ReleaseError("PATH_OUTSIDE_REPOSITORY", `${path} is outside the repository.`);
277
+ if (!isInside(canonicalRoot, existsSync(path) ? await realpath(path) : await realpath(dirname(path)))) throw new ReleaseError("PATH_OUTSIDE_REPOSITORY", `${path} resolves outside the repository.`);
278
+ return path;
279
+ }
280
+ async function findManifests(directory) {
281
+ const entries = await readdir(directory, { withFileTypes: true });
282
+ return (await Promise.all(entries.map(async (entry) => {
283
+ if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) return findManifests(join(directory, entry.name));
284
+ return entry.name === "package.json" ? [join(directory, entry.name)] : [];
285
+ }))).flat();
286
+ }
287
+ function assertCurrent(path, actual, expected) {
288
+ if (actual !== expected) throw new ReleaseError("VERSION_MISMATCH", `${path} has version ${actual}; expected ${expected}.`);
289
+ }
290
+ function parseJsonString(raw) {
291
+ try {
292
+ const parsed = JSON.parse(raw);
293
+ return isString$1(parsed) ? parsed : void 0;
294
+ } catch {
295
+ return;
296
+ }
297
+ }
298
+ function readJsonString(content, start) {
299
+ let index = start + 1;
300
+ let escape = false;
301
+ while (index < content.length) {
302
+ const char = content[index];
303
+ if (char === void 0) break;
304
+ if (escape) escape = false;
305
+ else if (char === "\\") escape = true;
306
+ else if (char === "\"") {
307
+ index += 1;
308
+ return {
309
+ end: index,
310
+ raw: content.slice(start, index)
311
+ };
312
+ }
313
+ index += 1;
314
+ }
315
+ return {
316
+ end: content.length,
317
+ raw: content.slice(start)
318
+ };
319
+ }
320
+ function objectKeyPath(stack, propertyKey) {
321
+ const path = [];
322
+ for (const frame of stack) if (frame.kind === "object" && frame.introKey !== void 0) path.push(frame.introKey);
323
+ path.push(propertyKey);
324
+ return path;
325
+ }
326
+ /**
327
+ * Locate a JSON string property by object-key path without reformatting the file.
328
+ * Nested keys with the same name are never selected.
329
+ */
330
+ function findJsonStringProperty(content, keyPath) {
331
+ if (keyPath.length === 0) return void 0;
332
+ const stack = [];
333
+ let index = 0;
334
+ let pendingKey;
335
+ let afterColon = false;
336
+ const skipWhitespace = () => {
337
+ while (index < content.length && /\s/.test(content[index] ?? "")) index += 1;
338
+ };
339
+ while (index < content.length) {
340
+ skipWhitespace();
341
+ const char = content[index];
342
+ if (char === void 0) break;
343
+ if (char === "{") {
344
+ stack.push({
345
+ kind: "object",
346
+ introKey: pendingKey
347
+ });
348
+ pendingKey = void 0;
349
+ afterColon = false;
350
+ index += 1;
351
+ continue;
352
+ }
353
+ if (char === "}") {
354
+ stack.pop();
355
+ pendingKey = void 0;
356
+ afterColon = false;
357
+ index += 1;
358
+ continue;
359
+ }
360
+ if (char === "[") {
361
+ stack.push({ kind: "array" });
362
+ pendingKey = void 0;
363
+ afterColon = false;
364
+ index += 1;
365
+ continue;
366
+ }
367
+ if (char === "]") {
368
+ stack.pop();
369
+ pendingKey = void 0;
370
+ afterColon = false;
371
+ index += 1;
372
+ continue;
373
+ }
374
+ if (char === ",") {
375
+ pendingKey = void 0;
376
+ afterColon = false;
377
+ index += 1;
378
+ continue;
379
+ }
380
+ if (char === ":") {
381
+ afterColon = true;
382
+ index += 1;
383
+ continue;
384
+ }
385
+ if (char === "\"") {
386
+ const { end, raw } = readJsonString(content, index);
387
+ index = end;
388
+ const decoded = parseJsonString(raw);
389
+ if (decoded === void 0) continue;
390
+ if (stack.at(-1)?.kind === "object" && !afterColon) {
391
+ pendingKey = decoded;
392
+ continue;
393
+ }
394
+ if (afterColon && pendingKey !== void 0) {
395
+ const path = objectKeyPath(stack, pendingKey);
396
+ if (path.length === keyPath.length && path.every((segment, i) => segment === keyPath[i])) return {
397
+ value: decoded,
398
+ valueStart: end - raw.length + 1,
399
+ valueEnd: end - 1
400
+ };
401
+ pendingKey = void 0;
402
+ afterColon = false;
403
+ }
404
+ continue;
405
+ }
406
+ if (afterColon) {
407
+ pendingKey = void 0;
408
+ afterColon = false;
409
+ }
410
+ index += 1;
411
+ }
412
+ }
413
+ function replaceJsonStringProperty(content, span, value) {
414
+ return `${content.slice(0, span.valueStart)}${JSON.stringify(value).slice(1, -1)}${content.slice(span.valueEnd)}`;
415
+ }
416
+ function replaceJsonVersion(content, path, current, version) {
417
+ const parsed = JSON.parse(content);
418
+ if (!isObject$1(parsed) || !("version" in parsed) || !isString$1(parsed.version)) throw new ReleaseError("MISSING_VERSION", `${path} has no top-level version string.`);
419
+ assertCurrent(path, parsed.version, current);
420
+ const span = findJsonStringProperty(content, ["version"]);
421
+ if (span === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no top-level version string.`);
422
+ const updated = replaceJsonStringProperty(content, span, version);
423
+ if (updated === content) throw new ReleaseError("VERSION_UNCHANGED", `${path} was not updated.`);
424
+ return updated;
425
+ }
426
+ function replacePackageLockVersion(content, path, current, version) {
427
+ const parsed = JSON.parse(content);
428
+ if (!isPackageLock(parsed)) throw new ReleaseError("INVALID_LOCKFILE", `${path} is invalid.`);
429
+ const data = parsed;
430
+ if (!isString$1(data.version)) throw new ReleaseError("MISSING_VERSION", `${path} has no root version.`);
431
+ assertCurrent(path, data.version, current);
432
+ const rootVersion = data.packages?.[""]?.version;
433
+ if (rootVersion !== void 0 && !isString$1(rootVersion)) throw new ReleaseError("INVALID_LOCKFILE", `${path} has an invalid root package version.`);
434
+ if (isString$1(rootVersion)) assertCurrent(path, rootVersion, current);
435
+ const rootSpan = findJsonStringProperty(content, ["version"]);
436
+ if (rootSpan === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no root version.`);
437
+ let updated = replaceJsonStringProperty(content, rootSpan, version);
438
+ if (isString$1(rootVersion)) {
439
+ const packagesSpan = findJsonStringProperty(updated, [
440
+ "packages",
441
+ "",
442
+ "version"
443
+ ]);
444
+ if (packagesSpan === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no packages[""].version string to update.`);
445
+ updated = replaceJsonStringProperty(updated, packagesSpan, version);
446
+ }
447
+ if (updated === content) throw new ReleaseError("VERSION_UNCHANGED", `${path} was not updated.`);
448
+ return updated;
449
+ }
450
+ function cargoSection(content) {
451
+ const start = content.search(/^\[package\]\s*$/m);
452
+ if (start < 0) throw new ReleaseError("MISSING_CARGO_PACKAGE", "Cargo.toml has no [package] section.");
453
+ const next = content.slice(start + 1).search(/^\[[^[]/m);
454
+ return {
455
+ start,
456
+ end: next < 0 ? content.length : start + 1 + next
457
+ };
458
+ }
459
+ function cargoName(content) {
460
+ const section = cargoSection(content);
461
+ const match = /^name\s*=\s*"([^"]+)"/m.exec(content.slice(section.start, section.end));
462
+ if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_NAME", "Cargo.toml has no package name.");
463
+ return match[1];
464
+ }
465
+ function replaceCargoVersion(content, path, current, version) {
466
+ const section = cargoSection(content);
467
+ const body = content.slice(section.start, section.end);
468
+ const match = /^version\s*=\s*"([^"]*)"/m.exec(body);
469
+ if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_VERSION", "Cargo.toml has no package version.");
470
+ assertCurrent(path, match[1], current);
471
+ const updated = body.replace(/(^version\s*=\s*")[^"]*(")/m, `$1${version}$2`);
472
+ if (updated === body) throw new ReleaseError("MISSING_CARGO_VERSION", "Cargo.toml has no package version.");
473
+ return `${content.slice(0, section.start)}${updated}${content.slice(section.end)}`;
474
+ }
475
+ function replaceCargoLockVersion(content, path, name, current, version) {
476
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
477
+ const blocks = content.split(/(?=^\[\[package\]\]\s*$)/m);
478
+ let matches = 0;
479
+ const updated = blocks.map((block) => {
480
+ if (!new RegExp(`^name\\s*=\\s*"${escaped}"\\s*$`, "m").test(block)) return block;
481
+ matches += 1;
482
+ const match = /^version\s*=\s*"([^"]*)"\s*$/m.exec(block);
483
+ if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_LOCK_VERSION", `${path} package ${name} has no version.`);
484
+ assertCurrent(path, match[1], current);
485
+ return block.replace(/(^version\s*=\s*")[^"]*(")/m, `$1${version}$2`);
486
+ }).join("");
487
+ if (matches === 0) throw new ReleaseError("MISSING_CARGO_LOCK_PACKAGE", `Cargo.lock has no package named ${name}.`);
488
+ if (matches > 1) throw new ReleaseError("AMBIGUOUS_CARGO_LOCK_PACKAGE", `${path} contains package ${name} more than once.`);
489
+ return updated;
490
+ }
491
+ async function transform(path, current, version) {
492
+ const content = await readFile(path, "utf8");
493
+ const name = basename(path);
494
+ if (name === "package-lock.json") return replacePackageLockVersion(content, path, current, version);
495
+ if (name === "package.json" || name === "tauri.conf.json" || name.endsWith(".json")) return replaceJsonVersion(content, path, current, version);
496
+ if (name === "Cargo.toml") return replaceCargoVersion(content, path, current, version);
497
+ if (name === "Cargo.lock") {
498
+ const manifestPath = join(dirname(path), "Cargo.toml");
499
+ if (!existsSync(manifestPath)) throw new ReleaseError("MISSING_CARGO_MANIFEST", `${relative(process.cwd(), manifestPath)} is required.`);
500
+ return replaceCargoLockVersion(content, path, cargoName(await readFile(manifestPath, "utf8")), current, version);
501
+ }
502
+ const occurrences = content.split(current).length - 1;
503
+ if (occurrences !== 1) throw new ReleaseError("AMBIGUOUS_VERSION", `${path} must contain the current version exactly once; found ${occurrences}.`);
504
+ return content.replace(current, version);
505
+ }
506
+ async function planVersionChanges(cwd, current, version, config) {
507
+ const configured = await Promise.all((config.files ?? ["package.json"]).map((path) => resolveRepositoryPath(cwd, path)));
508
+ const paths = config.recursive === true ? [...configured, ...await findManifests(cwd)] : configured;
509
+ const unique = [...new Set(paths)];
510
+ return Promise.all(unique.map(async (path) => {
511
+ if (!existsSync(path)) throw new ReleaseError("MISSING_VERSION_FILE", `${relative(cwd, path)} does not exist.`);
512
+ return {
513
+ path,
514
+ before: await readFile(path, "utf8"),
515
+ after: await transform(path, current, version)
516
+ };
517
+ }));
518
+ }
519
+ async function applyVersionChanges(changes) {
520
+ try {
521
+ await Promise.all(changes.map((change) => writeFile(change.path, change.after)));
522
+ } catch (error) {
523
+ await restoreVersionChanges(changes);
524
+ throw error;
525
+ }
526
+ }
527
+ async function restoreVersionChanges(changes) {
528
+ await Promise.all(changes.map((change) => writeFile(change.path, change.before)));
529
+ }
530
+ //#endregion
531
+ //#region src/version.ts
532
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
533
+ function parseVersion(value) {
534
+ const match = VERSION_PATTERN.exec(value);
535
+ if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) throw new ReleaseError("INVALID_VERSION", `Invalid semantic version: ${value}`);
536
+ const identifiers = match[4]?.split(".");
537
+ if (identifiers?.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) throw new ReleaseError("INVALID_VERSION", `Invalid semantic version: ${value}`);
538
+ const version = {
539
+ major: Number(match[1]),
540
+ minor: Number(match[2]),
541
+ patch: Number(match[3])
542
+ };
543
+ if (identifiers !== void 0) version.prerelease = identifiers;
544
+ return version;
545
+ }
546
+ function format(version) {
547
+ const base = `${version.major}.${version.minor}.${version.patch}`;
548
+ return version.prerelease === void 0 ? base : `${base}-${version.prerelease.join(".")}`;
549
+ }
550
+ function applyPrerelease(version, preid) {
551
+ if (version.prerelease?.[0] === preid) {
552
+ const identifiers = [...version.prerelease];
553
+ const last = identifiers.at(-1);
554
+ if (last !== void 0 && /^\d+$/.test(last)) identifiers[identifiers.length - 1] = String(Number(last) + 1);
555
+ else identifiers.push("0");
556
+ return {
557
+ ...version,
558
+ prerelease: identifiers
559
+ };
560
+ }
561
+ return {
562
+ ...version,
563
+ prerelease: [preid, "0"]
564
+ };
565
+ }
566
+ function bumpVersion(current, release, preid = "beta") {
567
+ const version = parseVersion(current);
568
+ if (!/^[0-9A-Za-z-]+$/.test(preid)) throw new ReleaseError("INVALID_PREID", `Invalid prerelease identifier: ${preid}`);
569
+ const stable = version.prerelease === void 0 ? version : {
570
+ ...version,
571
+ prerelease: void 0
572
+ };
573
+ switch (release) {
574
+ case "major": return format(version.prerelease !== void 0 && version.minor === 0 && version.patch === 0 ? stable : {
575
+ major: version.major + 1,
576
+ minor: 0,
577
+ patch: 0
578
+ });
579
+ case "minor": return format(version.prerelease !== void 0 && version.patch === 0 ? stable : {
580
+ major: version.major,
581
+ minor: version.minor + 1,
582
+ patch: 0
583
+ });
584
+ case "patch": return format(version.prerelease === void 0 ? {
585
+ major: version.major,
586
+ minor: version.minor,
587
+ patch: version.patch + 1
588
+ } : stable);
589
+ case "premajor": return format(applyPrerelease({
590
+ major: version.major + 1,
591
+ minor: 0,
592
+ patch: 0
593
+ }, preid));
594
+ case "preminor": return format(applyPrerelease({
595
+ major: version.major,
596
+ minor: version.minor + 1,
597
+ patch: 0
598
+ }, preid));
599
+ case "prepatch": return format(applyPrerelease({
600
+ major: version.major,
601
+ minor: version.minor,
602
+ patch: version.patch + 1
603
+ }, preid));
604
+ case "prerelease": return format(applyPrerelease(version.prerelease === void 0 ? {
605
+ ...version,
606
+ patch: version.patch + 1
607
+ } : version, preid));
608
+ }
609
+ throw new ReleaseError("INVALID_RELEASE_TYPE", "Unsupported release type.");
610
+ }
611
+ //#endregion
612
+ //#region src/release.ts
613
+ const render = (value, version) => value.replaceAll("{{version}}", version);
614
+ const list = (value) => value === void 0 ? [] : Array.isArray(value) ? value : [value];
615
+ function isObject(value) {
616
+ return value !== null && Object(value) === value && !Array.isArray(value);
617
+ }
618
+ function isString(value) {
619
+ return Object.prototype.toString.call(value) === "[object String]";
620
+ }
621
+ function gitLabContext(config) {
622
+ if (config?.enabled !== true) throw new ReleaseError("GITLAB_RELEASE_FAILED", "Enable gitlab before creating a release.");
623
+ const tokenEnv = config.tokenEnv ?? "GITLAB_TOKEN";
624
+ const token = process.env[tokenEnv];
625
+ const project = config.project ?? process.env.GITLAB_PROJECT;
626
+ if (token === void 0 || token.length === 0) throw new ReleaseError("GITLAB_RELEASE_FAILED", `Set ${tokenEnv} to create a GitLab release.`);
627
+ if (project === void 0 || project.length === 0) throw new ReleaseError("GITLAB_RELEASE_FAILED", "Set gitlab.project or GITLAB_PROJECT to create a GitLab release.");
628
+ const context = {
629
+ host: config.host ?? process.env.GITLAB_HOST ?? "https://gitlab.com",
630
+ project,
631
+ token
632
+ };
633
+ if (config.releaseName !== void 0) context.releaseName = config.releaseName;
634
+ return context;
635
+ }
636
+ function resolveGitHubContext(config, cwd) {
637
+ if (config?.enabled !== true) throw new ReleaseError("GITHUB_RELEASE_FAILED", "Enable github before creating a release.");
638
+ const token = resolveGitHubToken(config.tokenEnv);
639
+ if (token === void 0) throw new ReleaseError("GITHUB_RELEASE_FAILED", `Set ${config.tokenEnv ?? "GITHUB_TOKEN"} to create a GitHub release.`);
640
+ return resolveGitHubRepo(cwd, {
641
+ host: config.host,
642
+ repo: config.repo
643
+ }).then(({ host, repo }) => {
644
+ const context = {
645
+ host,
646
+ repo,
647
+ token
648
+ };
649
+ if (config.releaseName !== void 0) context.releaseName = config.releaseName;
650
+ return context;
651
+ });
652
+ }
653
+ async function changelogText(cwd, config) {
654
+ if (config.changelog === false) return "";
655
+ const path = await resolveRepositoryPath(cwd, config.changelog === true ? "CHANGELOG.md" : config.changelog ?? "CHANGELOG.md");
656
+ return existsSync(path) ? readFile(path, "utf8") : "";
657
+ }
658
+ async function publishGitLab(context, cwd, config, tag, version) {
659
+ await createGitLabRelease({
660
+ host: context.host,
661
+ project: context.project,
662
+ token: context.token,
663
+ tag,
664
+ name: context.releaseName?.replaceAll("{{version}}", version) ?? tag,
665
+ description: releaseNotes(await changelogText(cwd, config), tag)
666
+ });
667
+ }
668
+ async function publishGitHub(context, cwd, config, tag, version) {
669
+ await createGitHubRelease({
670
+ host: context.host,
671
+ repo: context.repo,
672
+ token: context.token,
673
+ tag,
674
+ name: context.releaseName?.replaceAll("{{version}}", version) ?? tag,
675
+ description: releaseNotes(await changelogText(cwd, config), tag)
676
+ });
677
+ }
678
+ function filter(commits, config, exclude) {
679
+ return commits.filter((commit) => {
680
+ const type = config.types[commit.type.toLowerCase()];
681
+ return type !== void 0 && type !== false && !(exclude && commit.type === "chore" && commit.scope === "deps" && !commit.isBreaking);
682
+ });
683
+ }
684
+ async function collect(cwd, config) {
685
+ const changelog = await loadChangelogConfig(cwd);
686
+ const commits = parseCommits(await getGitDiff(changelog.from, changelog.to, cwd), changelog);
687
+ for (const commit of commits) commit.type = commit.type.toLowerCase();
688
+ return {
689
+ changelog,
690
+ commits: filter(commits, changelog, config.excludeDependencyCommits !== false)
691
+ };
692
+ }
693
+ async function confirm(message) {
694
+ const input = createInterface({
695
+ input: process.stdin,
696
+ output: process.stdout
697
+ });
698
+ try {
699
+ return /^y(es)?$/i.test((await input.question(`${message} (y/N) `)).trim());
700
+ } finally {
701
+ input.close();
702
+ }
703
+ }
704
+ async function prepend(path, markdown) {
705
+ const current = existsSync(path) ? await readFile(path, "utf8") : "# Changelog\n";
706
+ const match = current.match(/^# .+$/m);
707
+ const offset = match?.index === void 0 ? 0 : match.index + match[0].length;
708
+ const prefix = current.slice(0, offset).trimEnd();
709
+ const suffix = current.slice(offset).trim();
710
+ await writeFile(path, `${prefix}${prefix ? "\n\n" : ""}${markdown.trim()}${suffix ? `\n\n${suffix}` : ""}\n`);
711
+ }
712
+ function packageVersion(cwd) {
713
+ const data = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
714
+ if (!isObject(data) || !("version" in data) || !isString(data.version)) throw new ReleaseError("INVALID_PACKAGE", "package.json must contain a version string.");
715
+ return data.version;
716
+ }
717
+ async function runRelease(options) {
718
+ const overrides = {};
719
+ if (options.release !== void 0) overrides.release = options.release;
720
+ if (options.preid !== void 0) overrides.preid = options.preid;
721
+ if (options.push !== void 0) overrides.git = { push: options.push };
722
+ const config = await loadReleaseConfig(options.cwd, options.configFile, overrides);
723
+ const cwd = options.cwd;
724
+ if (!isGitRepository(cwd)) throw new ReleaseError("NOT_A_REPOSITORY", `${cwd} is not a Git repository.`);
725
+ const currentVersion = packageVersion(cwd);
726
+ if (options.gitlabRetryTag !== void 0) {
727
+ const context = gitLabContext(config.gitlab);
728
+ const remote = config.git?.remote ?? "origin";
729
+ if (!remoteTagExists(cwd, remote, options.gitlabRetryTag)) throw new ReleaseError("GITLAB_RELEASE_FAILED", `Tag ${options.gitlabRetryTag} does not exist on ${remote}.`);
730
+ await publishGitLab(context, cwd, config, options.gitlabRetryTag, currentVersion);
731
+ return {
732
+ currentVersion,
733
+ tag: options.gitlabRetryTag,
734
+ pushed: true,
735
+ dryRun: false,
736
+ commitCount: 0,
737
+ gitlabReleaseCreated: true
738
+ };
739
+ }
740
+ if (options.githubRetryTag !== void 0) {
741
+ const context = await resolveGitHubContext(config.github, cwd);
742
+ const remote = config.git?.remote ?? "origin";
743
+ if (!remoteTagExists(cwd, remote, options.githubRetryTag)) throw new ReleaseError("GITHUB_RELEASE_FAILED", `Tag ${options.githubRetryTag} does not exist on ${remote}.`);
744
+ await publishGitHub(context, cwd, config, options.githubRetryTag, currentVersion);
745
+ return {
746
+ currentVersion,
747
+ tag: options.githubRetryTag,
748
+ pushed: true,
749
+ dryRun: false,
750
+ commitCount: 0,
751
+ githubReleaseCreated: true
752
+ };
753
+ }
754
+ if (config.git?.requireClean !== false && git(["status", "--porcelain"], cwd) !== "") throw new ReleaseError("DIRTY_WORKTREE", "Commit or stash all changes before releasing.");
755
+ const branch = git(["branch", "--show-current"], cwd);
756
+ if (branch === "") throw new ReleaseError("DETACHED_HEAD", "Releases require a checked-out branch.");
757
+ const push = config.git?.push !== false;
758
+ if (push && config.git?.requireUpstream !== false) git([
759
+ "rev-parse",
760
+ "--abbrev-ref",
761
+ "--symbolic-full-name",
762
+ "@{upstream}"
763
+ ], cwd);
764
+ const { changelog, commits } = await collect(cwd, config);
765
+ const detected = config.release ?? determineSemverChange(commits, changelog);
766
+ if (detected === null) return {
767
+ currentVersion,
768
+ pushed: false,
769
+ dryRun: options.dryRun,
770
+ commitCount: 0
771
+ };
772
+ const releaseType = detected;
773
+ console.info(`Release: v${currentVersion} → ${releaseType} (${commits.length} commits)`);
774
+ const plannedVersion = bumpVersion(currentVersion, releaseType, config.preid);
775
+ const plannedTag = render(config.git?.tagName ?? "v{{version}}", plannedVersion);
776
+ if (options.dryRun) {
777
+ console.info(`Dry run: v${currentVersion} → v${plannedVersion} (${plannedTag})`);
778
+ console.info(await generateMarkDown(commits, changelog));
779
+ return {
780
+ currentVersion,
781
+ newVersion: plannedVersion,
782
+ releaseType,
783
+ tag: plannedTag,
784
+ pushed: false,
785
+ dryRun: true,
786
+ commitCount: commits.length
787
+ };
788
+ }
789
+ let gitlab;
790
+ if (config.gitlab?.enabled === true) {
791
+ if (!push) throw new ReleaseError("GITLAB_RELEASE_FAILED", "GitLab release creation requires git.push to be enabled.");
792
+ gitlab = gitLabContext(config.gitlab);
793
+ }
794
+ let github;
795
+ if (config.github?.enabled === true) {
796
+ if (!push) throw new ReleaseError("GITHUB_RELEASE_FAILED", "GitHub release creation requires git.push to be enabled.");
797
+ github = await resolveGitHubContext(config.github, cwd);
798
+ }
799
+ if (!options.yes && !await confirm(`Create a ${releaseType} release?`)) throw new ReleaseError("CANCELLED", "Release cancelled.");
800
+ const version = plannedVersion;
801
+ const tag = plannedTag;
802
+ const remote = config.git?.remote ?? "origin";
803
+ if (tagExists(cwd, tag) || push && remoteTagExists(cwd, remote, tag)) throw new ReleaseError("TAG_EXISTS", `Tag ${tag} already exists.`);
804
+ for (const command of list(config.hooks?.before)) runHook(command, cwd);
805
+ const changes = await planVersionChanges(cwd, currentVersion, version, config);
806
+ const changed = changes.map((change) => change.path);
807
+ let changelogSnapshot;
808
+ if (config.changelog !== false) {
809
+ const path = await resolveRepositoryPath(cwd, config.changelog === true ? "CHANGELOG.md" : config.changelog ?? "CHANGELOG.md");
810
+ const existed = existsSync(path);
811
+ changelogSnapshot = {
812
+ path,
813
+ existed
814
+ };
815
+ if (existed) changelogSnapshot.content = await readFile(path, "utf8");
816
+ changed.push(path);
817
+ }
818
+ const indexTree = git(["write-tree"], cwd);
819
+ try {
820
+ await applyVersionChanges(changes);
821
+ if (changelogSnapshot !== void 0) await prepend(changelogSnapshot.path, await generateMarkDown(commits, {
822
+ ...changelog,
823
+ newVersion: version
824
+ }));
825
+ git([
826
+ "add",
827
+ "--",
828
+ ...[...new Set(changed)].map((path) => relative(cwd, path))
829
+ ], cwd);
830
+ const commitArgs = ["commit"];
831
+ if (config.git?.sign === true) commitArgs.push("-S");
832
+ commitArgs.push("-m", render(config.git?.commitMessage ?? "chore(release): v{{version}}", version));
833
+ git(commitArgs, cwd);
834
+ } catch (error) {
835
+ await restoreVersionChanges(changes);
836
+ if (changelogSnapshot?.content !== void 0) await writeFile(changelogSnapshot.path, changelogSnapshot.content);
837
+ else if (changelogSnapshot !== void 0 && existsSync(changelogSnapshot.path)) await unlink(changelogSnapshot.path);
838
+ git(["read-tree", indexTree], cwd);
839
+ throw error;
840
+ }
841
+ const tagArgs = ["tag", "-a"];
842
+ if (config.git?.sign === true) tagArgs.push("-s");
843
+ tagArgs.push(tag, "-m", render(config.git?.tagMessage ?? "v{{version}}", version));
844
+ git(tagArgs, cwd);
845
+ if (push) git([
846
+ "push",
847
+ "--atomic",
848
+ remote,
849
+ `HEAD:${branch}`,
850
+ `refs/tags/${tag}`
851
+ ], cwd);
852
+ let gitlabReleaseCreated = false;
853
+ if (gitlab !== void 0) try {
854
+ await publishGitLab(gitlab, cwd, config, tag, version);
855
+ gitlabReleaseCreated = true;
856
+ } catch (error) {
857
+ throw new ReleaseError("RELEASE_PUBLISHED_GITLAB_FAILED", `Git release ${tag} was pushed, but GitLab release creation failed. Retry with: genbumppush --retry-gitlab ${tag}`, { cause: error });
858
+ }
859
+ let githubReleaseCreated = false;
860
+ if (github !== void 0) try {
861
+ await publishGitHub(github, cwd, config, tag, version);
862
+ githubReleaseCreated = true;
863
+ } catch (error) {
864
+ throw new ReleaseError("RELEASE_PUBLISHED_GITHUB_FAILED", `Git release ${tag} was pushed, but GitHub release creation failed. Retry with: genbumppush --retry-github ${tag}`, { cause: error });
865
+ }
866
+ for (const command of list(config.hooks?.after)) runHook(command, cwd);
867
+ const result = {
868
+ currentVersion,
869
+ newVersion: version,
870
+ releaseType,
871
+ tag,
872
+ pushed: push,
873
+ dryRun: false,
874
+ commitCount: commits.length
875
+ };
876
+ if (gitlabReleaseCreated) result.gitlabReleaseCreated = true;
877
+ if (githubReleaseCreated) result.githubReleaseCreated = true;
878
+ return result;
879
+ }
880
+ //#endregion
881
+ export { loadReleaseConfig as i, ReleaseError as n, defineConfig as r, runRelease as t };