@releasekit/version 0.2.0-next.1 → 0.2.0-next.11

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.
@@ -1,6 +1,10 @@
1
1
  import {
2
2
  BaseVersionError
3
3
  } from "./chunk-GQLJ7JQY.js";
4
+ import {
5
+ execAsync,
6
+ execSync
7
+ } from "./chunk-LMPZV35Z.js";
4
8
 
5
9
  // src/config.ts
6
10
  import { loadVersionConfig } from "@releasekit/config";
@@ -202,24 +206,6 @@ import semver3 from "semver";
202
206
  // src/git/repository.ts
203
207
  import { existsSync, statSync } from "fs";
204
208
  import { join } from "path";
205
-
206
- // src/git/commandExecutor.ts
207
- import { execFile, execFileSync } from "child_process";
208
- var execAsync = (file, args, options) => {
209
- const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
210
- return new Promise((resolve2, reject) => {
211
- execFile(file, args, defaultOptions, (error, stdout, stderr) => {
212
- if (error) {
213
- reject(error);
214
- } else {
215
- resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
216
- }
217
- });
218
- });
219
- };
220
- var execSync = (file, args, options) => execFileSync(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
221
-
222
- // src/git/repository.ts
223
209
  function isGitRepository(directory) {
224
210
  const gitDir = join(directory, ".git");
225
211
  if (!existsSync(gitDir)) {
@@ -374,28 +360,37 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
374
360
  `Looking for tags for package ${packageName} with prefix ${versionPrefix || "none"}, packageSpecificTags: ${packageSpecificTags}`,
375
361
  "debug"
376
362
  );
377
- const allTags = await getSemverTags({
378
- tagPrefix: versionPrefix
379
- });
380
- log(`Retrieved ${allTags.length} tags: ${allTags.join(", ")}`, "debug");
363
+ let allTags = [];
364
+ try {
365
+ const { execSync: execSync2 } = await import("./commandExecutor-E44ID5U4.js");
366
+ const tagsOutput = execSync2("git", ["tag", "-l"], { cwd: process.cwd() });
367
+ allTags = tagsOutput.toString().trim().split("\n").filter((tag) => tag.length > 0);
368
+ } catch (err) {
369
+ log(`Error getting tags: ${err instanceof Error ? err.message : String(err)}`, "error");
370
+ }
371
+ log(`Retrieved ${allTags.length} tags`, "debug");
381
372
  if (packageSpecificTags) {
382
373
  const packageTagPattern = escapeRegExp(tagTemplate).replace(/\\\$\\\{packageName\\\}/g, `(?:${escapedPackageName})`).replace(/\\\$\\\{prefix\\\}/g, `(?:${escapedPrefix})`).replace(/\\\$\\\{version\\\}/g, "(?:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)");
383
374
  log(`Using package tag pattern: ${packageTagPattern}`, "debug");
384
375
  const packageTagRegex = new RegExp(`^${packageTagPattern}$`);
385
376
  let packageTags = allTags.filter((tag) => packageTagRegex.test(tag));
377
+ log(`Found ${packageTags.length} matching tags for ${packageName}`, "debug");
386
378
  if (packageTags.length > 0) {
387
379
  const chronologicalFirst = packageTags[0];
380
+ void chronologicalFirst;
388
381
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
389
382
  let versionA = "";
390
383
  let versionB = "";
391
384
  if (a.includes("@")) {
392
- const afterAt = a.split("@")[1] || "";
385
+ const parts = a.split("@");
386
+ const afterAt = parts[parts.length - 1] || "";
393
387
  versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
394
388
  } else {
395
389
  versionA = a.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
396
390
  }
397
391
  if (b.includes("@")) {
398
- const afterAtB = b.split("@")[1] || "";
392
+ const parts = b.split("@");
393
+ const afterAtB = parts[parts.length - 1] || "";
399
394
  versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
400
395
  } else {
401
396
  versionB = b.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
@@ -406,12 +401,6 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
406
401
  });
407
402
  log(`Found ${packageTags.length} package tags using configured pattern`, "debug");
408
403
  log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
409
- if (sortedPackageTags2[0] !== chronologicalFirst) {
410
- log(
411
- `Package tag ordering differs: chronological first is ${chronologicalFirst}, semantic latest is ${sortedPackageTags2[0]}`,
412
- "debug"
413
- );
414
- }
415
404
  return sortedPackageTags2[0];
416
405
  }
417
406
  if (versionPrefix) {
@@ -419,9 +408,11 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
419
408
  packageTags = allTags.filter((tag) => pattern1.test(tag));
420
409
  if (packageTags.length > 0) {
421
410
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
422
- const afterAt = a.split("@")[1] || "";
411
+ const aParts = a.split("@");
412
+ const afterAt = aParts[aParts.length - 1] || "";
423
413
  const versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
424
- const afterAtB = b.split("@")[1] || "";
414
+ const bParts = b.split("@");
415
+ const afterAtB = bParts[bParts.length - 1] || "";
425
416
  const versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
426
417
  const cleanVersionA = semver.clean(versionA) || "0.0.0";
427
418
  const cleanVersionB = semver.clean(versionB) || "0.0.0";
@@ -437,8 +428,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
437
428
  packageTags = allTags.filter((tag) => pattern2.test(tag));
438
429
  if (packageTags.length > 0) {
439
430
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
440
- const versionA = semver.clean(a.split("@")[1] || "") || "0.0.0";
441
- const versionB = semver.clean(b.split("@")[1] || "") || "0.0.0";
431
+ const aParts = a.split("@");
432
+ const versionA = semver.clean(aParts[aParts.length - 1] || "") || "0.0.0";
433
+ const bParts = b.split("@");
434
+ const versionB = semver.clean(bParts[bParts.length - 1] || "") || "0.0.0";
442
435
  return semver.rcompare(versionA, versionB);
443
436
  });
444
437
  log(`Found ${packageTags.length} package tags using pattern: ${versionPrefix}packageName@...`, "debug");
@@ -458,8 +451,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
458
451
  return "";
459
452
  }
460
453
  const sortedPackageTags = [...packageTags].sort((a, b) => {
461
- const versionA = semver.clean(a.split("@")[1] || "") || "0.0.0";
462
- const versionB = semver.clean(b.split("@")[1] || "") || "0.0.0";
454
+ const aParts = a.split("@");
455
+ const versionA = semver.clean(aParts[aParts.length - 1] || "") || "0.0.0";
456
+ const bParts = b.split("@");
457
+ const versionB = semver.clean(bParts[bParts.length - 1] || "") || "0.0.0";
463
458
  return semver.rcompare(versionA, versionB);
464
459
  });
465
460
  log(`Found ${packageTags.length} package tags for ${packageName}`, "debug");
@@ -698,12 +693,17 @@ var VersionMismatchError = class extends Error {
698
693
  this.name = "VersionMismatchError";
699
694
  }
700
695
  };
701
- async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error") {
696
+ async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error", strictReachable = false) {
702
697
  if (!tagName?.trim()) {
703
698
  return packageVersion ? { source: "package", version: packageVersion, reason: "No git tag provided" } : { source: "initial", version: "0.1.0", reason: "No git tag or package version available" };
704
699
  }
705
700
  const verification = verifyTag(tagName, cwd4);
706
701
  if (!verification.exists || !verification.reachable) {
702
+ if (strictReachable) {
703
+ throw new Error(
704
+ `Git tag '${tagName}' is not reachable from the current commit. The tag exists but cannot be reached from HEAD, which usually means you're on a different branch or the tag is orphaned. To allow fallback to package version, set strictReachable to false in your configuration.`
705
+ );
706
+ }
707
707
  if (packageVersion) {
708
708
  log(
709
709
  `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
@@ -809,7 +809,8 @@ async function calculateVersion(config, options) {
809
809
  prereleaseIdentifier: configPrereleaseIdentifier,
810
810
  branchPattern,
811
811
  baseBranch,
812
- mismatchStrategy
812
+ mismatchStrategy,
813
+ strictReachable
813
814
  } = config;
814
815
  const {
815
816
  latestTag,
@@ -854,7 +855,13 @@ async function calculateVersion(config, options) {
854
855
  const packageDir = pkgPath || cwd();
855
856
  const manifestResult = getVersionFromManifests(packageDir);
856
857
  const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
857
- versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir, mismatchStrategy);
858
+ versionSource = await getBestVersionSource(
859
+ latestTag,
860
+ packageVersion,
861
+ packageDir,
862
+ mismatchStrategy,
863
+ strictReachable
864
+ );
858
865
  log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
859
866
  }
860
867
  const specifiedType = type;
@@ -962,12 +969,83 @@ import { exit } from "process";
962
969
  // src/changelog/commitParser.ts
963
970
  var CONVENTIONAL_COMMIT_REGEX = /^(\w+)(?:\(([^)]+)\))?(!)?: (.+)(?:\n\n([\s\S]*))?/;
964
971
  var BREAKING_CHANGE_REGEX = /BREAKING CHANGE: ([\s\S]+?)(?:\n\n|$)/;
965
- function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
972
+ function extractAllChangelogEntriesWithHash(projectDir, revisionRange) {
966
973
  try {
967
- const output = execSync("git", ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"], {
974
+ const args = ["log", revisionRange, "--pretty=format:%H|||%B---COMMIT_DELIMITER---", "--no-merges"];
975
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
976
+ const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
977
+ return commits.map((commit) => {
978
+ const [hash, ...messageParts] = commit.split("|||");
979
+ const message = messageParts.join("|||").trim();
980
+ const entry = parseCommitMessage(message);
981
+ if (entry && hash) {
982
+ return { hash: hash.trim(), entry };
983
+ }
984
+ return null;
985
+ }).filter((item) => item !== null);
986
+ } catch (error) {
987
+ const errorMessage = error instanceof Error ? error.message : String(error);
988
+ log(`Error extracting all commits with hash: ${errorMessage}`, "error");
989
+ return [];
990
+ }
991
+ }
992
+ function commitTouchesAnyPackage(projectDir, commitHash, packageDirs, sharedPackageDirs = []) {
993
+ try {
994
+ const output = execSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
968
995
  cwd: projectDir,
969
996
  encoding: "utf8"
970
- }).toString();
997
+ }).toString().trim();
998
+ if (!output) {
999
+ return false;
1000
+ }
1001
+ const changedFiles = output.split("\n");
1002
+ return changedFiles.some((file) => {
1003
+ return packageDirs.some((pkgDir) => {
1004
+ if (sharedPackageDirs.some((sharedDir) => pkgDir.includes(sharedDir))) {
1005
+ return false;
1006
+ }
1007
+ const normalizedFile = file.replace(/\\/g, "/");
1008
+ const normalizedPkgDir = pkgDir.replace(/\\/g, "/").replace(/^\.\//, "");
1009
+ return normalizedFile.startsWith(normalizedPkgDir);
1010
+ });
1011
+ });
1012
+ } catch (error) {
1013
+ log(
1014
+ `Error checking if commit ${commitHash} touches packages: ${error instanceof Error ? error.message : String(error)}`,
1015
+ "debug"
1016
+ );
1017
+ return false;
1018
+ }
1019
+ }
1020
+ function extractRepoLevelChangelogEntries(projectDir, revisionRange, packageDirs, sharedPackageDirs = []) {
1021
+ try {
1022
+ const allCommits = extractAllChangelogEntriesWithHash(projectDir, revisionRange);
1023
+ const repoLevelCommits = allCommits.filter((commit) => {
1024
+ const touchesPackage = commitTouchesAnyPackage(projectDir, commit.hash, packageDirs, sharedPackageDirs);
1025
+ return !touchesPackage;
1026
+ });
1027
+ if (repoLevelCommits.length > 0) {
1028
+ log(
1029
+ `Found ${repoLevelCommits.length} repo-level commit(s) (including shared packages: ${sharedPackageDirs.join(", ")})`,
1030
+ "debug"
1031
+ );
1032
+ }
1033
+ return repoLevelCommits.map((c) => c.entry);
1034
+ } catch (error) {
1035
+ log(`Error extracting repo-level commits: ${error instanceof Error ? error.message : String(error)}`, "warning");
1036
+ return [];
1037
+ }
1038
+ }
1039
+ function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
1040
+ return extractCommitsFromGitLog(projectDir, revisionRange, true);
1041
+ }
1042
+ function extractCommitsFromGitLog(projectDir, revisionRange, filterToPath) {
1043
+ try {
1044
+ const args = ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"];
1045
+ if (filterToPath) {
1046
+ args.push("--", ".");
1047
+ }
1048
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
971
1049
  const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
972
1050
  return commits.map((commit) => parseCommitMessage(commit)).filter((entry) => entry !== null);
973
1051
  } catch (error) {
@@ -1395,6 +1473,11 @@ var PackageProcessor = class {
1395
1473
  if (verification.exists && verification.reachable) {
1396
1474
  revisionRange = `${latestTag}..HEAD`;
1397
1475
  } else {
1476
+ if (this.config.strictReachable) {
1477
+ throw new Error(
1478
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1479
+ );
1480
+ }
1398
1481
  log(`Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`, "debug");
1399
1482
  revisionRange = "HEAD";
1400
1483
  }
@@ -1402,6 +1485,19 @@ var PackageProcessor = class {
1402
1485
  revisionRange = "HEAD";
1403
1486
  }
1404
1487
  changelogEntries = extractChangelogEntriesFromCommits(pkgPath, revisionRange);
1488
+ const allPackageDirs = packages.map((p) => p.dir);
1489
+ const sharedPackageNames = ["config", "core", "@releasekit/config", "@releasekit/core"];
1490
+ const sharedPackageDirs = packages.filter((p) => sharedPackageNames.includes(p.packageJson.name)).map((p) => p.dir);
1491
+ const repoLevelEntries = extractRepoLevelChangelogEntries(
1492
+ pkgPath,
1493
+ revisionRange,
1494
+ allPackageDirs,
1495
+ sharedPackageDirs
1496
+ );
1497
+ if (repoLevelEntries.length > 0) {
1498
+ log(`Adding ${repoLevelEntries.length} repo-level commit(s) to ${name} changelog`, "debug");
1499
+ changelogEntries = [...repoLevelEntries, ...changelogEntries];
1500
+ }
1405
1501
  if (changelogEntries.length === 0) {
1406
1502
  changelogEntries = [
1407
1503
  {
@@ -1712,6 +1808,11 @@ function createSyncStrategy(config) {
1712
1808
  });
1713
1809
  revisionRange = `${latestTag}..HEAD`;
1714
1810
  } catch {
1811
+ if (config.strictReachable) {
1812
+ throw new Error(
1813
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1814
+ );
1815
+ }
1715
1816
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1716
1817
  revisionRange = "HEAD";
1717
1818
  }
@@ -1831,6 +1932,11 @@ function createSingleStrategy(config) {
1831
1932
  });
1832
1933
  revisionRange = `${latestTag}..HEAD`;
1833
1934
  } catch {
1935
+ if (config.strictReachable) {
1936
+ throw new Error(
1937
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1938
+ );
1939
+ }
1834
1940
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1835
1941
  revisionRange = "HEAD";
1836
1942
  }
@@ -0,0 +1,20 @@
1
+ // src/git/commandExecutor.ts
2
+ import { execFile, execFileSync } from "child_process";
3
+ var execAsync = (file, args, options) => {
4
+ const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
5
+ return new Promise((resolve, reject) => {
6
+ execFile(file, args, defaultOptions, (error, stdout, stderr) => {
7
+ if (error) {
8
+ reject(error);
9
+ } else {
10
+ resolve({ stdout: stdout.toString(), stderr: stderr.toString() });
11
+ }
12
+ });
13
+ });
14
+ };
15
+ var execSync = (file, args, options) => execFileSync(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
16
+
17
+ export {
18
+ execAsync,
19
+ execSync
20
+ };
package/dist/cli.cjs CHANGED
@@ -57,6 +57,33 @@ var init_baseError = __esm({
57
57
  }
58
58
  });
59
59
 
60
+ // src/git/commandExecutor.ts
61
+ var commandExecutor_exports = {};
62
+ __export(commandExecutor_exports, {
63
+ execAsync: () => execAsync,
64
+ execSync: () => execSync
65
+ });
66
+ var import_node_child_process, execAsync, execSync;
67
+ var init_commandExecutor = __esm({
68
+ "src/git/commandExecutor.ts"() {
69
+ "use strict";
70
+ import_node_child_process = require("child_process");
71
+ execAsync = (file, args, options) => {
72
+ const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
73
+ return new Promise((resolve2, reject) => {
74
+ (0, import_node_child_process.execFile)(file, args, defaultOptions, (error, stdout, stderr) => {
75
+ if (error) {
76
+ reject(error);
77
+ } else {
78
+ resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
79
+ }
80
+ });
81
+ });
82
+ };
83
+ execSync = (file, args, options) => (0, import_node_child_process.execFileSync)(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
84
+ }
85
+ });
86
+
60
87
  // src/cli.ts
61
88
  var cli_exports = {};
62
89
  __export(cli_exports, {
@@ -370,31 +397,87 @@ function matchesPackageNamePattern(packageName, pattern) {
370
397
  var import_node_fs6 = __toESM(require("fs"), 1);
371
398
  var path6 = __toESM(require("path"), 1);
372
399
 
373
- // src/git/commandExecutor.ts
374
- var import_node_child_process = require("child_process");
375
- var execAsync = (file, args, options) => {
376
- const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
377
- return new Promise((resolve2, reject) => {
378
- (0, import_node_child_process.execFile)(file, args, defaultOptions, (error, stdout, stderr) => {
379
- if (error) {
380
- reject(error);
381
- } else {
382
- resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
383
- }
384
- });
385
- });
386
- };
387
- var execSync = (file, args, options) => (0, import_node_child_process.execFileSync)(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
388
-
389
400
  // src/changelog/commitParser.ts
401
+ init_commandExecutor();
390
402
  var CONVENTIONAL_COMMIT_REGEX = /^(\w+)(?:\(([^)]+)\))?(!)?: (.+)(?:\n\n([\s\S]*))?/;
391
403
  var BREAKING_CHANGE_REGEX = /BREAKING CHANGE: ([\s\S]+?)(?:\n\n|$)/;
392
- function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
404
+ function extractAllChangelogEntriesWithHash(projectDir, revisionRange) {
393
405
  try {
394
- const output = execSync("git", ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"], {
406
+ const args = ["log", revisionRange, "--pretty=format:%H|||%B---COMMIT_DELIMITER---", "--no-merges"];
407
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
408
+ const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
409
+ return commits.map((commit) => {
410
+ const [hash, ...messageParts] = commit.split("|||");
411
+ const message = messageParts.join("|||").trim();
412
+ const entry = parseCommitMessage(message);
413
+ if (entry && hash) {
414
+ return { hash: hash.trim(), entry };
415
+ }
416
+ return null;
417
+ }).filter((item) => item !== null);
418
+ } catch (error) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+ log(`Error extracting all commits with hash: ${errorMessage}`, "error");
421
+ return [];
422
+ }
423
+ }
424
+ function commitTouchesAnyPackage(projectDir, commitHash, packageDirs, sharedPackageDirs = []) {
425
+ try {
426
+ const output = execSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
395
427
  cwd: projectDir,
396
428
  encoding: "utf8"
397
- }).toString();
429
+ }).toString().trim();
430
+ if (!output) {
431
+ return false;
432
+ }
433
+ const changedFiles = output.split("\n");
434
+ return changedFiles.some((file) => {
435
+ return packageDirs.some((pkgDir) => {
436
+ if (sharedPackageDirs.some((sharedDir) => pkgDir.includes(sharedDir))) {
437
+ return false;
438
+ }
439
+ const normalizedFile = file.replace(/\\/g, "/");
440
+ const normalizedPkgDir = pkgDir.replace(/\\/g, "/").replace(/^\.\//, "");
441
+ return normalizedFile.startsWith(normalizedPkgDir);
442
+ });
443
+ });
444
+ } catch (error) {
445
+ log(
446
+ `Error checking if commit ${commitHash} touches packages: ${error instanceof Error ? error.message : String(error)}`,
447
+ "debug"
448
+ );
449
+ return false;
450
+ }
451
+ }
452
+ function extractRepoLevelChangelogEntries(projectDir, revisionRange, packageDirs, sharedPackageDirs = []) {
453
+ try {
454
+ const allCommits = extractAllChangelogEntriesWithHash(projectDir, revisionRange);
455
+ const repoLevelCommits = allCommits.filter((commit) => {
456
+ const touchesPackage = commitTouchesAnyPackage(projectDir, commit.hash, packageDirs, sharedPackageDirs);
457
+ return !touchesPackage;
458
+ });
459
+ if (repoLevelCommits.length > 0) {
460
+ log(
461
+ `Found ${repoLevelCommits.length} repo-level commit(s) (including shared packages: ${sharedPackageDirs.join(", ")})`,
462
+ "debug"
463
+ );
464
+ }
465
+ return repoLevelCommits.map((c) => c.entry);
466
+ } catch (error) {
467
+ log(`Error extracting repo-level commits: ${error instanceof Error ? error.message : String(error)}`, "warning");
468
+ return [];
469
+ }
470
+ }
471
+ function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
472
+ return extractCommitsFromGitLog(projectDir, revisionRange, true);
473
+ }
474
+ function extractCommitsFromGitLog(projectDir, revisionRange, filterToPath) {
475
+ try {
476
+ const args = ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"];
477
+ if (filterToPath) {
478
+ args.push("--", ".");
479
+ }
480
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
398
481
  const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
399
482
  return commits.map((commit) => parseCommitMessage(commit)).filter((entry) => entry !== null);
400
483
  } catch (error) {
@@ -489,13 +572,16 @@ function extractIssueIds(body) {
489
572
 
490
573
  // src/core/versionStrategies.ts
491
574
  init_baseError();
575
+ init_commandExecutor();
492
576
 
493
577
  // src/git/commands.ts
494
578
  var import_node_process = require("process");
579
+ init_commandExecutor();
495
580
 
496
581
  // src/git/repository.ts
497
582
  var import_node_fs = require("fs");
498
583
  var import_node_path2 = require("path");
584
+ init_commandExecutor();
499
585
  function isGitRepository(directory) {
500
586
  const gitDir = (0, import_node_path2.join)(directory, ".git");
501
587
  if (!(0, import_node_fs.existsSync)(gitDir)) {
@@ -700,6 +786,7 @@ To fix this:
700
786
  }
701
787
 
702
788
  // src/git/tagsAndBranches.ts
789
+ init_commandExecutor();
703
790
  function getCommitsLength(pkgRoot, sinceTag) {
704
791
  try {
705
792
  let amount;
@@ -777,28 +864,37 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
777
864
  `Looking for tags for package ${packageName} with prefix ${versionPrefix || "none"}, packageSpecificTags: ${packageSpecificTags}`,
778
865
  "debug"
779
866
  );
780
- const allTags = await (0, import_git_semver_tags.getSemverTags)({
781
- tagPrefix: versionPrefix
782
- });
783
- log(`Retrieved ${allTags.length} tags: ${allTags.join(", ")}`, "debug");
867
+ let allTags = [];
868
+ try {
869
+ const { execSync: execSync2 } = await Promise.resolve().then(() => (init_commandExecutor(), commandExecutor_exports));
870
+ const tagsOutput = execSync2("git", ["tag", "-l"], { cwd: process.cwd() });
871
+ allTags = tagsOutput.toString().trim().split("\n").filter((tag) => tag.length > 0);
872
+ } catch (err) {
873
+ log(`Error getting tags: ${err instanceof Error ? err.message : String(err)}`, "error");
874
+ }
875
+ log(`Retrieved ${allTags.length} tags`, "debug");
784
876
  if (packageSpecificTags) {
785
877
  const packageTagPattern = escapeRegExp(tagTemplate).replace(/\\\$\\\{packageName\\\}/g, `(?:${escapedPackageName})`).replace(/\\\$\\\{prefix\\\}/g, `(?:${escapedPrefix})`).replace(/\\\$\\\{version\\\}/g, "(?:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)");
786
878
  log(`Using package tag pattern: ${packageTagPattern}`, "debug");
787
879
  const packageTagRegex = new RegExp(`^${packageTagPattern}$`);
788
880
  let packageTags = allTags.filter((tag) => packageTagRegex.test(tag));
881
+ log(`Found ${packageTags.length} matching tags for ${packageName}`, "debug");
789
882
  if (packageTags.length > 0) {
790
883
  const chronologicalFirst = packageTags[0];
884
+ void chronologicalFirst;
791
885
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
792
886
  let versionA = "";
793
887
  let versionB = "";
794
888
  if (a.includes("@")) {
795
- const afterAt = a.split("@")[1] || "";
889
+ const parts = a.split("@");
890
+ const afterAt = parts[parts.length - 1] || "";
796
891
  versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
797
892
  } else {
798
893
  versionA = a.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
799
894
  }
800
895
  if (b.includes("@")) {
801
- const afterAtB = b.split("@")[1] || "";
896
+ const parts = b.split("@");
897
+ const afterAtB = parts[parts.length - 1] || "";
802
898
  versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
803
899
  } else {
804
900
  versionB = b.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
@@ -809,12 +905,6 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
809
905
  });
810
906
  log(`Found ${packageTags.length} package tags using configured pattern`, "debug");
811
907
  log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
812
- if (sortedPackageTags2[0] !== chronologicalFirst) {
813
- log(
814
- `Package tag ordering differs: chronological first is ${chronologicalFirst}, semantic latest is ${sortedPackageTags2[0]}`,
815
- "debug"
816
- );
817
- }
818
908
  return sortedPackageTags2[0];
819
909
  }
820
910
  if (versionPrefix) {
@@ -822,9 +912,11 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
822
912
  packageTags = allTags.filter((tag) => pattern1.test(tag));
823
913
  if (packageTags.length > 0) {
824
914
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
825
- const afterAt = a.split("@")[1] || "";
915
+ const aParts = a.split("@");
916
+ const afterAt = aParts[aParts.length - 1] || "";
826
917
  const versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
827
- const afterAtB = b.split("@")[1] || "";
918
+ const bParts = b.split("@");
919
+ const afterAtB = bParts[bParts.length - 1] || "";
828
920
  const versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
829
921
  const cleanVersionA = import_semver.default.clean(versionA) || "0.0.0";
830
922
  const cleanVersionB = import_semver.default.clean(versionB) || "0.0.0";
@@ -840,8 +932,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
840
932
  packageTags = allTags.filter((tag) => pattern2.test(tag));
841
933
  if (packageTags.length > 0) {
842
934
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
843
- const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
844
- const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
935
+ const aParts = a.split("@");
936
+ const versionA = import_semver.default.clean(aParts[aParts.length - 1] || "") || "0.0.0";
937
+ const bParts = b.split("@");
938
+ const versionB = import_semver.default.clean(bParts[bParts.length - 1] || "") || "0.0.0";
845
939
  return import_semver.default.rcompare(versionA, versionB);
846
940
  });
847
941
  log(`Found ${packageTags.length} package tags using pattern: ${versionPrefix}packageName@...`, "debug");
@@ -861,8 +955,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
861
955
  return "";
862
956
  }
863
957
  const sortedPackageTags = [...packageTags].sort((a, b) => {
864
- const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
865
- const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
958
+ const aParts = a.split("@");
959
+ const versionA = import_semver.default.clean(aParts[aParts.length - 1] || "") || "0.0.0";
960
+ const bParts = b.split("@");
961
+ const versionB = import_semver.default.clean(bParts[bParts.length - 1] || "") || "0.0.0";
866
962
  return import_semver.default.rcompare(versionA, versionB);
867
963
  });
868
964
  log(`Found ${packageTags.length} package tags for ${packageName}`, "debug");
@@ -1039,6 +1135,7 @@ var import_config3 = require("@releasekit/config");
1039
1135
  var import_semver2 = __toESM(require("semver"), 1);
1040
1136
 
1041
1137
  // src/git/tagVerification.ts
1138
+ init_commandExecutor();
1042
1139
  function verifyTag(tagName, cwd4) {
1043
1140
  if (!tagName || tagName.trim() === "") {
1044
1141
  return { exists: false, reachable: false, error: "Empty tag name" };
@@ -1142,12 +1239,17 @@ var VersionMismatchError = class extends Error {
1142
1239
  this.name = "VersionMismatchError";
1143
1240
  }
1144
1241
  };
1145
- async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error") {
1242
+ async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error", strictReachable = false) {
1146
1243
  if (!tagName?.trim()) {
1147
1244
  return packageVersion ? { source: "package", version: packageVersion, reason: "No git tag provided" } : { source: "initial", version: "0.1.0", reason: "No git tag or package version available" };
1148
1245
  }
1149
1246
  const verification = verifyTag(tagName, cwd4);
1150
1247
  if (!verification.exists || !verification.reachable) {
1248
+ if (strictReachable) {
1249
+ throw new Error(
1250
+ `Git tag '${tagName}' is not reachable from the current commit. The tag exists but cannot be reached from HEAD, which usually means you're on a different branch or the tag is orphaned. To allow fallback to package version, set strictReachable to false in your configuration.`
1251
+ );
1252
+ }
1151
1253
  if (packageVersion) {
1152
1254
  log(
1153
1255
  `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
@@ -1253,7 +1355,8 @@ async function calculateVersion(config, options) {
1253
1355
  prereleaseIdentifier: configPrereleaseIdentifier,
1254
1356
  branchPattern,
1255
1357
  baseBranch,
1256
- mismatchStrategy
1358
+ mismatchStrategy,
1359
+ strictReachable
1257
1360
  } = config;
1258
1361
  const {
1259
1362
  latestTag,
@@ -1298,7 +1401,13 @@ async function calculateVersion(config, options) {
1298
1401
  const packageDir = pkgPath || (0, import_node_process2.cwd)();
1299
1402
  const manifestResult = getVersionFromManifests(packageDir);
1300
1403
  const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
1301
- versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir, mismatchStrategy);
1404
+ versionSource = await getBestVersionSource(
1405
+ latestTag,
1406
+ packageVersion,
1407
+ packageDir,
1408
+ mismatchStrategy,
1409
+ strictReachable
1410
+ );
1302
1411
  log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
1303
1412
  }
1304
1413
  const specifiedType = type;
@@ -1540,6 +1649,11 @@ var PackageProcessor = class {
1540
1649
  if (verification.exists && verification.reachable) {
1541
1650
  revisionRange = `${latestTag}..HEAD`;
1542
1651
  } else {
1652
+ if (this.config.strictReachable) {
1653
+ throw new Error(
1654
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1655
+ );
1656
+ }
1543
1657
  log(`Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`, "debug");
1544
1658
  revisionRange = "HEAD";
1545
1659
  }
@@ -1547,6 +1661,19 @@ var PackageProcessor = class {
1547
1661
  revisionRange = "HEAD";
1548
1662
  }
1549
1663
  changelogEntries = extractChangelogEntriesFromCommits(pkgPath, revisionRange);
1664
+ const allPackageDirs = packages.map((p) => p.dir);
1665
+ const sharedPackageNames = ["config", "core", "@releasekit/config", "@releasekit/core"];
1666
+ const sharedPackageDirs = packages.filter((p) => sharedPackageNames.includes(p.packageJson.name)).map((p) => p.dir);
1667
+ const repoLevelEntries = extractRepoLevelChangelogEntries(
1668
+ pkgPath,
1669
+ revisionRange,
1670
+ allPackageDirs,
1671
+ sharedPackageDirs
1672
+ );
1673
+ if (repoLevelEntries.length > 0) {
1674
+ log(`Adding ${repoLevelEntries.length} repo-level commit(s) to ${name} changelog`, "debug");
1675
+ changelogEntries = [...repoLevelEntries, ...changelogEntries];
1676
+ }
1550
1677
  if (changelogEntries.length === 0) {
1551
1678
  changelogEntries = [
1552
1679
  {
@@ -1855,6 +1982,11 @@ function createSyncStrategy(config) {
1855
1982
  });
1856
1983
  revisionRange = `${latestTag}..HEAD`;
1857
1984
  } catch {
1985
+ if (config.strictReachable) {
1986
+ throw new Error(
1987
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1988
+ );
1989
+ }
1858
1990
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1859
1991
  revisionRange = "HEAD";
1860
1992
  }
@@ -1974,6 +2106,11 @@ function createSingleStrategy(config) {
1974
2106
  });
1975
2107
  revisionRange = `${latestTag}..HEAD`;
1976
2108
  } catch {
2109
+ if (config.strictReachable) {
2110
+ throw new Error(
2111
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
2112
+ );
2113
+ }
1977
2114
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1978
2115
  revisionRange = "HEAD";
1979
2116
  }
package/dist/cli.js CHANGED
@@ -5,8 +5,9 @@ import {
5
5
  loadConfig,
6
6
  log,
7
7
  printJsonOutput
8
- } from "./chunk-3JG7UWIT.js";
8
+ } from "./chunk-25A2IEAC.js";
9
9
  import "./chunk-GQLJ7JQY.js";
10
+ import "./chunk-LMPZV35Z.js";
10
11
 
11
12
  // src/cli.ts
12
13
  import * as fs from "fs";
@@ -0,0 +1,8 @@
1
+ import {
2
+ execAsync,
3
+ execSync
4
+ } from "./chunk-LMPZV35Z.js";
5
+ export {
6
+ execAsync,
7
+ execSync
8
+ };
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/git/commandExecutor.ts
34
+ var commandExecutor_exports = {};
35
+ __export(commandExecutor_exports, {
36
+ execAsync: () => execAsync,
37
+ execSync: () => execSync
38
+ });
39
+ var import_node_child_process, execAsync, execSync;
40
+ var init_commandExecutor = __esm({
41
+ "src/git/commandExecutor.ts"() {
42
+ "use strict";
43
+ import_node_child_process = require("child_process");
44
+ execAsync = (file, args, options) => {
45
+ const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
46
+ return new Promise((resolve2, reject) => {
47
+ (0, import_node_child_process.execFile)(file, args, defaultOptions, (error, stdout, stderr) => {
48
+ if (error) {
49
+ reject(error);
50
+ } else {
51
+ resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
52
+ }
53
+ });
54
+ });
55
+ };
56
+ execSync = (file, args, options) => (0, import_node_child_process.execFileSync)(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
57
+ }
58
+ });
59
+
30
60
  // src/index.ts
31
61
  var index_exports = {};
32
62
  __export(index_exports, {
@@ -100,24 +130,7 @@ var import_semver3 = __toESM(require("semver"), 1);
100
130
  // src/git/repository.ts
101
131
  var import_node_fs = require("fs");
102
132
  var import_node_path = require("path");
103
-
104
- // src/git/commandExecutor.ts
105
- var import_node_child_process = require("child_process");
106
- var execAsync = (file, args, options) => {
107
- const defaultOptions = { maxBuffer: 1024 * 1024 * 10, ...options };
108
- return new Promise((resolve2, reject) => {
109
- (0, import_node_child_process.execFile)(file, args, defaultOptions, (error, stdout, stderr) => {
110
- if (error) {
111
- reject(error);
112
- } else {
113
- resolve2({ stdout: stdout.toString(), stderr: stderr.toString() });
114
- }
115
- });
116
- });
117
- };
118
- var execSync = (file, args, options) => (0, import_node_child_process.execFileSync)(file, args, { maxBuffer: 1024 * 1024 * 10, ...options });
119
-
120
- // src/git/repository.ts
133
+ init_commandExecutor();
121
134
  function isGitRepository(directory) {
122
135
  const gitDir = (0, import_node_path.join)(directory, ".git");
123
136
  if (!(0, import_node_fs.existsSync)(gitDir)) {
@@ -280,6 +293,7 @@ To fix this:
280
293
  }
281
294
 
282
295
  // src/git/tagsAndBranches.ts
296
+ init_commandExecutor();
283
297
  function getCommitsLength(pkgRoot, sinceTag) {
284
298
  try {
285
299
  let amount;
@@ -357,28 +371,37 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
357
371
  `Looking for tags for package ${packageName} with prefix ${versionPrefix || "none"}, packageSpecificTags: ${packageSpecificTags}`,
358
372
  "debug"
359
373
  );
360
- const allTags = await (0, import_git_semver_tags.getSemverTags)({
361
- tagPrefix: versionPrefix
362
- });
363
- log(`Retrieved ${allTags.length} tags: ${allTags.join(", ")}`, "debug");
374
+ let allTags = [];
375
+ try {
376
+ const { execSync: execSync2 } = await Promise.resolve().then(() => (init_commandExecutor(), commandExecutor_exports));
377
+ const tagsOutput = execSync2("git", ["tag", "-l"], { cwd: process.cwd() });
378
+ allTags = tagsOutput.toString().trim().split("\n").filter((tag) => tag.length > 0);
379
+ } catch (err) {
380
+ log(`Error getting tags: ${err instanceof Error ? err.message : String(err)}`, "error");
381
+ }
382
+ log(`Retrieved ${allTags.length} tags`, "debug");
364
383
  if (packageSpecificTags) {
365
384
  const packageTagPattern = escapeRegExp(tagTemplate).replace(/\\\$\\\{packageName\\\}/g, `(?:${escapedPackageName})`).replace(/\\\$\\\{prefix\\\}/g, `(?:${escapedPrefix})`).replace(/\\\$\\\{version\\\}/g, "(?:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)");
366
385
  log(`Using package tag pattern: ${packageTagPattern}`, "debug");
367
386
  const packageTagRegex = new RegExp(`^${packageTagPattern}$`);
368
387
  let packageTags = allTags.filter((tag) => packageTagRegex.test(tag));
388
+ log(`Found ${packageTags.length} matching tags for ${packageName}`, "debug");
369
389
  if (packageTags.length > 0) {
370
390
  const chronologicalFirst = packageTags[0];
391
+ void chronologicalFirst;
371
392
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
372
393
  let versionA = "";
373
394
  let versionB = "";
374
395
  if (a.includes("@")) {
375
- const afterAt = a.split("@")[1] || "";
396
+ const parts = a.split("@");
397
+ const afterAt = parts[parts.length - 1] || "";
376
398
  versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
377
399
  } else {
378
400
  versionA = a.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
379
401
  }
380
402
  if (b.includes("@")) {
381
- const afterAtB = b.split("@")[1] || "";
403
+ const parts = b.split("@");
404
+ const afterAtB = parts[parts.length - 1] || "";
382
405
  versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
383
406
  } else {
384
407
  versionB = b.replace(new RegExp(`^${escapeRegExp(packageName)}`), "").replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
@@ -389,12 +412,6 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
389
412
  });
390
413
  log(`Found ${packageTags.length} package tags using configured pattern`, "debug");
391
414
  log(`Using semantically latest tag: ${sortedPackageTags2[0]}`, "debug");
392
- if (sortedPackageTags2[0] !== chronologicalFirst) {
393
- log(
394
- `Package tag ordering differs: chronological first is ${chronologicalFirst}, semantic latest is ${sortedPackageTags2[0]}`,
395
- "debug"
396
- );
397
- }
398
415
  return sortedPackageTags2[0];
399
416
  }
400
417
  if (versionPrefix) {
@@ -402,9 +419,11 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
402
419
  packageTags = allTags.filter((tag) => pattern1.test(tag));
403
420
  if (packageTags.length > 0) {
404
421
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
405
- const afterAt = a.split("@")[1] || "";
422
+ const aParts = a.split("@");
423
+ const afterAt = aParts[aParts.length - 1] || "";
406
424
  const versionA = afterAt.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
407
- const afterAtB = b.split("@")[1] || "";
425
+ const bParts = b.split("@");
426
+ const afterAtB = bParts[bParts.length - 1] || "";
408
427
  const versionB = afterAtB.replace(new RegExp(`^${escapeRegExp(versionPrefix || "")}`), "");
409
428
  const cleanVersionA = import_semver.default.clean(versionA) || "0.0.0";
410
429
  const cleanVersionB = import_semver.default.clean(versionB) || "0.0.0";
@@ -420,8 +439,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
420
439
  packageTags = allTags.filter((tag) => pattern2.test(tag));
421
440
  if (packageTags.length > 0) {
422
441
  const sortedPackageTags2 = [...packageTags].sort((a, b) => {
423
- const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
424
- const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
442
+ const aParts = a.split("@");
443
+ const versionA = import_semver.default.clean(aParts[aParts.length - 1] || "") || "0.0.0";
444
+ const bParts = b.split("@");
445
+ const versionB = import_semver.default.clean(bParts[bParts.length - 1] || "") || "0.0.0";
425
446
  return import_semver.default.rcompare(versionA, versionB);
426
447
  });
427
448
  log(`Found ${packageTags.length} package tags using pattern: ${versionPrefix}packageName@...`, "debug");
@@ -441,8 +462,10 @@ async function getLatestTagForPackage(packageName, versionPrefix, options) {
441
462
  return "";
442
463
  }
443
464
  const sortedPackageTags = [...packageTags].sort((a, b) => {
444
- const versionA = import_semver.default.clean(a.split("@")[1] || "") || "0.0.0";
445
- const versionB = import_semver.default.clean(b.split("@")[1] || "") || "0.0.0";
465
+ const aParts = a.split("@");
466
+ const versionA = import_semver.default.clean(aParts[aParts.length - 1] || "") || "0.0.0";
467
+ const bParts = b.split("@");
468
+ const versionB = import_semver.default.clean(bParts[bParts.length - 1] || "") || "0.0.0";
446
469
  return import_semver.default.rcompare(versionA, versionB);
447
470
  });
448
471
  log(`Found ${packageTags.length} package tags for ${packageName}`, "debug");
@@ -578,6 +601,7 @@ var import_config3 = require("@releasekit/config");
578
601
  var import_semver2 = __toESM(require("semver"), 1);
579
602
 
580
603
  // src/git/tagVerification.ts
604
+ init_commandExecutor();
581
605
  function verifyTag(tagName, cwd4) {
582
606
  if (!tagName || tagName.trim() === "") {
583
607
  return { exists: false, reachable: false, error: "Empty tag name" };
@@ -681,12 +705,17 @@ var VersionMismatchError = class extends Error {
681
705
  this.name = "VersionMismatchError";
682
706
  }
683
707
  };
684
- async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error") {
708
+ async function getBestVersionSource(tagName, packageVersion, cwd4, mismatchStrategy = "error", strictReachable = false) {
685
709
  if (!tagName?.trim()) {
686
710
  return packageVersion ? { source: "package", version: packageVersion, reason: "No git tag provided" } : { source: "initial", version: "0.1.0", reason: "No git tag or package version available" };
687
711
  }
688
712
  const verification = verifyTag(tagName, cwd4);
689
713
  if (!verification.exists || !verification.reachable) {
714
+ if (strictReachable) {
715
+ throw new Error(
716
+ `Git tag '${tagName}' is not reachable from the current commit. The tag exists but cannot be reached from HEAD, which usually means you're on a different branch or the tag is orphaned. To allow fallback to package version, set strictReachable to false in your configuration.`
717
+ );
718
+ }
690
719
  if (packageVersion) {
691
720
  log(
692
721
  `Git tag '${tagName}' unreachable (${verification.error}), using package version: ${packageVersion}`,
@@ -792,7 +821,8 @@ async function calculateVersion(config, options) {
792
821
  prereleaseIdentifier: configPrereleaseIdentifier,
793
822
  branchPattern,
794
823
  baseBranch,
795
- mismatchStrategy
824
+ mismatchStrategy,
825
+ strictReachable
796
826
  } = config;
797
827
  const {
798
828
  latestTag,
@@ -837,7 +867,13 @@ async function calculateVersion(config, options) {
837
867
  const packageDir = pkgPath || (0, import_node_process.cwd)();
838
868
  const manifestResult = getVersionFromManifests(packageDir);
839
869
  const packageVersion = manifestResult.manifestFound && manifestResult.version ? manifestResult.version : void 0;
840
- versionSource = await getBestVersionSource(latestTag, packageVersion, packageDir, mismatchStrategy);
870
+ versionSource = await getBestVersionSource(
871
+ latestTag,
872
+ packageVersion,
873
+ packageDir,
874
+ mismatchStrategy,
875
+ strictReachable
876
+ );
841
877
  log(`Using version source: ${versionSource.source} (${versionSource.reason})`, "info");
842
878
  }
843
879
  const specifiedType = type;
@@ -1129,14 +1165,86 @@ var import_node_fs6 = __toESM(require("fs"), 1);
1129
1165
  var path6 = __toESM(require("path"), 1);
1130
1166
 
1131
1167
  // src/changelog/commitParser.ts
1168
+ init_commandExecutor();
1132
1169
  var CONVENTIONAL_COMMIT_REGEX = /^(\w+)(?:\(([^)]+)\))?(!)?: (.+)(?:\n\n([\s\S]*))?/;
1133
1170
  var BREAKING_CHANGE_REGEX = /BREAKING CHANGE: ([\s\S]+?)(?:\n\n|$)/;
1134
- function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
1171
+ function extractAllChangelogEntriesWithHash(projectDir, revisionRange) {
1172
+ try {
1173
+ const args = ["log", revisionRange, "--pretty=format:%H|||%B---COMMIT_DELIMITER---", "--no-merges"];
1174
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
1175
+ const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
1176
+ return commits.map((commit) => {
1177
+ const [hash, ...messageParts] = commit.split("|||");
1178
+ const message = messageParts.join("|||").trim();
1179
+ const entry = parseCommitMessage(message);
1180
+ if (entry && hash) {
1181
+ return { hash: hash.trim(), entry };
1182
+ }
1183
+ return null;
1184
+ }).filter((item) => item !== null);
1185
+ } catch (error) {
1186
+ const errorMessage = error instanceof Error ? error.message : String(error);
1187
+ log(`Error extracting all commits with hash: ${errorMessage}`, "error");
1188
+ return [];
1189
+ }
1190
+ }
1191
+ function commitTouchesAnyPackage(projectDir, commitHash, packageDirs, sharedPackageDirs = []) {
1135
1192
  try {
1136
- const output = execSync("git", ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"], {
1193
+ const output = execSync("git", ["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash], {
1137
1194
  cwd: projectDir,
1138
1195
  encoding: "utf8"
1139
- }).toString();
1196
+ }).toString().trim();
1197
+ if (!output) {
1198
+ return false;
1199
+ }
1200
+ const changedFiles = output.split("\n");
1201
+ return changedFiles.some((file) => {
1202
+ return packageDirs.some((pkgDir) => {
1203
+ if (sharedPackageDirs.some((sharedDir) => pkgDir.includes(sharedDir))) {
1204
+ return false;
1205
+ }
1206
+ const normalizedFile = file.replace(/\\/g, "/");
1207
+ const normalizedPkgDir = pkgDir.replace(/\\/g, "/").replace(/^\.\//, "");
1208
+ return normalizedFile.startsWith(normalizedPkgDir);
1209
+ });
1210
+ });
1211
+ } catch (error) {
1212
+ log(
1213
+ `Error checking if commit ${commitHash} touches packages: ${error instanceof Error ? error.message : String(error)}`,
1214
+ "debug"
1215
+ );
1216
+ return false;
1217
+ }
1218
+ }
1219
+ function extractRepoLevelChangelogEntries(projectDir, revisionRange, packageDirs, sharedPackageDirs = []) {
1220
+ try {
1221
+ const allCommits = extractAllChangelogEntriesWithHash(projectDir, revisionRange);
1222
+ const repoLevelCommits = allCommits.filter((commit) => {
1223
+ const touchesPackage = commitTouchesAnyPackage(projectDir, commit.hash, packageDirs, sharedPackageDirs);
1224
+ return !touchesPackage;
1225
+ });
1226
+ if (repoLevelCommits.length > 0) {
1227
+ log(
1228
+ `Found ${repoLevelCommits.length} repo-level commit(s) (including shared packages: ${sharedPackageDirs.join(", ")})`,
1229
+ "debug"
1230
+ );
1231
+ }
1232
+ return repoLevelCommits.map((c) => c.entry);
1233
+ } catch (error) {
1234
+ log(`Error extracting repo-level commits: ${error instanceof Error ? error.message : String(error)}`, "warning");
1235
+ return [];
1236
+ }
1237
+ }
1238
+ function extractChangelogEntriesFromCommits(projectDir, revisionRange) {
1239
+ return extractCommitsFromGitLog(projectDir, revisionRange, true);
1240
+ }
1241
+ function extractCommitsFromGitLog(projectDir, revisionRange, filterToPath) {
1242
+ try {
1243
+ const args = ["log", revisionRange, "--pretty=format:%B---COMMIT_DELIMITER---", "--no-merges"];
1244
+ if (filterToPath) {
1245
+ args.push("--", ".");
1246
+ }
1247
+ const output = execSync("git", args, { cwd: projectDir, encoding: "utf8" }).toString();
1140
1248
  const commits = output.split("---COMMIT_DELIMITER---").filter((commit) => commit.trim() !== "");
1141
1249
  return commits.map((commit) => parseCommitMessage(commit)).filter((entry) => entry !== null);
1142
1250
  } catch (error) {
@@ -1229,8 +1337,12 @@ function extractIssueIds(body) {
1229
1337
  return issueIds;
1230
1338
  }
1231
1339
 
1340
+ // src/core/versionStrategies.ts
1341
+ init_commandExecutor();
1342
+
1232
1343
  // src/git/commands.ts
1233
1344
  var import_node_process2 = require("process");
1345
+ init_commandExecutor();
1234
1346
  async function gitAdd(files) {
1235
1347
  return execAsync("git", ["add", ...files]);
1236
1348
  }
@@ -1535,6 +1647,11 @@ var PackageProcessor = class {
1535
1647
  if (verification.exists && verification.reachable) {
1536
1648
  revisionRange = `${latestTag}..HEAD`;
1537
1649
  } else {
1650
+ if (this.config.strictReachable) {
1651
+ throw new Error(
1652
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1653
+ );
1654
+ }
1538
1655
  log(`Tag ${latestTag} is unreachable (${verification.error}), using all commits for changelog`, "debug");
1539
1656
  revisionRange = "HEAD";
1540
1657
  }
@@ -1542,6 +1659,19 @@ var PackageProcessor = class {
1542
1659
  revisionRange = "HEAD";
1543
1660
  }
1544
1661
  changelogEntries = extractChangelogEntriesFromCommits(pkgPath, revisionRange);
1662
+ const allPackageDirs = packages.map((p) => p.dir);
1663
+ const sharedPackageNames = ["config", "core", "@releasekit/config", "@releasekit/core"];
1664
+ const sharedPackageDirs = packages.filter((p) => sharedPackageNames.includes(p.packageJson.name)).map((p) => p.dir);
1665
+ const repoLevelEntries = extractRepoLevelChangelogEntries(
1666
+ pkgPath,
1667
+ revisionRange,
1668
+ allPackageDirs,
1669
+ sharedPackageDirs
1670
+ );
1671
+ if (repoLevelEntries.length > 0) {
1672
+ log(`Adding ${repoLevelEntries.length} repo-level commit(s) to ${name} changelog`, "debug");
1673
+ changelogEntries = [...repoLevelEntries, ...changelogEntries];
1674
+ }
1545
1675
  if (changelogEntries.length === 0) {
1546
1676
  changelogEntries = [
1547
1677
  {
@@ -1850,6 +1980,11 @@ function createSyncStrategy(config) {
1850
1980
  });
1851
1981
  revisionRange = `${latestTag}..HEAD`;
1852
1982
  } catch {
1983
+ if (config.strictReachable) {
1984
+ throw new Error(
1985
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
1986
+ );
1987
+ }
1853
1988
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1854
1989
  revisionRange = "HEAD";
1855
1990
  }
@@ -1969,6 +2104,11 @@ function createSingleStrategy(config) {
1969
2104
  });
1970
2105
  revisionRange = `${latestTag}..HEAD`;
1971
2106
  } catch {
2107
+ if (config.strictReachable) {
2108
+ throw new Error(
2109
+ `Cannot generate changelog: tag '${latestTag}' is not reachable from the current commit. When strictReachable is enabled, all tags must be reachable. To allow fallback to all commits, set strictReachable to false.`
2110
+ );
2111
+ }
1972
2112
  log(`Tag ${latestTag} doesn't exist, using all commits for changelog`, "debug");
1973
2113
  revisionRange = "HEAD";
1974
2114
  }
package/dist/index.d.cts CHANGED
@@ -11,6 +11,7 @@ interface VersionConfigBase {
11
11
  baseBranch?: string;
12
12
  path?: string;
13
13
  name?: string;
14
+ strictReachable?: boolean;
14
15
  }
15
16
  interface Config extends VersionConfigBase {
16
17
  tagTemplate: string;
@@ -30,6 +31,7 @@ interface Config extends VersionConfigBase {
30
31
  latestTag?: string;
31
32
  isPrerelease?: boolean;
32
33
  mismatchStrategy?: 'error' | 'warn' | 'ignore' | 'prefer-package' | 'prefer-git';
34
+ strictReachable?: boolean;
33
35
  cargo?: {
34
36
  enabled?: boolean;
35
37
  paths?: string[];
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ interface VersionConfigBase {
11
11
  baseBranch?: string;
12
12
  path?: string;
13
13
  name?: string;
14
+ strictReachable?: boolean;
14
15
  }
15
16
  interface Config extends VersionConfigBase {
16
17
  tagTemplate: string;
@@ -30,6 +31,7 @@ interface Config extends VersionConfigBase {
30
31
  latestTag?: string;
31
32
  isPrerelease?: boolean;
32
33
  mismatchStrategy?: 'error' | 'warn' | 'ignore' | 'prefer-package' | 'prefer-git';
34
+ strictReachable?: boolean;
33
35
  cargo?: {
34
36
  enabled?: boolean;
35
37
  paths?: string[];
package/dist/index.js CHANGED
@@ -10,10 +10,11 @@ import {
10
10
  enableJsonOutput,
11
11
  getJsonData,
12
12
  loadConfig
13
- } from "./chunk-3JG7UWIT.js";
13
+ } from "./chunk-25A2IEAC.js";
14
14
  import {
15
15
  BaseVersionError
16
16
  } from "./chunk-GQLJ7JQY.js";
17
+ import "./chunk-LMPZV35Z.js";
17
18
  export {
18
19
  BaseVersionError,
19
20
  PackageProcessor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@releasekit/version",
3
- "version": "0.2.0-next.1",
3
+ "version": "0.2.0-next.11",
4
4
  "description": "Semantic versioning based on Git history and conventional commits",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",