relizy 0.2.8 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { logger, execPromise } from '@maz-ui/node';
2
+ import process$1 from 'node:process';
2
3
  import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
4
  import path, { join, relative } from 'node:path';
4
5
  import { getGitDiff, parseCommits, formatCompareChanges, formatReference, resolveRepoConfig, getRepoConfig, createGithubRelease } from 'changelogen';
@@ -7,7 +8,6 @@ import { confirm, input } from '@inquirer/prompts';
7
8
  import { upperFirst, formatJson } from '@maz-ui/utils';
8
9
  import * as semver from 'semver';
9
10
  import { execSync } from 'node:child_process';
10
- import process$1, { exit } from 'node:process';
11
11
  import { setupDotenv, loadConfig } from 'c12';
12
12
  import { defu } from 'defu';
13
13
  import { convert } from 'convert-gitmoji';
@@ -276,11 +276,11 @@ function getPackageReleaseType({
276
276
  });
277
277
  }
278
278
  async function getPackages({
279
- patterns,
280
279
  config,
281
280
  suffix,
282
281
  force
283
282
  }) {
283
+ const patterns = config.monorepo?.packages;
284
284
  const readedPackages = readPackages({
285
285
  cwd: config.cwd,
286
286
  patterns,
@@ -445,14 +445,13 @@ async function getPackageCommits({
445
445
  if (!isAllowedCommit({ commit, type, changelog })) {
446
446
  return false;
447
447
  }
448
- const isTracked = isCommitOfTrackedPackages({ commit, config });
449
- if ((pkg.path === changelogConfig.cwd || pkg.name === rootPackage.name) && isTracked) {
448
+ const isTrackedPackage = isCommitOfTrackedPackages({ commit, config });
449
+ if ((pkg.path === changelogConfig.cwd || pkg.name === rootPackage.name) && isTrackedPackage) {
450
450
  return true;
451
451
  }
452
452
  const packageRelativePath = relative(changelogConfig.cwd, pkg.path);
453
- const scopeMatches = commit.scope === pkg.name;
454
453
  const bodyContainsPath = commit.body.includes(packageRelativePath);
455
- return (scopeMatches || bodyContainsPath) && isTracked;
454
+ return bodyContainsPath && isTrackedPackage;
456
455
  });
457
456
  logger.debug(`Found ${commits.length} commit(s) for ${pkg.name} from ${from} to ${to}`);
458
457
  if (commits.length > 0) {
@@ -477,7 +476,7 @@ async function executeHook(hook, config, dryRun, params) {
477
476
  logger.info(`Executing hook ${hook}`);
478
477
  const result = await hookInput(config, dryRun, params);
479
478
  if (result)
480
- logger.debug(`Hook ${hook} returned: ${result}`);
479
+ logger.debug(`Hook ${hook} returned: ${typeof result === "object" ? JSON.stringify(result) : result}`);
481
480
  logger.info(`Hook ${hook} executed`);
482
481
  return result;
483
482
  }
@@ -490,62 +489,62 @@ async function executeHook(hook, config, dryRun, params) {
490
489
  noStdout: true
491
490
  });
492
491
  if (result)
493
- logger.debug(`Hook ${hook} returned: ${result}`);
492
+ logger.debug(`Hook ${hook} returned: ${typeof result === "object" ? JSON.stringify(result) : result}`);
494
493
  logger.info(`Hook ${hook} executed`);
495
494
  return result;
496
495
  }
497
496
  }
498
497
  function isInCI() {
499
498
  return Boolean(
500
- process.env.CI === "true" || process.env.CONTINUOUS_INTEGRATION === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.GITHUB_WORKFLOW || process.env.GITLAB_CI === "true" || process.env.CIRCLECI === "true" || process.env.TRAVIS === "true" || process.env.JENKINS_HOME || process.env.JENKINS_URL || process.env.BUILD_ID || process.env.TF_BUILD === "True" || process.env.AZURE_PIPELINES === "true" || process.env.TEAMCITY_VERSION || process.env.BITBUCKET_BUILD_NUMBER || process.env.DRONE === "true" || process.env.APPVEYOR === "True" || process.env.APPVEYOR === "true" || process.env.BUILDKITE === "true" || process.env.CODEBUILD_BUILD_ID || process.env.NETLIFY === "true" || process.env.VERCEL === "1" || process.env.HEROKU_TEST_RUN_ID || process.env.BUDDY === "true" || process.env.SEMAPHORE === "true" || process.env.CF_BUILD_ID || process.env.bamboo_buildKey || process.env.BUILD_ID && process.env.PROJECT_ID || process.env.SCREWDRIVER === "true" || process.env.STRIDER === "true"
499
+ process$1.env.CI === "true" || process$1.env.CONTINUOUS_INTEGRATION === "true" || process$1.env.GITHUB_ACTIONS === "true" || process$1.env.GITHUB_WORKFLOW || process$1.env.GITLAB_CI === "true" || process$1.env.CIRCLECI === "true" || process$1.env.TRAVIS === "true" || process$1.env.JENKINS_HOME || process$1.env.JENKINS_URL || process$1.env.BUILD_ID || process$1.env.TF_BUILD === "True" || process$1.env.AZURE_PIPELINES === "true" || process$1.env.TEAMCITY_VERSION || process$1.env.BITBUCKET_BUILD_NUMBER || process$1.env.DRONE === "true" || process$1.env.APPVEYOR === "True" || process$1.env.APPVEYOR === "true" || process$1.env.BUILDKITE === "true" || process$1.env.CODEBUILD_BUILD_ID || process$1.env.NETLIFY === "true" || process$1.env.VERCEL === "1" || process$1.env.HEROKU_TEST_RUN_ID || process$1.env.BUDDY === "true" || process$1.env.SEMAPHORE === "true" || process$1.env.CF_BUILD_ID || process$1.env.bamboo_buildKey || process$1.env.BUILD_ID && process$1.env.PROJECT_ID || process$1.env.SCREWDRIVER === "true" || process$1.env.STRIDER === "true"
501
500
  );
502
501
  }
503
502
  function getCIName() {
504
- if (process.env.GITHUB_ACTIONS === "true")
503
+ if (process$1.env.GITHUB_ACTIONS === "true")
505
504
  return "GitHub Actions";
506
- if (process.env.GITLAB_CI === "true")
505
+ if (process$1.env.GITLAB_CI === "true")
507
506
  return "GitLab CI";
508
- if (process.env.CIRCLECI === "true")
507
+ if (process$1.env.CIRCLECI === "true")
509
508
  return "CircleCI";
510
- if (process.env.TRAVIS === "true")
509
+ if (process$1.env.TRAVIS === "true")
511
510
  return "Travis CI";
512
- if (process.env.JENKINS_HOME || process.env.JENKINS_URL)
511
+ if (process$1.env.JENKINS_HOME || process$1.env.JENKINS_URL)
513
512
  return "Jenkins";
514
- if (process.env.TF_BUILD === "True")
513
+ if (process$1.env.TF_BUILD === "True")
515
514
  return "Azure Pipelines";
516
- if (process.env.TEAMCITY_VERSION)
515
+ if (process$1.env.TEAMCITY_VERSION)
517
516
  return "TeamCity";
518
- if (process.env.BITBUCKET_BUILD_NUMBER)
517
+ if (process$1.env.BITBUCKET_BUILD_NUMBER)
519
518
  return "Bitbucket Pipelines";
520
- if (process.env.DRONE === "true")
519
+ if (process$1.env.DRONE === "true")
521
520
  return "Drone";
522
- if (process.env.APPVEYOR)
521
+ if (process$1.env.APPVEYOR)
523
522
  return "AppVeyor";
524
- if (process.env.BUILDKITE === "true")
523
+ if (process$1.env.BUILDKITE === "true")
525
524
  return "Buildkite";
526
- if (process.env.CODEBUILD_BUILD_ID)
525
+ if (process$1.env.CODEBUILD_BUILD_ID)
527
526
  return "AWS CodeBuild";
528
- if (process.env.NETLIFY === "true")
527
+ if (process$1.env.NETLIFY === "true")
529
528
  return "Netlify";
530
- if (process.env.VERCEL === "1")
529
+ if (process$1.env.VERCEL === "1")
531
530
  return "Vercel";
532
- if (process.env.HEROKU_TEST_RUN_ID)
531
+ if (process$1.env.HEROKU_TEST_RUN_ID)
533
532
  return "Heroku CI";
534
- if (process.env.BUDDY === "true")
533
+ if (process$1.env.BUDDY === "true")
535
534
  return "Buddy";
536
- if (process.env.SEMAPHORE === "true")
535
+ if (process$1.env.SEMAPHORE === "true")
537
536
  return "Semaphore";
538
- if (process.env.CF_BUILD_ID)
537
+ if (process$1.env.CF_BUILD_ID)
539
538
  return "Codefresh";
540
- if (process.env.bamboo_buildKey)
539
+ if (process$1.env.bamboo_buildKey)
541
540
  return "Bamboo";
542
- if (process.env.BUILD_ID && process.env.PROJECT_ID)
541
+ if (process$1.env.BUILD_ID && process$1.env.PROJECT_ID)
543
542
  return "Google Cloud Build";
544
- if (process.env.SCREWDRIVER === "true")
543
+ if (process$1.env.SCREWDRIVER === "true")
545
544
  return "Screwdriver";
546
- if (process.env.STRIDER === "true")
545
+ if (process$1.env.STRIDER === "true")
547
546
  return "Strider";
548
- if (process.env.CI === "true")
547
+ if (process$1.env.CI === "true")
549
548
  return "Unknown CI";
550
549
  return null;
551
550
  }
@@ -611,17 +610,19 @@ async function getPackagesOrBumpedPackages({
611
610
  }
612
611
  return await getPackages({
613
612
  config,
614
- patterns: config.monorepo?.packages,
615
613
  suffix,
616
614
  force
617
615
  });
618
616
  }
619
617
 
620
- function getGitStatus(cwd) {
621
- return execSync("git status --porcelain", {
618
+ function getGitStatus(cwd, trim = true) {
619
+ const status = execSync("git status --porcelain", {
622
620
  cwd,
623
621
  encoding: "utf8"
624
- }).trim();
622
+ });
623
+ if (trim)
624
+ return status.trim();
625
+ return status;
625
626
  }
626
627
  function checkGitStatusIfDirty() {
627
628
  logger.debug("Checking git status");
@@ -661,6 +662,9 @@ function detectGitProvider(cwd = process.cwd()) {
661
662
  if (remoteUrl.includes("gitlab.com") || remoteUrl.includes("gitlab")) {
662
663
  return "gitlab";
663
664
  }
665
+ if (remoteUrl.includes("bitbucket.org") || remoteUrl.includes("bitbucket")) {
666
+ return "bitbucket";
667
+ }
664
668
  return null;
665
669
  } catch {
666
670
  return null;
@@ -668,7 +672,7 @@ function detectGitProvider(cwd = process.cwd()) {
668
672
  }
669
673
  function parseGitRemoteUrl(remoteUrl) {
670
674
  const sshRegex = /git@[\w.-]+:([\w.-]+)\/([\w.-]+?)(?:\.git)?$/;
671
- const httpsRegex = /https?:\/\/[\w.-]+\/([\w.-]+)\/([\w.-]+?)(?:\.git)?$/;
675
+ const httpsRegex = /https?:\/\/[\w.-]+\/(.+?)\/([^/]+?)(?:\.git)?$/;
672
676
  const sshMatch = remoteUrl.match(sshRegex);
673
677
  if (sshMatch) {
674
678
  return {
@@ -685,6 +689,27 @@ function parseGitRemoteUrl(remoteUrl) {
685
689
  }
686
690
  return null;
687
691
  }
692
+ function getModifiedReleaseFilePatterns({ config }) {
693
+ const gitStatusRaw = getGitStatus(config.cwd, false);
694
+ if (!gitStatusRaw) {
695
+ logger.debug("No modified files in git status");
696
+ return [];
697
+ }
698
+ const modifiedFiles = gitStatusRaw.split("\n").filter((line) => line.length > 0).map((line) => {
699
+ if (line.length < 4)
700
+ return null;
701
+ const filename = line.substring(3).trim();
702
+ return filename || null;
703
+ }).filter((file) => file !== null);
704
+ const releaseFiles = modifiedFiles.filter((file) => {
705
+ const isPackageJson = file === "package.json" || file.endsWith("/package.json");
706
+ const isChangelog = file === "CHANGELOG.md" || file.endsWith("/CHANGELOG.md");
707
+ const isLerna = file === "lerna.json";
708
+ return isPackageJson || isChangelog || isLerna;
709
+ });
710
+ logger.debug(`Found ${releaseFiles.length} modified release files:`, releaseFiles.join(", "));
711
+ return releaseFiles;
712
+ }
688
713
  async function createCommitAndTags({
689
714
  config,
690
715
  noVerify,
@@ -696,13 +721,7 @@ async function createCommitAndTags({
696
721
  const internalConfig = config || await loadRelizyConfig();
697
722
  try {
698
723
  await executeHook("before:commit-and-tag", internalConfig, dryRun ?? false);
699
- const filePatternsToAdd = [
700
- "package.json",
701
- "lerna.json",
702
- "CHANGELOG.md",
703
- "**/CHANGELOG.md",
704
- "**/package.json"
705
- ];
724
+ const filePatternsToAdd = getModifiedReleaseFilePatterns({ config: internalConfig });
706
725
  logger.start("Start commit and tag");
707
726
  logger.debug("Adding files to git staging area...");
708
727
  for (const pattern of filePatternsToAdd) {
@@ -821,6 +840,47 @@ async function pushCommitAndTags({ config, dryRun, logLevel, cwd }) {
821
840
  }
822
841
  logger.success("Pushing changes and tags completed!");
823
842
  }
843
+ async function rollbackModifiedFiles({
844
+ config
845
+ }) {
846
+ const modifiedFiles = getModifiedReleaseFilePatterns({ config });
847
+ if (modifiedFiles.length === 0) {
848
+ logger.debug("No modified files to rollback");
849
+ return;
850
+ }
851
+ logger.debug(`Rolling back ${modifiedFiles.length} modified file(s)...`);
852
+ logger.debug(`Files to rollback: ${modifiedFiles.join(", ")}`);
853
+ try {
854
+ const fileList = modifiedFiles.join(" ");
855
+ logger.debug(`Restoring specific files from HEAD: ${fileList}`);
856
+ await execPromise(`git checkout HEAD -- ${fileList}`, {
857
+ cwd: config.cwd,
858
+ logLevel: config.logLevel,
859
+ noStderr: true
860
+ });
861
+ logger.debug("Checking for untracked release files to remove...");
862
+ for (const file of modifiedFiles) {
863
+ const filePath = join(config.cwd, file);
864
+ if (existsSync(filePath)) {
865
+ try {
866
+ execSync(`git ls-files --error-unmatch "${file}"`, {
867
+ cwd: config.cwd,
868
+ encoding: "utf8",
869
+ stdio: "pipe"
870
+ });
871
+ } catch {
872
+ logger.debug(`Removing untracked file: ${file}`);
873
+ execSync(`rm "${filePath}"`, { cwd: config.cwd });
874
+ }
875
+ }
876
+ }
877
+ logger.success(`Successfully rolled back ${modifiedFiles.length} release file(s)`);
878
+ } catch (error) {
879
+ logger.error("Failed to rollback modified files automatically");
880
+ logger.warn(`Please manually restore these files: ${modifiedFiles.join(", ")}`);
881
+ throw error;
882
+ }
883
+ }
824
884
  function getFirstCommit(cwd) {
825
885
  const result = execSync(
826
886
  "git rev-list --max-parents=0 HEAD",
@@ -848,7 +908,8 @@ async function generateMarkDown({
848
908
  config,
849
909
  from,
850
910
  to,
851
- isFirstCommit
911
+ isFirstCommit,
912
+ minify
852
913
  }) {
853
914
  const typeGroups = groupBy(commits, "type");
854
915
  const markdown = [];
@@ -861,7 +922,7 @@ async function generateMarkDown({
861
922
  const versionTitle = updatedConfig.to;
862
923
  const changelogTitle = `${updatedConfig.from}...${updatedConfig.to}`;
863
924
  markdown.push("", `## ${changelogTitle}`, "");
864
- if (updatedConfig.repo && updatedConfig.from && versionTitle) {
925
+ if (updatedConfig.repo && updatedConfig.from && versionTitle && !minify) {
865
926
  const formattedCompareLink = formatCompareChanges(versionTitle, {
866
927
  ...updatedConfig,
867
928
  from: isFirstCommit ? getFirstCommit(updatedConfig.cwd) : updatedConfig.from
@@ -878,7 +939,11 @@ async function generateMarkDown({
878
939
  }
879
940
  markdown.push("", `### ${updatedConfig.types[type]?.title}`, "");
880
941
  for (const commit of group.reverse()) {
881
- const line = formatCommit(commit, updatedConfig);
942
+ const line = formatCommit({
943
+ commit,
944
+ config: updatedConfig,
945
+ minify
946
+ });
882
947
  markdown.push(line);
883
948
  if (commit.isBreaking) {
884
949
  breakingChanges.push(line);
@@ -890,7 +955,7 @@ async function generateMarkDown({
890
955
  }
891
956
  const _authors = /* @__PURE__ */ new Map();
892
957
  for (const commit of commits) {
893
- if (!commit.author) {
958
+ if (!commit.author || minify) {
894
959
  continue;
895
960
  }
896
961
  const name = formatName(commit.author.name);
@@ -970,9 +1035,9 @@ function getCommitBody(commit) {
970
1035
  ${indentedBody}
971
1036
  `;
972
1037
  }
973
- function formatCommit(commit, config) {
974
- const body = config.changelog.includeCommitBody ? getCommitBody(commit) : "";
975
- return `- ${commit.scope ? `**${commit.scope.trim()}:** ` : ""}${commit.isBreaking ? "\u26A0\uFE0F " : ""}${upperFirst(commit.description)}${formatReferences(commit.references, config)}${body}`;
1038
+ function formatCommit({ commit, config, minify }) {
1039
+ const body = config.changelog.includeCommitBody && !minify ? getCommitBody(commit) : "";
1040
+ return `- ${commit.scope ? `**${commit.scope.trim()}:** ` : ""}${commit.isBreaking ? "\u26A0\uFE0F " : ""}${upperFirst(commit.description)}${minify ? "" : formatReferences(commit.references, config)}${body}`;
976
1041
  }
977
1042
  function formatReferences(references, config) {
978
1043
  const pr = references.filter((ref) => ref.type === "pull-request");
@@ -997,1472 +1062,1500 @@ function groupBy(items, key) {
997
1062
  return groups;
998
1063
  }
999
1064
 
1000
- function fromTagIsFirstCommit(fromTag, cwd) {
1001
- return fromTag === getFirstCommit(cwd);
1065
+ function isGraduatingToStableBetweenVersion(version, newVersion) {
1066
+ const isSameBase = semver.major(version) === semver.major(newVersion) && semver.minor(version) === semver.minor(newVersion) && semver.patch(version) === semver.patch(newVersion);
1067
+ const fromPrerelease = semver.prerelease(version) !== null;
1068
+ const toStable = semver.prerelease(newVersion) === null;
1069
+ return isSameBase && fromPrerelease && toStable;
1002
1070
  }
1003
- async function generateChangelog({
1004
- pkg,
1005
- config,
1006
- dryRun,
1007
- newVersion
1071
+ function determineSemverChange(commits, types) {
1072
+ let [hasMajor, hasMinor, hasPatch] = [false, false, false];
1073
+ for (const commit of commits) {
1074
+ const commitType = types[commit.type];
1075
+ if (!commitType) {
1076
+ continue;
1077
+ }
1078
+ const semverType = commitType.semver;
1079
+ if (semverType === "major" || commit.isBreaking) {
1080
+ hasMajor = true;
1081
+ } else if (semverType === "minor") {
1082
+ hasMinor = true;
1083
+ } else if (semverType === "patch") {
1084
+ hasPatch = true;
1085
+ }
1086
+ }
1087
+ return hasMajor ? "major" : hasMinor ? "minor" : hasPatch ? "patch" : void 0;
1088
+ }
1089
+ function detectReleaseTypeFromCommits(commits, types) {
1090
+ return determineSemverChange(commits, types);
1091
+ }
1092
+ function validatePrereleaseDowngrade(currentVersion, targetPreid, configuredType) {
1093
+ if (configuredType !== "prerelease" || !targetPreid || !isPrerelease(currentVersion)) {
1094
+ return;
1095
+ }
1096
+ const testVersion = semver.inc(currentVersion, "prerelease", targetPreid);
1097
+ const isNotUpgrade = testVersion && !semver.gt(testVersion, currentVersion);
1098
+ if (isNotUpgrade) {
1099
+ throw new Error(`Unable to graduate from ${currentVersion} to ${testVersion}, it's not a valid prerelease`);
1100
+ }
1101
+ }
1102
+ function handleStableVersionWithReleaseType(commits, types, force) {
1103
+ if (!commits?.length && !force) {
1104
+ logger.debug('No commits found for stable version with "release" type, skipping bump');
1105
+ return void 0;
1106
+ }
1107
+ const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1108
+ if (!detectedType && !force) {
1109
+ logger.debug("No significant commits found, skipping bump");
1110
+ return void 0;
1111
+ }
1112
+ logger.debug(`Auto-detected release type from commits: ${detectedType}`);
1113
+ return detectedType;
1114
+ }
1115
+ function handleStableVersionWithPrereleaseType(commits, types, force) {
1116
+ if (!commits?.length && !force) {
1117
+ logger.debug('No commits found for stable version with "prerelease" type, skipping bump');
1118
+ return void 0;
1119
+ }
1120
+ const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1121
+ if (!detectedType) {
1122
+ logger.debug("No significant commits found, using prepatch as default");
1123
+ return "prepatch";
1124
+ }
1125
+ const prereleaseType = `pre${detectedType}`;
1126
+ logger.debug(`Auto-detected prerelease type from commits: ${prereleaseType}`);
1127
+ return prereleaseType;
1128
+ }
1129
+ function handlePrereleaseVersionToStable(currentVersion) {
1130
+ logger.debug(`Graduating from prerelease ${currentVersion} to stable release`);
1131
+ return "release";
1132
+ }
1133
+ function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force }) {
1134
+ const currentPreid = getPreid(currentVersion);
1135
+ const hasChangedPreid = preid && currentPreid && currentPreid !== preid;
1136
+ if (hasChangedPreid) {
1137
+ const testVersion = semver.inc(currentVersion, "prerelease", preid);
1138
+ if (!testVersion) {
1139
+ throw new Error(`Unable to change preid from ${currentPreid} to ${preid} for version ${currentVersion}`);
1140
+ }
1141
+ const isUpgrade = semver.gt(testVersion, currentVersion);
1142
+ if (!isUpgrade) {
1143
+ throw new Error(`Unable to change preid from ${currentVersion} to ${testVersion}, it's not a valid upgrade (cannot downgrade from ${currentPreid} to ${preid})`);
1144
+ }
1145
+ return "prerelease";
1146
+ }
1147
+ if (!commits?.length && !force) {
1148
+ logger.debug("No commits found for prerelease version, skipping bump");
1149
+ return void 0;
1150
+ }
1151
+ logger.debug(`Incrementing prerelease version: ${currentVersion}`);
1152
+ return "prerelease";
1153
+ }
1154
+ function handleExplicitReleaseType({
1155
+ releaseType,
1156
+ currentVersion
1008
1157
  }) {
1009
- let fromTag = config.from || pkg.fromTag || getFirstCommit(config.cwd);
1010
- const isFirstCommit = fromTagIsFirstCommit(fromTag, config.cwd);
1011
- if (isFirstCommit) {
1012
- fromTag = config.monorepo?.versionMode === "independent" ? getIndependentTag({ version: "0.0.0", name: pkg.name }) : config.templates.tagBody.replace("{{newVersion}}", "0.0.0");
1158
+ const isCurrentPrerelease = isPrerelease(currentVersion);
1159
+ const isGraduatingToStable = isCurrentPrerelease && isStableReleaseType(releaseType);
1160
+ if (isGraduatingToStable) {
1161
+ logger.debug(`Graduating from prerelease ${currentVersion} to stable with type: ${releaseType}`);
1162
+ } else {
1163
+ logger.debug(`Using explicit release type: ${releaseType}`);
1013
1164
  }
1014
- let toTag = config.to;
1015
- if (!toTag) {
1016
- toTag = config.monorepo?.versionMode === "independent" ? getIndependentTag({ version: newVersion, name: pkg.name }) : config.templates.tagBody.replace("{{newVersion}}", newVersion);
1165
+ return releaseType;
1166
+ }
1167
+ function determineReleaseType({
1168
+ currentVersion,
1169
+ commits,
1170
+ releaseType,
1171
+ preid,
1172
+ types,
1173
+ force
1174
+ }) {
1175
+ if (releaseType === "release" && preid) {
1176
+ throw new Error('You cannot use a "release" type with a "preid", to use a preid you must use a "prerelease" type');
1017
1177
  }
1018
- if (!toTag) {
1019
- throw new Error(`No tag found for ${pkg.name}`);
1178
+ validatePrereleaseDowngrade(currentVersion, preid, releaseType);
1179
+ if (force) {
1180
+ logger.debug(`Force flag enabled, using configured type: ${releaseType}`);
1181
+ return releaseType;
1020
1182
  }
1021
- logger.debug(`Generating changelog for ${pkg.name} - from ${fromTag} to ${toTag}`);
1022
- try {
1023
- config = {
1024
- ...config,
1025
- from: fromTag,
1026
- to: toTag
1027
- };
1028
- const generatedChangelog = await generateMarkDown({
1029
- commits: pkg.commits,
1030
- config,
1031
- from: fromTag,
1032
- isFirstCommit,
1033
- to: toTag
1034
- });
1035
- let changelog = generatedChangelog;
1036
- if (pkg.commits.length === 0) {
1037
- changelog = `${changelog}
1038
-
1039
- ${config.templates.emptyChangelogContent}`;
1183
+ const isCurrentPrerelease = isPrerelease(currentVersion);
1184
+ if (!isCurrentPrerelease) {
1185
+ if (releaseType === "release") {
1186
+ return handleStableVersionWithReleaseType(commits, types, force);
1040
1187
  }
1041
- const changelogResult = await executeHook("generate:changelog", config, dryRun, {
1042
- commits: pkg.commits,
1043
- changelog
1044
- });
1045
- changelog = changelogResult || changelog;
1046
- logger.verbose(`Output changelog for ${pkg.name}:
1047
- ${changelog}`);
1048
- logger.debug(`Changelog generated for ${pkg.name} (${pkg.commits.length} commits)`);
1049
- logger.verbose(`Final changelog for ${pkg.name}:
1050
-
1051
- ${changelog}
1052
-
1053
- `);
1188
+ if (releaseType === "prerelease") {
1189
+ return handleStableVersionWithPrereleaseType(commits, types, force);
1190
+ }
1191
+ return handleExplicitReleaseType({ releaseType, currentVersion });
1192
+ }
1193
+ if (releaseType === "release") {
1194
+ return handlePrereleaseVersionToStable(currentVersion);
1195
+ }
1196
+ if (releaseType === "prerelease") {
1197
+ return handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force });
1198
+ }
1199
+ return handleExplicitReleaseType({ releaseType, currentVersion });
1200
+ }
1201
+ function writeVersion(pkgPath, newVersion, dryRun = false) {
1202
+ const packageJsonPath = join(pkgPath, "package.json");
1203
+ try {
1204
+ logger.debug(`Writing ${newVersion} to ${pkgPath}`);
1205
+ const content = readFileSync(packageJsonPath, "utf8");
1206
+ const packageJson = JSON.parse(content);
1207
+ const oldVersion = packageJson.version;
1208
+ packageJson.version = newVersion;
1054
1209
  if (dryRun) {
1055
- logger.info(`[dry-run] ${pkg.name} - Generate changelog ${fromTag}...${toTag}`);
1210
+ logger.debug(`[dry-run] Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1211
+ return;
1056
1212
  }
1057
- return changelog;
1213
+ writeFileSync(packageJsonPath, `${formatJson(packageJson)}
1214
+ `, "utf8");
1215
+ logger.debug(`Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1058
1216
  } catch (error) {
1059
- throw new Error(`Error generating changelog for ${pkg.name} (${fromTag}...${toTag}): ${error}`);
1217
+ throw new Error(`Unable to write version to ${packageJsonPath}: ${error}`);
1060
1218
  }
1061
1219
  }
1062
- function writeChangelogToFile({
1063
- cwd,
1064
- pkg,
1065
- changelog,
1066
- dryRun = false
1220
+ function getPackageNewVersion({
1221
+ name,
1222
+ currentVersion,
1223
+ releaseType,
1224
+ preid,
1225
+ suffix
1067
1226
  }) {
1068
- const changelogPath = join(pkg.path, "CHANGELOG.md");
1069
- let existingChangelog = "";
1070
- if (existsSync(changelogPath)) {
1071
- existingChangelog = readFileSync(changelogPath, "utf8");
1227
+ let newVersion = semver.inc(currentVersion, releaseType, preid);
1228
+ if (!newVersion) {
1229
+ throw new Error(`Unable to bump "${name}" version "${currentVersion}" with release type "${releaseType}"
1230
+
1231
+ You should use an explicit release type (use flag: --major, --minor, --patch, --premajor, --preminor, --prepatch, --prerelease)`);
1072
1232
  }
1073
- const lines = existingChangelog.split("\n");
1074
- const titleIndex = lines.findIndex((line) => line.startsWith("# "));
1075
- let updatedChangelog;
1076
- if (titleIndex !== -1) {
1077
- const beforeTitle = lines.slice(0, titleIndex + 1);
1078
- const afterTitle = lines.slice(titleIndex + 1);
1079
- updatedChangelog = [...beforeTitle, "", changelog, "", ...afterTitle].join("\n");
1080
- } else {
1081
- const title = "# Changelog\n";
1082
- updatedChangelog = `${title}
1083
- ${changelog}
1084
- ${existingChangelog}`;
1233
+ if (isPrereleaseReleaseType(releaseType) && suffix) {
1234
+ newVersion = newVersion.replace(/\.(\d+)$/, `.${suffix}`);
1085
1235
  }
1086
- if (dryRun) {
1087
- const relativeChangelogPath = relative(cwd, changelogPath);
1088
- logger.info(`[dry-run] ${pkg.name} - Write changelog to ${relativeChangelogPath}`);
1089
- } else {
1090
- logger.debug(`Writing changelog to ${changelogPath}`);
1091
- writeFileSync(changelogPath, updatedChangelog, "utf8");
1092
- logger.info(`Changelog updated for ${pkg.name} (${"newVersion" in pkg && pkg.newVersion || pkg.version})`);
1093
- }
1094
- }
1095
-
1096
- function getDefaultConfig() {
1097
- return {
1098
- cwd: process$1.cwd(),
1099
- types: {
1100
- feat: { title: "\u{1F680} Enhancements", semver: "minor" },
1101
- perf: { title: "\u{1F525} Performance", semver: "patch" },
1102
- fix: { title: "\u{1FA79} Fixes", semver: "patch" },
1103
- refactor: { title: "\u{1F485} Refactors", semver: "patch" },
1104
- docs: { title: "\u{1F4D6} Documentation", semver: "patch" },
1105
- build: { title: "\u{1F4E6} Build", semver: "patch" },
1106
- types: { title: "\u{1F30A} Types", semver: "patch" },
1107
- chore: { title: "\u{1F3E1} Chore" },
1108
- examples: { title: "\u{1F3C0} Examples" },
1109
- test: { title: "\u2705 Tests" },
1110
- style: { title: "\u{1F3A8} Styles" },
1111
- ci: { title: "\u{1F916} CI" }
1112
- },
1113
- templates: {
1114
- commitMessage: "chore(release): bump version to {{newVersion}}",
1115
- tagMessage: "Bump version to {{newVersion}}",
1116
- tagBody: "v{{newVersion}}",
1117
- emptyChangelogContent: "No relevant changes for this release"
1118
- },
1119
- excludeAuthors: [],
1120
- noAuthors: false,
1121
- bump: {
1122
- type: "release",
1123
- clean: true,
1124
- dependencyTypes: ["dependencies"],
1125
- yes: false
1126
- },
1127
- changelog: {
1128
- rootChangelog: true,
1129
- includeCommitBody: true
1130
- },
1131
- publish: {
1132
- private: false,
1133
- args: [],
1134
- safetyCheck: false
1135
- },
1136
- tokens: {
1137
- gitlab: process$1.env.RELIZY_GITLAB_TOKEN || process$1.env.GITLAB_TOKEN || process$1.env.GITLAB_API_TOKEN || process$1.env.CI_JOB_TOKEN,
1138
- github: process$1.env.RELIZY_GITHUB_TOKEN || process$1.env.GITHUB_TOKEN || process$1.env.GH_TOKEN
1139
- },
1140
- scopeMap: {},
1141
- release: {
1142
- commit: true,
1143
- publish: true,
1144
- changelog: true,
1145
- push: true,
1146
- clean: true,
1147
- providerRelease: true,
1148
- noVerify: false,
1149
- gitTag: true
1150
- },
1151
- logLevel: "default",
1152
- safetyCheck: true
1153
- };
1154
- }
1155
- function setupLogger(logLevel) {
1156
- if (logLevel) {
1157
- logger.setLevel(logLevel);
1158
- logger.debug(`Log level set to: ${logLevel}`);
1159
- }
1160
- }
1161
- async function resolveConfig(config, cwd) {
1162
- if (!config.repo) {
1163
- const resolvedRepoConfig = await resolveRepoConfig(cwd);
1164
- config.repo = {
1165
- ...resolvedRepoConfig,
1166
- provider: resolvedRepoConfig.provider
1167
- };
1236
+ const isValidVersion = semver.gt(newVersion, currentVersion);
1237
+ if (!isValidVersion) {
1238
+ throw new Error(`Unable to bump "${name}" version "${currentVersion}" to "${newVersion}", new version is not greater than current version`);
1168
1239
  }
1169
- if (typeof config.repo === "string") {
1170
- const resolvedRepoConfig = getRepoConfig(config.repo);
1171
- config.repo = {
1172
- ...resolvedRepoConfig,
1173
- provider: resolvedRepoConfig.provider
1174
- };
1240
+ if (isGraduating(currentVersion, releaseType)) {
1241
+ logger.info(`Graduating "${name}" from prerelease ${currentVersion} to stable ${newVersion}`);
1175
1242
  }
1176
- return config;
1177
- }
1178
- async function loadRelizyConfig(options) {
1179
- const cwd = options?.overrides?.cwd ?? process$1.cwd();
1180
- await setupDotenv({ cwd });
1181
- const configFile = options?.configFile ?? "relizy";
1182
- const defaultConfig = getDefaultConfig();
1183
- const overridesConfig = defu(options?.overrides, options?.baseConfig);
1184
- const results = await loadConfig({
1185
- dotenv: true,
1186
- cwd,
1187
- name: configFile,
1188
- packageJson: true,
1189
- defaults: defaultConfig,
1190
- overrides: overridesConfig
1191
- });
1192
- if (typeof results._configFile !== "string") {
1193
- logger.debug(`No config file found with name "${configFile}"`);
1194
- if (options?.configFile) {
1195
- logger.error(`No config file found with name "${configFile}"`);
1196
- process$1.exit(1);
1197
- }
1243
+ if (isChangedPreid(currentVersion, preid)) {
1244
+ logger.debug(`Graduating "${name}" from ${getPreid(currentVersion)} to ${preid}`);
1198
1245
  }
1199
- setupLogger(options?.overrides?.logLevel || results.config.logLevel);
1200
- logger.verbose("User config:", formatJson(results.config.changelog));
1201
- const resolvedConfig = await resolveConfig(results.config, cwd);
1202
- logger.debug("Resolved config:", formatJson(resolvedConfig));
1203
- return resolvedConfig;
1204
- }
1205
- function defineConfig(config) {
1206
- return config;
1246
+ return newVersion;
1207
1247
  }
1208
-
1209
- async function githubIndependentMode({
1210
- config,
1211
- dryRun,
1212
- bumpResult,
1213
- force,
1214
- suffix
1248
+ function updateLernaVersion({
1249
+ rootDir,
1250
+ versionMode,
1251
+ version,
1252
+ dryRun = false
1215
1253
  }) {
1216
- const repoConfig = config.repo;
1217
- if (!repoConfig) {
1218
- throw new Error("No repository configuration found. Please check your changelog config.");
1254
+ const lernaJsonExists = hasLernaJson(rootDir);
1255
+ if (!lernaJsonExists) {
1256
+ return;
1219
1257
  }
1220
- logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1221
- if (!config.tokens.github && !config.repo?.token) {
1222
- throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
1258
+ const lernaJsonPath = join(rootDir, "lerna.json");
1259
+ if (!existsSync(lernaJsonPath)) {
1260
+ return;
1223
1261
  }
1224
- const packages = await getPackagesOrBumpedPackages({
1225
- config,
1226
- bumpResult,
1227
- suffix,
1228
- force
1229
- });
1230
- logger.info(`Creating ${packages.length} GitHub release(s)`);
1231
- const postedReleases = [];
1232
- for (const pkg of packages) {
1233
- const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
1234
- const from = config.from || pkg.fromTag;
1235
- const to = config.to || getIndependentTag({ version: newVersion, name: pkg.name });
1236
- if (!from) {
1237
- logger.warn(`No from tag found for ${pkg.name}, skipping release`);
1238
- continue;
1262
+ try {
1263
+ logger.debug("Updating lerna.json version");
1264
+ const content = readFileSync(lernaJsonPath, "utf8");
1265
+ const lernaJson = JSON.parse(content);
1266
+ const oldVersion = lernaJson.version;
1267
+ if (lernaJson.version === "independent" || versionMode === "independent") {
1268
+ logger.debug("Lerna version is independent or version mode is independent, skipping update");
1269
+ return;
1239
1270
  }
1240
- const toTag = dryRun ? "HEAD" : to;
1241
- logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${toTag}`);
1242
- const changelog = await generateChangelog({
1243
- pkg,
1244
- config,
1245
- dryRun,
1246
- newVersion
1247
- });
1248
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1249
- const release = {
1250
- tag_name: to,
1251
- name: to,
1252
- body: releaseBody,
1253
- prerelease: isPrerelease(newVersion)
1254
- };
1255
- logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
1271
+ lernaJson.version = version;
1256
1272
  if (dryRun) {
1257
- logger.info(`[dry-run] Publish GitHub release for ${to}`);
1258
- postedReleases.push({
1259
- name: pkg.name,
1260
- tag: release.tag_name,
1261
- version: newVersion,
1262
- prerelease: release.prerelease
1263
- });
1264
- } else {
1265
- logger.debug(`Publishing release ${to} to GitHub...`);
1266
- await createGithubRelease({
1267
- ...config,
1268
- from,
1269
- to,
1270
- repo: repoConfig
1271
- }, release);
1272
- postedReleases.push({
1273
- name: pkg.name,
1274
- tag: release.tag_name,
1275
- version: newVersion,
1276
- prerelease: release.prerelease
1277
- });
1273
+ logger.info(`[dry-run] update lerna.json: ${oldVersion} \u2192 ${version}`);
1274
+ return;
1278
1275
  }
1276
+ writeFileSync(lernaJsonPath, `${formatJson(lernaJson)}
1277
+ `, "utf8");
1278
+ logger.success(`Updated lerna.json: ${oldVersion} \u2192 ${version}`);
1279
+ } catch (error) {
1280
+ logger.fail(`Unable to update lerna.json: ${error}`);
1279
1281
  }
1280
- if (postedReleases.length === 0) {
1281
- logger.warn("No releases created");
1282
- } else {
1283
- logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitHub!`);
1282
+ }
1283
+ function extractVersionFromPackageTag(tag) {
1284
+ const atIndex = tag.lastIndexOf("@");
1285
+ if (atIndex === -1) {
1286
+ return null;
1284
1287
  }
1285
- return postedReleases;
1288
+ return tag.slice(atIndex + 1);
1286
1289
  }
1287
- async function githubUnified({
1288
- config,
1289
- dryRun,
1290
- rootPackage,
1291
- bumpResult
1292
- }) {
1293
- const repoConfig = config.repo;
1294
- if (!repoConfig) {
1295
- throw new Error("No repository configuration found. Please check your changelog config.");
1290
+ function isPrerelease(version) {
1291
+ if (!version)
1292
+ return false;
1293
+ const prerelease = semver.prerelease(version);
1294
+ return prerelease ? prerelease.length > 0 : false;
1295
+ }
1296
+ function isStableReleaseType(releaseType) {
1297
+ const stableTypes = ["release", "major", "minor", "patch"];
1298
+ return stableTypes.includes(releaseType);
1299
+ }
1300
+ function isPrereleaseReleaseType(releaseType) {
1301
+ const prereleaseTypes = ["prerelease", "premajor", "preminor", "prepatch"];
1302
+ return prereleaseTypes.includes(releaseType);
1303
+ }
1304
+ function isGraduating(currentVersion, releaseType) {
1305
+ return isPrerelease(currentVersion) && isStableReleaseType(releaseType);
1306
+ }
1307
+ function getPreid(version) {
1308
+ if (!version)
1309
+ return null;
1310
+ const prerelease = semver.prerelease(version);
1311
+ if (!prerelease || prerelease.length === 0) {
1312
+ return null;
1296
1313
  }
1297
- logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1298
- if (!config.tokens.github && !config.repo?.token) {
1299
- throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
1314
+ return prerelease[0];
1315
+ }
1316
+ function isChangedPreid(currentVersion, targetPreid) {
1317
+ if (!targetPreid || !isPrerelease(currentVersion)) {
1318
+ return false;
1300
1319
  }
1301
- const newVersion = bumpResult?.newVersion || rootPackage.version;
1302
- const to = config.to || config.templates.tagBody.replace("{{newVersion}}", newVersion);
1303
- const changelog = await generateChangelog({
1304
- pkg: rootPackage,
1305
- config,
1306
- dryRun,
1307
- newVersion
1320
+ const currentPreid = getPreid(currentVersion);
1321
+ if (!currentPreid) {
1322
+ return false;
1323
+ }
1324
+ return currentPreid !== targetPreid;
1325
+ }
1326
+ function getBumpedPackageIndependently({
1327
+ pkg,
1328
+ dryRun
1329
+ }) {
1330
+ logger.debug(`Analyzing ${pkg.name}`);
1331
+ const currentVersion = pkg.version || "0.0.0";
1332
+ const newVersion = pkg.newVersion;
1333
+ if (!newVersion) {
1334
+ return { bumped: false };
1335
+ }
1336
+ logger.debug(`Bumping ${pkg.name} from ${currentVersion} to ${newVersion}`);
1337
+ writeVersion(pkg.path, newVersion, dryRun);
1338
+ return { bumped: true, newVersion, oldVersion: currentVersion };
1339
+ }
1340
+ function displayRootAndLernaUpdates({
1341
+ versionMode,
1342
+ currentVersion,
1343
+ newVersion,
1344
+ dryRun,
1345
+ lernaJsonExists
1346
+ }) {
1347
+ if (versionMode !== "independent" && currentVersion && newVersion) {
1348
+ logger.log(`${dryRun ? "[dry-run] " : ""}Root package.json: ${currentVersion} \u2192 ${newVersion}`);
1349
+ logger.log("");
1350
+ if (lernaJsonExists) {
1351
+ logger.log(`${dryRun ? "[dry-run] " : ""}lerna.json: ${currentVersion} \u2192 ${newVersion}`);
1352
+ logger.log("");
1353
+ }
1354
+ }
1355
+ }
1356
+ function displayUnifiedModePackages({
1357
+ packages,
1358
+ newVersion,
1359
+ force
1360
+ }) {
1361
+ logger.log(`${packages.length} package(s):`);
1362
+ packages.forEach((pkg) => {
1363
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
1308
1364
  });
1309
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1310
- const release = {
1311
- tag_name: to,
1312
- name: to,
1313
- body: releaseBody,
1314
- prerelease: isPrerelease(to)
1315
- };
1316
- logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
1317
- logger.debug("Release details:", formatJson({
1318
- tag_name: release.tag_name,
1319
- name: release.name,
1320
- prerelease: release.prerelease
1321
- }));
1322
- if (dryRun) {
1323
- logger.info("[dry-run] Publish GitHub release for", release.tag_name);
1365
+ logger.log("");
1366
+ }
1367
+ function displaySelectiveModePackages({
1368
+ packages,
1369
+ newVersion,
1370
+ force
1371
+ }) {
1372
+ if (force) {
1373
+ logger.log(`${packages.length} package(s):`);
1374
+ packages.forEach((pkg) => {
1375
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (force)`);
1376
+ });
1377
+ logger.log("");
1324
1378
  } else {
1325
- logger.debug("Publishing release to GitHub...");
1326
- await createGithubRelease({
1327
- ...config,
1328
- from: bumpResult?.bumped && bumpResult.fromTag || "v0.0.0",
1329
- to,
1330
- repo: repoConfig
1331
- }, release);
1379
+ const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
1380
+ const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
1381
+ const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
1382
+ if (packagesWithCommits.length > 0) {
1383
+ logger.log(`${packagesWithCommits.length} package(s) with commits:`);
1384
+ packagesWithCommits.forEach((pkg) => {
1385
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
1386
+ });
1387
+ logger.log("");
1388
+ }
1389
+ if (packagesAsDependents.length > 0) {
1390
+ logger.log(`${packagesAsDependents.length} dependent package(s):`);
1391
+ packagesAsDependents.forEach((pkg) => {
1392
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
1393
+ });
1394
+ logger.log("");
1395
+ }
1396
+ if (packagesAsGraduation.length > 0) {
1397
+ logger.log(`${packagesAsGraduation.length} graduation package(s):`);
1398
+ packagesAsGraduation.forEach((pkg) => {
1399
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
1400
+ });
1401
+ logger.log("");
1402
+ }
1332
1403
  }
1333
- logger.success(`Release ${to} published to GitHub!`);
1334
- return [{
1335
- name: to,
1336
- tag: to,
1337
- version: to,
1338
- prerelease: release.prerelease
1339
- }];
1340
1404
  }
1341
- async function github(options) {
1342
- try {
1343
- const dryRun = options.dryRun ?? false;
1344
- logger.debug(`Dry run: ${dryRun}`);
1345
- const config = await loadRelizyConfig({
1346
- configFile: options.configName,
1347
- baseConfig: options.config,
1348
- overrides: {
1349
- from: options.from,
1350
- to: options.to,
1351
- logLevel: options.logLevel,
1352
- tokens: {
1353
- github: options.token
1354
- }
1355
- }
1405
+ function displayIndependentModePackages({
1406
+ packages,
1407
+ force
1408
+ }) {
1409
+ if (force) {
1410
+ logger.log(`${packages.length} package(s):`);
1411
+ packages.forEach((pkg) => {
1412
+ logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (force)`);
1356
1413
  });
1357
- if (config.monorepo?.versionMode === "independent") {
1358
- return await githubIndependentMode({
1359
- config,
1360
- dryRun,
1361
- bumpResult: options.bumpResult,
1362
- force: options.force ?? false,
1363
- suffix: options.suffix
1414
+ logger.log("");
1415
+ } else {
1416
+ const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
1417
+ const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
1418
+ const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
1419
+ if (packagesWithCommits.length > 0) {
1420
+ logger.log(`${packagesWithCommits.length} package(s) with commits:`);
1421
+ packagesWithCommits.forEach((pkg) => {
1422
+ pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
1364
1423
  });
1424
+ logger.log("");
1365
1425
  }
1366
- const rootPackageBase = readPackageJson(config.cwd);
1367
- if (!rootPackageBase) {
1368
- throw new Error("Failed to read root package.json");
1426
+ if (packagesAsDependents.length > 0) {
1427
+ logger.log(`${packagesAsDependents.length} dependent package(s):`);
1428
+ packagesAsDependents.forEach((pkg) => {
1429
+ pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
1430
+ });
1431
+ logger.log("");
1432
+ }
1433
+ if (packagesAsGraduation.length > 0) {
1434
+ logger.log(`${packagesAsGraduation.length} graduation package(s):`);
1435
+ packagesAsGraduation.forEach((pkg) => {
1436
+ pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
1437
+ });
1438
+ logger.log("");
1369
1439
  }
1370
- const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
1371
- const { from, to } = await resolveTags({
1372
- config,
1373
- step: "provider-release",
1374
- newVersion,
1375
- pkg: rootPackageBase
1376
- });
1377
- const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
1378
- config,
1379
- force: options.force ?? false,
1380
- suffix: options.suffix,
1381
- changelog: true,
1382
- from,
1383
- to
1384
- });
1385
- return await githubUnified({
1386
- config,
1387
- dryRun,
1388
- rootPackage,
1389
- bumpResult: options.bumpResult
1390
- });
1391
- } catch (error) {
1392
- logger.error("Error publishing GitHub release:", error);
1393
- throw error;
1394
1440
  }
1395
1441
  }
1396
-
1397
- async function createGitlabRelease({
1442
+ async function confirmBump({
1443
+ versionMode,
1398
1444
  config,
1399
- release,
1445
+ packages,
1446
+ force,
1447
+ currentVersion,
1448
+ newVersion,
1400
1449
  dryRun
1401
1450
  }) {
1402
- const token = config.tokens.gitlab || config.repo?.token;
1403
- if (!token && !dryRun) {
1404
- throw new Error(
1405
- "No GitLab token found. Set GITLAB_TOKEN or CI_JOB_TOKEN environment variable or configure tokens.gitlab"
1406
- );
1451
+ if (packages.length === 0) {
1452
+ logger.debug("No packages to bump");
1453
+ return;
1407
1454
  }
1408
- const repoConfig = config.repo?.repo;
1409
- if (!repoConfig) {
1410
- throw new Error("No repository URL found in config");
1455
+ const lernaJsonExists = hasLernaJson(config.cwd);
1456
+ logger.log("");
1457
+ logger.info(`${dryRun ? "[dry-run] " : ""}The following packages will be updated:
1458
+ `);
1459
+ displayRootAndLernaUpdates({
1460
+ versionMode,
1461
+ currentVersion,
1462
+ newVersion,
1463
+ lernaJsonExists,
1464
+ dryRun
1465
+ });
1466
+ if (versionMode === "unified") {
1467
+ if (!newVersion) {
1468
+ throw new Error("Cannot confirm bump in unified mode without a new version");
1469
+ }
1470
+ displayUnifiedModePackages({ packages, newVersion, force });
1471
+ } else if (versionMode === "selective") {
1472
+ if (!newVersion) {
1473
+ throw new Error("Cannot confirm bump in selective mode without a new version");
1474
+ }
1475
+ displaySelectiveModePackages({ packages, newVersion, force });
1476
+ } else if (versionMode === "independent") {
1477
+ displayIndependentModePackages({ packages, force });
1411
1478
  }
1412
- logger.debug(`Parsed repository URL: ${repoConfig}`);
1413
- const projectPath = encodeURIComponent(repoConfig);
1414
- const gitlabDomain = config.repo?.domain || "gitlab.com";
1415
- const apiUrl = `https://${gitlabDomain}/api/v4/projects/${projectPath}/releases`;
1416
- logger.info(`Creating GitLab release at: ${apiUrl}`);
1417
- const payload = {
1418
- tag_name: release.tag_name,
1419
- name: release.name || release.tag_name,
1420
- description: release.description || "",
1421
- ref: release.ref || "main"
1422
- };
1423
1479
  try {
1424
- if (dryRun) {
1425
- logger.info("[dry-run] GitLab release:", formatJson(payload));
1426
- return {
1427
- tag_name: release.tag_name,
1428
- name: release.name || release.tag_name,
1429
- description: release.description || "",
1430
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1431
- released_at: (/* @__PURE__ */ new Date()).toISOString(),
1432
- _links: {
1433
- self: `${apiUrl}/${encodeURIComponent(release.tag_name)}`
1434
- }
1435
- };
1436
- }
1437
- logger.debug(`POST GitLab release to ${apiUrl} with payload: ${formatJson(payload)}`);
1438
- const response = await fetch(apiUrl, {
1439
- method: "POST",
1440
- headers: {
1441
- "Content-Type": "application/json",
1442
- "PRIVATE-TOKEN": token || ""
1443
- },
1444
- body: JSON.stringify(payload)
1480
+ const confirmed = await confirm({
1481
+ message: `${dryRun ? "[dry-run] " : ""}Do you want to proceed with these version updates?`,
1482
+ default: true
1445
1483
  });
1446
- if (!response.ok) {
1447
- const errorText = await response.text();
1448
- throw new Error(`GitLab API error (${response.status}): ${errorText}`);
1484
+ if (!confirmed) {
1485
+ logger.log("");
1486
+ logger.fail("Bump refused");
1487
+ process.exit(0);
1449
1488
  }
1450
- const result = await response.json();
1451
- logger.debug(`Created GitLab release: ${result._links.self}`);
1452
- return result;
1453
1489
  } catch (error) {
1454
- logger.error("Failed to create GitLab release:", error);
1455
- throw error;
1490
+ const userHasExited = error instanceof Error && error.name === "ExitPromptError";
1491
+ if (userHasExited) {
1492
+ logger.log("");
1493
+ logger.fail("Bump cancelled");
1494
+ process.exit(0);
1495
+ }
1496
+ logger.fail("Error while confirming bump");
1497
+ process.exit(1);
1456
1498
  }
1499
+ logger.log("");
1457
1500
  }
1458
- async function gitlabIndependentMode({
1459
- config,
1460
- dryRun,
1461
- bumpResult,
1462
- suffix,
1463
- force
1501
+ function getBumpedIndependentPackages({
1502
+ packages,
1503
+ dryRun
1464
1504
  }) {
1465
- logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1466
- const packages = await getPackagesOrBumpedPackages({
1467
- config,
1468
- bumpResult,
1469
- suffix,
1470
- force
1471
- });
1472
- logger.info(`Creating ${packages.length} GitLab release(s) for independent packages`);
1473
- logger.debug("Getting current branch...");
1474
- const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
1475
- noSuccess: true,
1476
- noStdout: true,
1477
- logLevel: config.logLevel,
1478
- cwd: config.cwd
1479
- });
1480
- const postedReleases = [];
1481
- for (const pkg of packages) {
1482
- const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
1483
- const from = config.from || pkg.fromTag;
1484
- const to = getIndependentTag({ version: newVersion, name: pkg.name });
1485
- if (!from) {
1486
- logger.warn(`No from tag found for ${pkg.name}, skipping release`);
1487
- continue;
1488
- }
1489
- logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${to}`);
1490
- const changelog = await generateChangelog({
1491
- pkg,
1492
- config,
1493
- dryRun,
1494
- newVersion
1505
+ const bumpedPackages = [];
1506
+ for (const pkgToBump of packages) {
1507
+ logger.debug(`Bumping ${pkgToBump.name} from ${pkgToBump.version} to ${pkgToBump.newVersion} (reason: ${pkgToBump.reason})`);
1508
+ const result = getBumpedPackageIndependently({
1509
+ pkg: pkgToBump,
1510
+ dryRun
1495
1511
  });
1496
- if (!changelog) {
1497
- logger.warn(`No changelog found for ${pkg.name}`);
1498
- continue;
1499
- }
1500
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1501
- const release = {
1502
- tag_name: to,
1503
- name: to,
1504
- description: releaseBody,
1505
- ref: currentBranch.trim()
1506
- };
1507
- logger.debug(`Creating release for ${to} (ref: ${release.ref})`);
1508
- if (dryRun) {
1509
- logger.info(`[dry-run] Publish GitLab release for ${to}`);
1510
- } else {
1511
- logger.debug(`Publishing release ${to} to GitLab...`);
1512
- await createGitlabRelease({
1513
- config,
1514
- release,
1515
- dryRun
1516
- });
1517
- postedReleases.push({
1518
- name: pkg.name,
1519
- tag: release.tag_name,
1520
- version: newVersion,
1521
- prerelease: isPrerelease(newVersion)
1512
+ if (result.bumped) {
1513
+ bumpedPackages.push({
1514
+ ...pkgToBump,
1515
+ version: result.oldVersion
1522
1516
  });
1523
1517
  }
1524
1518
  }
1525
- if (postedReleases.length === 0) {
1526
- logger.warn("No releases created");
1527
- } else {
1528
- logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitLab!`);
1519
+ return bumpedPackages;
1520
+ }
1521
+ function shouldFilterPrereleaseTags(currentVersion, graduating) {
1522
+ return !isPrerelease(currentVersion) && !graduating;
1523
+ }
1524
+ function extractVersionFromTag(tag, packageName) {
1525
+ if (!tag) {
1526
+ return null;
1529
1527
  }
1530
- return postedReleases;
1528
+ if (packageName) {
1529
+ const prefix = `${packageName}@`;
1530
+ if (tag.startsWith(prefix)) {
1531
+ return tag.slice(prefix.length);
1532
+ }
1533
+ }
1534
+ const atIndex = tag.lastIndexOf("@");
1535
+ if (atIndex !== -1) {
1536
+ return tag.slice(atIndex + 1);
1537
+ }
1538
+ if (tag.startsWith("v") && /^v\d/.test(tag)) {
1539
+ return tag.slice(1);
1540
+ }
1541
+ if (/^\d+\.\d+\.\d+/.test(tag)) {
1542
+ return tag;
1543
+ }
1544
+ return null;
1531
1545
  }
1532
- async function gitlabUnified({
1533
- config,
1534
- dryRun,
1535
- rootPackage,
1536
- bumpResult
1537
- }) {
1538
- logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1539
- const newVersion = bumpResult?.newVersion || rootPackage.newVersion || rootPackage.version;
1540
- const to = config.templates.tagBody.replace("{{newVersion}}", newVersion);
1541
- const changelog = await generateChangelog({
1542
- pkg: rootPackage,
1543
- config,
1544
- dryRun,
1545
- newVersion
1546
- });
1547
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1548
- logger.debug("Getting current branch...");
1549
- const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
1550
- noSuccess: true,
1546
+ function isTagVersionCompatibleWithCurrent(tagVersion, currentVersion) {
1547
+ try {
1548
+ const tagMajor = semver.major(tagVersion);
1549
+ const currentMajor = semver.major(currentVersion);
1550
+ return tagMajor <= currentMajor;
1551
+ } catch {
1552
+ return false;
1553
+ }
1554
+ }
1555
+
1556
+ function getIndependentTag({ version, name }) {
1557
+ return `${name}@${version}`;
1558
+ }
1559
+ async function getLastStableTag({ logLevel, cwd }) {
1560
+ const { stdout } = await execPromise(
1561
+ `git tag --sort=-creatordate | grep -E '^[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+$' | head -n 1`,
1562
+ {
1563
+ logLevel,
1564
+ noStderr: true,
1565
+ noStdout: true,
1566
+ noSuccess: true,
1567
+ cwd
1568
+ }
1569
+ );
1570
+ const lastTag = stdout.trim();
1571
+ logger.debug("Last stable tag:", lastTag || "No stable tags found");
1572
+ return lastTag;
1573
+ }
1574
+ async function getLastTag({ logLevel, cwd }) {
1575
+ const { stdout } = await execPromise(`git tag --sort=-creatordate | head -n 1`, {
1576
+ logLevel,
1577
+ noStderr: true,
1551
1578
  noStdout: true,
1552
- logLevel: config.logLevel,
1553
- cwd: config.cwd
1579
+ noSuccess: true,
1580
+ cwd
1554
1581
  });
1555
- const release = {
1556
- tag_name: to,
1557
- name: to,
1558
- description: releaseBody,
1559
- ref: currentBranch.trim()
1560
- };
1561
- logger.info(`Creating release for ${to} (ref: ${release.ref})`);
1562
- logger.debug("Release details:", formatJson({
1563
- tag_name: release.tag_name,
1564
- name: release.name,
1565
- ref: release.ref
1566
- }));
1567
- if (dryRun) {
1568
- logger.info("[dry-run] Publish GitLab release for", release.tag_name);
1569
- } else {
1570
- logger.debug("Publishing release to GitLab...");
1571
- await createGitlabRelease({
1572
- config,
1573
- release,
1574
- dryRun
1575
- });
1576
- }
1577
- logger.success(`Release ${to} published to GitLab!`);
1578
- return [{
1579
- name: to,
1580
- tag: to,
1581
- version: to,
1582
- prerelease: isPrerelease(newVersion)
1583
- }];
1582
+ const lastTag = stdout.trim();
1583
+ logger.debug("Last tag:", lastTag || "No tags found");
1584
+ return lastTag;
1584
1585
  }
1585
- async function gitlab(options = {}) {
1586
+ async function getAllRecentRepoTags(options) {
1587
+ const limit = options?.limit;
1586
1588
  try {
1587
- const dryRun = options.dryRun ?? false;
1588
- logger.debug(`Dry run: ${dryRun}`);
1589
- const config = await loadRelizyConfig({
1590
- configFile: options.configName,
1591
- baseConfig: options.config,
1592
- overrides: {
1593
- from: options.from,
1594
- to: options.to,
1595
- logLevel: options.logLevel,
1596
- tokens: {
1597
- gitlab: options.token
1598
- }
1599
- }
1600
- });
1601
- if (config.monorepo?.versionMode === "independent") {
1602
- return await gitlabIndependentMode({
1603
- config,
1604
- dryRun,
1605
- bumpResult: options.bumpResult,
1606
- suffix: options.suffix,
1607
- force: options.force ?? false
1608
- });
1609
- }
1610
- const rootPackageBase = readPackageJson(config.cwd);
1611
- if (!rootPackageBase) {
1612
- throw new Error("Failed to read root package.json");
1613
- }
1614
- const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
1615
- const { from, to } = await resolveTags({
1616
- config,
1617
- step: "provider-release",
1618
- newVersion,
1619
- pkg: rootPackageBase
1620
- });
1621
- const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
1622
- config,
1623
- force: options.force ?? false,
1624
- suffix: options.suffix,
1625
- changelog: true,
1626
- from,
1627
- to
1628
- });
1629
- logger.debug(`Root package: ${getIndependentTag({ name: rootPackage.name, version: newVersion })}`);
1630
- return await gitlabUnified({
1631
- config,
1632
- dryRun,
1633
- rootPackage,
1634
- bumpResult: options.bumpResult
1635
- });
1636
- } catch (error) {
1637
- logger.error("Error publishing GitLab release:", error);
1638
- throw error;
1589
+ const { stdout } = await execPromise(
1590
+ `git tag --sort=-creatordate | head -n ${limit}`,
1591
+ {
1592
+ logLevel: options?.logLevel,
1593
+ noStderr: true,
1594
+ noStdout: true,
1595
+ noSuccess: true,
1596
+ cwd: options?.cwd
1597
+ }
1598
+ );
1599
+ const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
1600
+ logger.debug(`Retrieved ${tags.length} recent repo tags`);
1601
+ return tags;
1602
+ } catch {
1603
+ return [];
1639
1604
  }
1640
1605
  }
1641
-
1642
- function isGraduatingToStableBetweenVersion(version, newVersion) {
1643
- const isSameBase = semver.major(version) === semver.major(newVersion) && semver.minor(version) === semver.minor(newVersion) && semver.patch(version) === semver.patch(newVersion);
1644
- const fromPrerelease = semver.prerelease(version) !== null;
1645
- const toStable = semver.prerelease(newVersion) === null;
1646
- return isSameBase && fromPrerelease && toStable;
1606
+ async function getAllRecentPackageTags({
1607
+ packageName,
1608
+ limit = 50,
1609
+ logLevel,
1610
+ cwd
1611
+ }) {
1612
+ try {
1613
+ const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
1614
+ const { stdout } = await execPromise(
1615
+ `git tag --sort=-creatordate | grep -E '^${escapedPackageName}@' | head -n ${limit}`,
1616
+ {
1617
+ logLevel,
1618
+ noStderr: true,
1619
+ noStdout: true,
1620
+ noSuccess: true,
1621
+ cwd
1622
+ }
1623
+ );
1624
+ const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
1625
+ logger.debug(`Retrieved ${tags.length} recent tags for package ${packageName}`);
1626
+ return tags;
1627
+ } catch {
1628
+ return [];
1629
+ }
1647
1630
  }
1648
- function determineSemverChange(commits, types) {
1649
- let [hasMajor, hasMinor, hasPatch] = [false, false, false];
1650
- for (const commit of commits) {
1651
- const commitType = types[commit.type];
1652
- if (!commitType) {
1653
- continue;
1631
+ function filterCompatibleTags({
1632
+ tags,
1633
+ currentVersion,
1634
+ onlyStable,
1635
+ packageName
1636
+ }) {
1637
+ const filtered = tags.filter((tag) => {
1638
+ const tagVersion = extractVersionFromTag(tag, packageName);
1639
+ if (!tagVersion) {
1640
+ logger.debug(`Skipping tag ${tag}: cannot extract version`);
1641
+ return false;
1654
1642
  }
1655
- const semverType = commitType.semver;
1656
- if (semverType === "major" || commit.isBreaking) {
1657
- hasMajor = true;
1658
- } else if (semverType === "minor") {
1659
- hasMinor = true;
1660
- } else if (semverType === "patch") {
1661
- hasPatch = true;
1643
+ if (onlyStable && isPrerelease(tagVersion)) {
1644
+ logger.debug(`Skipping tag ${tag}: prerelease version ${tagVersion} (onlyStable=${onlyStable})`);
1645
+ return false;
1662
1646
  }
1663
- }
1664
- return hasMajor ? "major" : hasMinor ? "minor" : hasPatch ? "patch" : void 0;
1665
- }
1666
- function detectReleaseTypeFromCommits(commits, types) {
1667
- return determineSemverChange(commits, types);
1647
+ if (!isTagVersionCompatibleWithCurrent(tagVersion, currentVersion)) {
1648
+ logger.debug(`Skipping tag ${tag}: version ${tagVersion} has higher major than current ${currentVersion}`);
1649
+ return false;
1650
+ }
1651
+ logger.debug(`Tag ${tag} with version ${tagVersion} is compatible`);
1652
+ return true;
1653
+ });
1654
+ logger.debug(`Filtered ${tags.length} tags down to ${filtered.length} compatible tags`);
1655
+ return filtered;
1668
1656
  }
1669
- function validatePrereleaseDowngrade(currentVersion, targetPreid, configuredType) {
1670
- if (configuredType !== "prerelease" || !targetPreid || !isPrerelease(currentVersion)) {
1671
- return;
1657
+ function getLastRepoTag(options) {
1658
+ if (options?.currentVersion) {
1659
+ return getLastRepoTagWithFiltering({
1660
+ currentVersion: options.currentVersion,
1661
+ onlyStable: options.onlyStable ?? false,
1662
+ logLevel: options.logLevel,
1663
+ cwd: options.cwd
1664
+ });
1672
1665
  }
1673
- const testVersion = semver.inc(currentVersion, "prerelease", targetPreid);
1674
- const isNotUpgrade = testVersion && !semver.gt(testVersion, currentVersion);
1675
- if (isNotUpgrade) {
1676
- throw new Error(`Unable to graduate from ${currentVersion} to ${testVersion}, it's not a valid prerelease`);
1666
+ if (options?.onlyStable) {
1667
+ return getLastStableTag({ logLevel: options?.logLevel, cwd: options?.cwd });
1677
1668
  }
1669
+ return getLastTag({ logLevel: options?.logLevel, cwd: options?.cwd });
1678
1670
  }
1679
- function handleStableVersionWithReleaseType(commits, types, force) {
1680
- if (!commits?.length && !force) {
1681
- logger.debug('No commits found for stable version with "release" type, skipping bump');
1682
- return void 0;
1671
+ async function getLastRepoTagWithFiltering({
1672
+ currentVersion,
1673
+ onlyStable,
1674
+ logLevel,
1675
+ cwd
1676
+ }) {
1677
+ const recentTags = await getAllRecentRepoTags({ limit: 50, logLevel, cwd });
1678
+ if (recentTags.length === 0) {
1679
+ logger.debug("No tags found in repository");
1680
+ return null;
1683
1681
  }
1684
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1685
- if (!detectedType && !force) {
1686
- logger.debug("No significant commits found, skipping bump");
1687
- return void 0;
1682
+ const compatibleTags = filterCompatibleTags({
1683
+ tags: recentTags,
1684
+ currentVersion,
1685
+ onlyStable
1686
+ });
1687
+ if (compatibleTags.length === 0) {
1688
+ logger.debug("No compatible tags found");
1689
+ return null;
1688
1690
  }
1689
- logger.debug(`Auto-detected release type from commits: ${detectedType}`);
1690
- return detectedType;
1691
+ const lastTag = compatibleTags[0];
1692
+ logger.debug(`Last compatible repo tag: ${lastTag}`);
1693
+ return lastTag;
1691
1694
  }
1692
- function handleStableVersionWithPrereleaseType(commits, types, force) {
1693
- if (!commits?.length && !force) {
1694
- logger.debug('No commits found for stable version with "prerelease" type, skipping bump');
1695
- return void 0;
1696
- }
1697
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1698
- if (!detectedType) {
1699
- logger.debug("No significant commits found, using prepatch as default");
1700
- return "prepatch";
1695
+ async function getLastPackageTag({
1696
+ packageName,
1697
+ onlyStable,
1698
+ currentVersion,
1699
+ logLevel,
1700
+ cwd
1701
+ }) {
1702
+ if (currentVersion) {
1703
+ return getLastPackageTagWithFiltering({
1704
+ packageName,
1705
+ currentVersion,
1706
+ onlyStable: onlyStable ?? false,
1707
+ logLevel,
1708
+ cwd
1709
+ });
1701
1710
  }
1702
- const prereleaseType = `pre${detectedType}`;
1703
- logger.debug(`Auto-detected prerelease type from commits: ${prereleaseType}`);
1704
- return prereleaseType;
1705
- }
1706
- function handlePrereleaseVersionToStable(currentVersion) {
1707
- logger.debug(`Graduating from prerelease ${currentVersion} to stable release`);
1708
- return "release";
1709
- }
1710
- function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force }) {
1711
- const currentPreid = getPreid(currentVersion);
1712
- const hasChangedPreid = preid && currentPreid && currentPreid !== preid;
1713
- if (hasChangedPreid) {
1714
- const testVersion = semver.inc(currentVersion, "prerelease", preid);
1715
- if (!testVersion) {
1716
- throw new Error(`Unable to change preid from ${currentPreid} to ${preid} for version ${currentVersion}`);
1717
- }
1718
- const isUpgrade = semver.gt(testVersion, currentVersion);
1719
- if (!isUpgrade) {
1720
- throw new Error(`Unable to change preid from ${currentVersion} to ${testVersion}, it's not a valid upgrade (cannot downgrade from ${currentPreid} to ${preid})`);
1711
+ try {
1712
+ const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
1713
+ let grepPattern;
1714
+ if (onlyStable) {
1715
+ grepPattern = `^${escapedPackageName}@[0-9]+\\.[0-9]+\\.[0-9]+$`;
1716
+ } else {
1717
+ grepPattern = `^${escapedPackageName}@`;
1721
1718
  }
1722
- return "prerelease";
1723
- }
1724
- if (!commits?.length && !force) {
1725
- logger.debug("No commits found for prerelease version, skipping bump");
1726
- return void 0;
1719
+ const { stdout } = await execPromise(
1720
+ `git tag --sort=-creatordate | grep -E '${grepPattern}' | sed -n '1p'`,
1721
+ {
1722
+ logLevel,
1723
+ noStderr: true,
1724
+ noStdout: true,
1725
+ noSuccess: true,
1726
+ cwd
1727
+ }
1728
+ );
1729
+ const tag = stdout.trim();
1730
+ return tag || null;
1731
+ } catch {
1732
+ return null;
1727
1733
  }
1728
- logger.debug(`Incrementing prerelease version: ${currentVersion}`);
1729
- return "prerelease";
1730
1734
  }
1731
- function handleExplicitReleaseType({
1732
- releaseType,
1733
- currentVersion
1735
+ async function getLastPackageTagWithFiltering({
1736
+ packageName,
1737
+ currentVersion,
1738
+ onlyStable,
1739
+ logLevel,
1740
+ cwd
1734
1741
  }) {
1735
- const isCurrentPrerelease = isPrerelease(currentVersion);
1736
- const isGraduatingToStable = isCurrentPrerelease && isStableReleaseType(releaseType);
1737
- if (isGraduatingToStable) {
1738
- logger.debug(`Graduating from prerelease ${currentVersion} to stable with type: ${releaseType}`);
1739
- } else {
1740
- logger.debug(`Using explicit release type: ${releaseType}`);
1742
+ const recentTags = await getAllRecentPackageTags({
1743
+ packageName,
1744
+ limit: 50,
1745
+ logLevel,
1746
+ cwd
1747
+ });
1748
+ if (recentTags.length === 0) {
1749
+ logger.debug(`No tags found for package ${packageName}`);
1750
+ return null;
1741
1751
  }
1742
- return releaseType;
1752
+ const compatibleTags = filterCompatibleTags({
1753
+ tags: recentTags,
1754
+ currentVersion,
1755
+ onlyStable,
1756
+ packageName
1757
+ });
1758
+ if (compatibleTags.length === 0) {
1759
+ logger.debug(`No compatible tags found for package ${packageName}`);
1760
+ return null;
1761
+ }
1762
+ const lastTag = compatibleTags[0];
1763
+ logger.debug(`Last compatible package tag for ${packageName}: ${lastTag}`);
1764
+ return lastTag;
1743
1765
  }
1744
- function determineReleaseType({
1766
+ async function resolveFromTagIndependent({
1767
+ cwd,
1768
+ packageName,
1745
1769
  currentVersion,
1746
- commits,
1747
- releaseType,
1748
- preid,
1749
- types,
1750
- force
1770
+ graduating,
1771
+ logLevel
1751
1772
  }) {
1752
- if (releaseType === "release" && preid) {
1753
- throw new Error('You cannot use a "release" type with a "preid", to use a preid you must use a "prerelease" type');
1754
- }
1755
- validatePrereleaseDowngrade(currentVersion, preid, releaseType);
1756
- if (force) {
1757
- logger.debug(`Force flag enabled, using configured type: ${releaseType}`);
1758
- return releaseType;
1759
- }
1760
- const isCurrentPrerelease = isPrerelease(currentVersion);
1761
- if (!isCurrentPrerelease) {
1762
- if (releaseType === "release") {
1763
- return handleStableVersionWithReleaseType(commits, types, force);
1764
- }
1765
- if (releaseType === "prerelease") {
1766
- return handleStableVersionWithPrereleaseType(commits, types, force);
1767
- }
1768
- return handleExplicitReleaseType({ releaseType, currentVersion });
1769
- }
1770
- if (releaseType === "release") {
1771
- return handlePrereleaseVersionToStable(currentVersion);
1772
- }
1773
- if (releaseType === "prerelease") {
1774
- return handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force });
1773
+ const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
1774
+ const onlyStable = graduating || filterPrereleases;
1775
+ const lastPackageTag = await getLastPackageTag({
1776
+ packageName,
1777
+ currentVersion,
1778
+ onlyStable,
1779
+ logLevel,
1780
+ cwd
1781
+ });
1782
+ if (!lastPackageTag) {
1783
+ return getFirstCommit(cwd);
1775
1784
  }
1776
- return handleExplicitReleaseType({ releaseType, currentVersion });
1785
+ return lastPackageTag;
1777
1786
  }
1778
- function writeVersion(pkgPath, newVersion, dryRun = false) {
1779
- const packageJsonPath = join(pkgPath, "package.json");
1780
- try {
1781
- logger.debug(`Writing ${newVersion} to ${pkgPath}`);
1782
- const content = readFileSync(packageJsonPath, "utf8");
1783
- const packageJson = JSON.parse(content);
1784
- const oldVersion = packageJson.version;
1785
- packageJson.version = newVersion;
1786
- if (dryRun) {
1787
- logger.info(`[dry-run] Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1788
- return;
1789
- }
1790
- writeFileSync(packageJsonPath, `${formatJson(packageJson)}
1791
- `, "utf8");
1792
- logger.info(`Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1793
- } catch (error) {
1794
- throw new Error(`Unable to write version to ${packageJsonPath}: ${error}`);
1795
- }
1787
+ async function resolveFromTagUnified({
1788
+ config,
1789
+ currentVersion,
1790
+ graduating,
1791
+ logLevel
1792
+ }) {
1793
+ const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
1794
+ const onlyStable = graduating || filterPrereleases;
1795
+ const from = await getLastRepoTag({
1796
+ currentVersion,
1797
+ onlyStable,
1798
+ logLevel,
1799
+ cwd: config.cwd
1800
+ }) || getFirstCommit(config.cwd);
1801
+ return from;
1796
1802
  }
1797
- function getPackageNewVersion({
1798
- name,
1803
+ async function resolveFromTag({
1804
+ config,
1805
+ versionMode,
1806
+ step,
1807
+ packageName,
1799
1808
  currentVersion,
1800
- releaseType,
1801
- preid,
1802
- suffix
1809
+ graduating,
1810
+ logLevel
1803
1811
  }) {
1804
- let newVersion = semver.inc(currentVersion, releaseType, preid);
1805
- if (!newVersion) {
1806
- throw new Error(`Unable to bump "${name}" version "${currentVersion}" with release type "${releaseType}"
1807
-
1808
- You should use an explicit release type (use flag: --major, --minor, --patch, --premajor, --preminor, --prepatch, --prerelease)`);
1809
- }
1810
- if (isPrereleaseReleaseType(releaseType) && suffix) {
1811
- newVersion = newVersion.replace(/\.(\d+)$/, `.${suffix}`);
1812
- }
1813
- const isValidVersion = semver.gt(newVersion, currentVersion);
1814
- if (!isValidVersion) {
1815
- throw new Error(`Unable to bump "${name}" version "${currentVersion}" to "${newVersion}", new version is not greater than current version`);
1816
- }
1817
- if (isGraduating(currentVersion, releaseType)) {
1818
- logger.info(`Graduating "${name}" from prerelease ${currentVersion} to stable ${newVersion}`);
1819
- }
1820
- if (isChangedPreid(currentVersion, preid)) {
1821
- logger.debug(`Graduating "${name}" from ${getPreid(currentVersion)} to ${preid}`);
1812
+ let from;
1813
+ if (versionMode === "independent") {
1814
+ if (!packageName) {
1815
+ throw new Error("Package name is required for independent version mode");
1816
+ }
1817
+ from = await resolveFromTagIndependent({
1818
+ cwd: config.cwd,
1819
+ packageName,
1820
+ currentVersion,
1821
+ graduating,
1822
+ logLevel
1823
+ });
1824
+ } else {
1825
+ from = await resolveFromTagUnified({
1826
+ config,
1827
+ currentVersion,
1828
+ graduating,
1829
+ logLevel
1830
+ });
1822
1831
  }
1823
- return newVersion;
1832
+ logger.debug(`[${versionMode}](${step}) Using from tag: ${from}`);
1833
+ return config.from || from;
1824
1834
  }
1825
- function updateLernaVersion({
1826
- rootDir,
1835
+ function resolveToTag({
1836
+ config,
1827
1837
  versionMode,
1828
- version,
1829
- dryRun = false
1838
+ newVersion,
1839
+ step,
1840
+ packageName
1830
1841
  }) {
1831
- const lernaJsonExists = hasLernaJson(rootDir);
1832
- if (!lernaJsonExists) {
1833
- return;
1834
- }
1835
- const lernaJsonPath = join(rootDir, "lerna.json");
1836
- if (!existsSync(lernaJsonPath)) {
1837
- return;
1838
- }
1839
- try {
1840
- logger.debug("Updating lerna.json version");
1841
- const content = readFileSync(lernaJsonPath, "utf8");
1842
- const lernaJson = JSON.parse(content);
1843
- const oldVersion = lernaJson.version;
1844
- if (lernaJson.version === "independent" || versionMode === "independent") {
1845
- logger.debug("Lerna version is independent or version mode is independent, skipping update");
1846
- return;
1842
+ const isUntaggedStep = step === "bump" || step === "changelog";
1843
+ let to;
1844
+ if (isUntaggedStep) {
1845
+ to = getCurrentGitRef(config.cwd);
1846
+ } else if (versionMode === "independent") {
1847
+ if (!packageName) {
1848
+ throw new Error("Package name is required for independent version mode");
1847
1849
  }
1848
- lernaJson.version = version;
1849
- if (dryRun) {
1850
- logger.info(`[dry-run] update lerna.json: ${oldVersion} \u2192 ${version}`);
1851
- return;
1850
+ if (!newVersion) {
1851
+ throw new Error("New version is required for independent version mode");
1852
1852
  }
1853
- writeFileSync(lernaJsonPath, `${formatJson(lernaJson)}
1854
- `, "utf8");
1855
- logger.success(`Updated lerna.json: ${oldVersion} \u2192 ${version}`);
1856
- } catch (error) {
1857
- logger.fail(`Unable to update lerna.json: ${error}`);
1858
- }
1859
- }
1860
- function extractVersionFromPackageTag(tag) {
1861
- const atIndex = tag.lastIndexOf("@");
1862
- if (atIndex === -1) {
1863
- return null;
1864
- }
1865
- return tag.slice(atIndex + 1);
1866
- }
1867
- function isPrerelease(version) {
1868
- if (!version)
1869
- return false;
1870
- const prerelease = semver.prerelease(version);
1871
- return prerelease ? prerelease.length > 0 : false;
1872
- }
1873
- function isStableReleaseType(releaseType) {
1874
- const stableTypes = ["release", "major", "minor", "patch"];
1875
- return stableTypes.includes(releaseType);
1876
- }
1877
- function isPrereleaseReleaseType(releaseType) {
1878
- const prereleaseTypes = ["prerelease", "premajor", "preminor", "prepatch"];
1879
- return prereleaseTypes.includes(releaseType);
1880
- }
1881
- function isGraduating(currentVersion, releaseType) {
1882
- return isPrerelease(currentVersion) && isStableReleaseType(releaseType);
1883
- }
1884
- function getPreid(version) {
1885
- if (!version)
1886
- return null;
1887
- const prerelease = semver.prerelease(version);
1888
- if (!prerelease || prerelease.length === 0) {
1889
- return null;
1890
- }
1891
- return prerelease[0];
1892
- }
1893
- function isChangedPreid(currentVersion, targetPreid) {
1894
- if (!targetPreid || !isPrerelease(currentVersion)) {
1895
- return false;
1896
- }
1897
- const currentPreid = getPreid(currentVersion);
1898
- if (!currentPreid) {
1899
- return false;
1853
+ to = getIndependentTag({ version: newVersion, name: packageName });
1854
+ } else {
1855
+ to = newVersion ? config.templates.tagBody.replace("{{newVersion}}", newVersion) : getCurrentGitRef(config.cwd);
1900
1856
  }
1901
- return currentPreid !== targetPreid;
1857
+ logger.debug(`[${versionMode}](${step}) Using to tag: ${to}`);
1858
+ return config.to || to;
1902
1859
  }
1903
- function getBumpedPackageIndependently({
1860
+ async function resolveTags({
1861
+ config,
1862
+ step,
1904
1863
  pkg,
1905
- dryRun
1864
+ newVersion
1906
1865
  }) {
1907
- logger.debug(`Analyzing ${pkg.name}`);
1908
- const currentVersion = pkg.version || "0.0.0";
1909
- const newVersion = pkg.newVersion;
1910
- if (!newVersion) {
1911
- return { bumped: false };
1912
- }
1913
- logger.debug(`Bumping ${pkg.name} from ${currentVersion} to ${newVersion}`);
1914
- writeVersion(pkg.path, newVersion, dryRun);
1915
- return { bumped: true, newVersion, oldVersion: currentVersion };
1916
- }
1917
- function displayRootAndLernaUpdates({
1918
- versionMode,
1919
- currentVersion,
1920
- newVersion,
1921
- dryRun,
1922
- lernaJsonExists
1923
- }) {
1924
- if (versionMode !== "independent" && currentVersion && newVersion) {
1925
- logger.log(`${dryRun ? "[dry-run] " : ""}Root package.json: ${currentVersion} \u2192 ${newVersion}`);
1926
- logger.log("");
1927
- if (lernaJsonExists) {
1928
- logger.log(`${dryRun ? "[dry-run] " : ""}lerna.json: ${currentVersion} \u2192 ${newVersion}`);
1929
- logger.log("");
1930
- }
1931
- }
1932
- }
1933
- function displayUnifiedModePackages({
1934
- packages,
1935
- newVersion,
1936
- force
1937
- }) {
1938
- logger.log(`${packages.length} package(s):`);
1939
- packages.forEach((pkg) => {
1940
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
1866
+ const versionMode = config.monorepo?.versionMode || "standalone";
1867
+ const logLevel = config.logLevel;
1868
+ logger.debug(`[${versionMode}](${step}) Resolving tags`);
1869
+ const releaseType = config.bump.type;
1870
+ const graduating = typeof newVersion === "string" ? isGraduatingToStableBetweenVersion(pkg.version, newVersion) : isGraduating(pkg.version, releaseType);
1871
+ const from = await resolveFromTag({
1872
+ config,
1873
+ versionMode,
1874
+ step,
1875
+ packageName: pkg.name,
1876
+ currentVersion: pkg.version,
1877
+ graduating,
1878
+ logLevel
1941
1879
  });
1942
- logger.log("");
1943
- }
1944
- function displaySelectiveModePackages({
1945
- packages,
1946
- newVersion,
1947
- force
1948
- }) {
1949
- if (force) {
1950
- logger.log(`${packages.length} package(s):`);
1951
- packages.forEach((pkg) => {
1952
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (force)`);
1953
- });
1954
- logger.log("");
1955
- } else {
1956
- const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
1957
- const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
1958
- const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
1959
- if (packagesWithCommits.length > 0) {
1960
- logger.log(`${packagesWithCommits.length} package(s) with commits:`);
1961
- packagesWithCommits.forEach((pkg) => {
1962
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
1963
- });
1964
- logger.log("");
1965
- }
1966
- if (packagesAsDependents.length > 0) {
1967
- logger.log(`${packagesAsDependents.length} dependent package(s):`);
1968
- packagesAsDependents.forEach((pkg) => {
1969
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
1970
- });
1971
- logger.log("");
1972
- }
1973
- if (packagesAsGraduation.length > 0) {
1974
- logger.log(`${packagesAsGraduation.length} graduation package(s):`);
1975
- packagesAsGraduation.forEach((pkg) => {
1976
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
1977
- });
1978
- logger.log("");
1979
- }
1980
- }
1880
+ const to = resolveToTag({
1881
+ config,
1882
+ versionMode,
1883
+ newVersion,
1884
+ step,
1885
+ packageName: pkg.name
1886
+ });
1887
+ logger.debug(`[${versionMode}](${step}) Using tags: ${from} \u2192 ${to}`);
1888
+ return { from, to };
1981
1889
  }
1982
- function displayIndependentModePackages({
1983
- packages,
1984
- force
1985
- }) {
1986
- if (force) {
1987
- logger.log(`${packages.length} package(s):`);
1988
- packages.forEach((pkg) => {
1989
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (force)`);
1990
- });
1991
- logger.log("");
1992
- } else {
1993
- const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
1994
- const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
1995
- const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
1996
- if (packagesWithCommits.length > 0) {
1997
- logger.log(`${packagesWithCommits.length} package(s) with commits:`);
1998
- packagesWithCommits.forEach((pkg) => {
1999
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
2000
- });
2001
- logger.log("");
2002
- }
2003
- if (packagesAsDependents.length > 0) {
2004
- logger.log(`${packagesAsDependents.length} dependent package(s):`);
2005
- packagesAsDependents.forEach((pkg) => {
2006
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
2007
- });
2008
- logger.log("");
2009
- }
2010
- if (packagesAsGraduation.length > 0) {
2011
- logger.log(`${packagesAsGraduation.length} graduation package(s):`);
2012
- packagesAsGraduation.forEach((pkg) => {
2013
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
2014
- });
2015
- logger.log("");
2016
- }
2017
- }
1890
+
1891
+ function fromTagIsFirstCommit(fromTag, cwd) {
1892
+ return fromTag === getFirstCommit(cwd);
2018
1893
  }
2019
- async function confirmBump({
2020
- versionMode,
1894
+ async function generateChangelog({
1895
+ pkg,
2021
1896
  config,
2022
- packages,
2023
- force,
2024
- currentVersion,
1897
+ dryRun,
2025
1898
  newVersion,
2026
- dryRun
1899
+ minify
2027
1900
  }) {
2028
- if (packages.length === 0) {
2029
- logger.debug("No packages to bump");
2030
- return;
1901
+ let fromTag = config.from || pkg.fromTag || getFirstCommit(config.cwd);
1902
+ const isFirstCommit = fromTagIsFirstCommit(fromTag, config.cwd);
1903
+ if (isFirstCommit) {
1904
+ fromTag = config.monorepo?.versionMode === "independent" ? getIndependentTag({ version: "0.0.0", name: pkg.name }) : config.templates.tagBody.replace("{{newVersion}}", "0.0.0");
2031
1905
  }
2032
- const lernaJsonExists = hasLernaJson(config.cwd);
2033
- logger.log("");
2034
- logger.info(`${dryRun ? "[dry-run] " : ""}The following packages will be updated:
2035
- `);
2036
- displayRootAndLernaUpdates({
2037
- versionMode,
2038
- currentVersion,
2039
- newVersion,
2040
- lernaJsonExists,
2041
- dryRun
2042
- });
2043
- if (versionMode === "unified") {
2044
- if (!newVersion) {
2045
- throw new Error("Cannot confirm bump in unified mode without a new version");
2046
- }
2047
- displayUnifiedModePackages({ packages, newVersion, force });
2048
- } else if (versionMode === "selective") {
2049
- if (!newVersion) {
2050
- throw new Error("Cannot confirm bump in selective mode without a new version");
2051
- }
2052
- displaySelectiveModePackages({ packages, newVersion, force });
2053
- } else if (versionMode === "independent") {
2054
- displayIndependentModePackages({ packages, force });
1906
+ let toTag = config.to;
1907
+ if (!toTag) {
1908
+ toTag = config.monorepo?.versionMode === "independent" ? getIndependentTag({ version: newVersion, name: pkg.name }) : config.templates.tagBody.replace("{{newVersion}}", newVersion);
1909
+ }
1910
+ if (!toTag) {
1911
+ throw new Error(`No tag found for ${pkg.name}`);
2055
1912
  }
1913
+ logger.debug(`Generating changelog for ${pkg.name} - from ${fromTag} to ${toTag}`);
2056
1914
  try {
2057
- const confirmed = await confirm({
2058
- message: `${dryRun ? "[dry-run] " : ""}Do you want to proceed with these version updates?`,
2059
- default: true
1915
+ config = {
1916
+ ...config,
1917
+ from: fromTag,
1918
+ to: toTag
1919
+ };
1920
+ const generatedChangelog = await generateMarkDown({
1921
+ commits: pkg.commits,
1922
+ config,
1923
+ from: fromTag,
1924
+ isFirstCommit,
1925
+ to: toTag,
1926
+ minify
2060
1927
  });
2061
- if (!confirmed) {
2062
- logger.log("");
2063
- logger.fail("Bump refused");
2064
- process.exit(0);
2065
- }
2066
- } catch (error) {
2067
- const userHasExited = error instanceof Error && error.name === "ExitPromptError";
2068
- if (userHasExited) {
2069
- logger.log("");
2070
- logger.fail("Bump cancelled");
2071
- process.exit(0);
1928
+ let changelog = generatedChangelog;
1929
+ if (pkg.commits.length === 0) {
1930
+ changelog = `${changelog}
1931
+
1932
+ ${config.templates.emptyChangelogContent}`;
2072
1933
  }
2073
- logger.fail("Error while confirming bump");
2074
- process.exit(1);
2075
- }
2076
- logger.log("");
2077
- }
2078
- function getBumpedIndependentPackages({
2079
- packages,
2080
- dryRun
2081
- }) {
2082
- const bumpedPackages = [];
2083
- for (const pkgToBump of packages) {
2084
- logger.debug(`Bumping ${pkgToBump.name} from ${pkgToBump.version} to ${pkgToBump.newVersion} (reason: ${pkgToBump.reason})`);
2085
- const result = getBumpedPackageIndependently({
2086
- pkg: pkgToBump,
2087
- dryRun
1934
+ const changelogResult = await executeHook("generate:changelog", config, dryRun, {
1935
+ commits: pkg.commits,
1936
+ changelog
2088
1937
  });
2089
- if (result.bumped) {
2090
- bumpedPackages.push({
2091
- ...pkgToBump,
2092
- version: result.oldVersion
2093
- });
1938
+ changelog = changelogResult || changelog;
1939
+ logger.verbose(`Output changelog for ${pkg.name}:
1940
+ ${changelog}`);
1941
+ logger.debug(`Changelog generated for ${pkg.name} (${pkg.commits.length} commits)`);
1942
+ logger.verbose(`Final changelog for ${pkg.name}:
1943
+
1944
+ ${changelog}
1945
+
1946
+ `);
1947
+ if (dryRun) {
1948
+ logger.info(`[dry-run] ${pkg.name} - Generate changelog ${fromTag}...${toTag}`);
2094
1949
  }
1950
+ return changelog;
1951
+ } catch (error) {
1952
+ throw new Error(`Error generating changelog for ${pkg.name} (${fromTag}...${toTag}): ${error}`);
2095
1953
  }
2096
- return bumpedPackages;
2097
1954
  }
2098
- function shouldFilterPrereleaseTags(currentVersion, graduating) {
2099
- return !isPrerelease(currentVersion) && !graduating;
2100
- }
2101
- function extractVersionFromTag(tag, packageName) {
2102
- if (!tag) {
2103
- return null;
1955
+ function writeChangelogToFile({
1956
+ cwd,
1957
+ pkg,
1958
+ changelog,
1959
+ dryRun = false
1960
+ }) {
1961
+ const changelogPath = join(pkg.path, "CHANGELOG.md");
1962
+ let existingChangelog = "";
1963
+ if (existsSync(changelogPath)) {
1964
+ existingChangelog = readFileSync(changelogPath, "utf8");
2104
1965
  }
2105
- if (packageName) {
2106
- const prefix = `${packageName}@`;
2107
- if (tag.startsWith(prefix)) {
2108
- return tag.slice(prefix.length);
2109
- }
2110
- }
2111
- const atIndex = tag.lastIndexOf("@");
2112
- if (atIndex !== -1) {
2113
- return tag.slice(atIndex + 1);
2114
- }
2115
- if (tag.startsWith("v") && /^v\d/.test(tag)) {
2116
- return tag.slice(1);
2117
- }
2118
- if (/^\d+\.\d+\.\d+/.test(tag)) {
2119
- return tag;
1966
+ const lines = existingChangelog.split("\n");
1967
+ const titleIndex = lines.findIndex((line) => line.startsWith("# "));
1968
+ let updatedChangelog;
1969
+ if (titleIndex !== -1) {
1970
+ const beforeTitle = lines.slice(0, titleIndex + 1);
1971
+ const afterTitle = lines.slice(titleIndex + 1);
1972
+ updatedChangelog = [...beforeTitle, "", changelog, "", ...afterTitle].join("\n");
1973
+ } else {
1974
+ const title = "# Changelog\n";
1975
+ updatedChangelog = `${title}
1976
+ ${changelog}
1977
+ ${existingChangelog}`;
2120
1978
  }
2121
- return null;
2122
- }
2123
- function isTagVersionCompatibleWithCurrent(tagVersion, currentVersion) {
2124
- try {
2125
- const tagMajor = semver.major(tagVersion);
2126
- const currentMajor = semver.major(currentVersion);
2127
- return tagMajor <= currentMajor;
2128
- } catch {
2129
- return false;
1979
+ if (dryRun) {
1980
+ const relativeChangelogPath = relative(cwd, changelogPath);
1981
+ logger.info(`[dry-run] ${pkg.name} - Write changelog to ${relativeChangelogPath}`);
1982
+ } else {
1983
+ logger.debug(`Writing changelog to ${changelogPath}`);
1984
+ writeFileSync(changelogPath, updatedChangelog, "utf8");
1985
+ logger.info(`Changelog updated for ${pkg.name} (${"newVersion" in pkg && pkg.newVersion || pkg.version})`);
2130
1986
  }
2131
1987
  }
2132
1988
 
2133
- function getIndependentTag({ version, name }) {
2134
- return `${name}@${version}`;
2135
- }
2136
- async function getLastStableTag({ logLevel, cwd }) {
2137
- const { stdout } = await execPromise(
2138
- `git tag --sort=-creatordate | grep -E '^[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+$' | head -n 1`,
2139
- {
2140
- logLevel,
2141
- noStderr: true,
2142
- noStdout: true,
2143
- noSuccess: true,
2144
- cwd
2145
- }
2146
- );
2147
- const lastTag = stdout.trim();
2148
- logger.debug("Last stable tag:", lastTag || "No stable tags found");
2149
- return lastTag;
2150
- }
2151
- async function getLastTag({ logLevel, cwd }) {
2152
- const { stdout } = await execPromise(`git tag --sort=-creatordate | head -n 1`, {
2153
- logLevel,
2154
- noStderr: true,
2155
- noStdout: true,
2156
- noSuccess: true,
2157
- cwd
2158
- });
2159
- const lastTag = stdout.trim();
2160
- logger.debug("Last tag:", lastTag || "No tags found");
2161
- return lastTag;
2162
- }
2163
- async function getAllRecentRepoTags(options) {
2164
- const limit = options?.limit;
2165
- try {
2166
- const { stdout } = await execPromise(
2167
- `git tag --sort=-creatordate | head -n ${limit}`,
2168
- {
2169
- logLevel: options?.logLevel,
2170
- noStderr: true,
2171
- noStdout: true,
2172
- noSuccess: true,
2173
- cwd: options?.cwd
1989
+ function getDefaultConfig() {
1990
+ return {
1991
+ cwd: process$1.cwd(),
1992
+ types: {
1993
+ feat: { title: "\u{1F680} Enhancements", semver: "minor" },
1994
+ perf: { title: "\u{1F525} Performance", semver: "patch" },
1995
+ fix: { title: "\u{1FA79} Fixes", semver: "patch" },
1996
+ refactor: { title: "\u{1F485} Refactors", semver: "patch" },
1997
+ docs: { title: "\u{1F4D6} Documentation", semver: "patch" },
1998
+ build: { title: "\u{1F4E6} Build", semver: "patch" },
1999
+ types: { title: "\u{1F30A} Types", semver: "patch" },
2000
+ chore: { title: "\u{1F3E1} Chore" },
2001
+ examples: { title: "\u{1F3C0} Examples" },
2002
+ test: { title: "\u2705 Tests" },
2003
+ style: { title: "\u{1F3A8} Styles" },
2004
+ ci: { title: "\u{1F916} CI" }
2005
+ },
2006
+ templates: {
2007
+ commitMessage: "chore(release): bump version to {{newVersion}}",
2008
+ tagMessage: "Bump version to {{newVersion}}",
2009
+ tagBody: "v{{newVersion}}",
2010
+ emptyChangelogContent: "No relevant changes for this release",
2011
+ twitterMessage: "\u{1F680} {{projectName}} {{version}} is out!\n\n{{changelog}}\n\n{{releaseUrl}}\n{{changelogUrl}}",
2012
+ slackMessage: void 0
2013
+ // Use rich blocks format by default (no template)
2014
+ },
2015
+ excludeAuthors: [],
2016
+ noAuthors: false,
2017
+ bump: {
2018
+ type: "release",
2019
+ clean: true,
2020
+ dependencyTypes: ["dependencies"],
2021
+ yes: false
2022
+ },
2023
+ changelog: {
2024
+ rootChangelog: true,
2025
+ includeCommitBody: true
2026
+ },
2027
+ publish: {
2028
+ private: false,
2029
+ args: [],
2030
+ token: process$1.env.RELIZY_NPM_TOKEN || process$1.env.NPM_TOKEN || process$1.env.NODE_AUTH_TOKEN,
2031
+ registry: "https://registry.npmjs.org/",
2032
+ safetyCheck: false
2033
+ },
2034
+ tokens: {
2035
+ registry: process$1.env.RELIZY_NPM_TOKEN || process$1.env.NPM_TOKEN || process$1.env.NODE_AUTH_TOKEN,
2036
+ gitlab: process$1.env.RELIZY_GITLAB_TOKEN || process$1.env.GITLAB_TOKEN || process$1.env.GITLAB_API_TOKEN || process$1.env.CI_JOB_TOKEN,
2037
+ github: process$1.env.RELIZY_GITHUB_TOKEN || process$1.env.GITHUB_TOKEN || process$1.env.GH_TOKEN,
2038
+ twitter: {
2039
+ apiKey: process$1.env.RELIZY_TWITTER_API_KEY || process$1.env.TWITTER_API_KEY,
2040
+ apiKeySecret: process$1.env.RELIZY_TWITTER_API_KEY_SECRET || process$1.env.TWITTER_API_KEY_SECRET,
2041
+ accessToken: process$1.env.RELIZY_TWITTER_ACCESS_TOKEN || process$1.env.TWITTER_ACCESS_TOKEN,
2042
+ accessTokenSecret: process$1.env.RELIZY_TWITTER_ACCESS_TOKEN_SECRET || process$1.env.TWITTER_ACCESS_TOKEN_SECRET
2043
+ },
2044
+ slack: process$1.env.RELIZY_SLACK_TOKEN || process$1.env.SLACK_TOKEN
2045
+ },
2046
+ scopeMap: {},
2047
+ release: {
2048
+ commit: true,
2049
+ publish: true,
2050
+ changelog: true,
2051
+ push: true,
2052
+ clean: true,
2053
+ providerRelease: true,
2054
+ noVerify: false,
2055
+ gitTag: true,
2056
+ social: true
2057
+ },
2058
+ social: {
2059
+ twitter: {
2060
+ enabled: false,
2061
+ onlyStable: true
2062
+ },
2063
+ slack: {
2064
+ enabled: false,
2065
+ onlyStable: true
2174
2066
  }
2175
- );
2176
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
2177
- logger.debug(`Retrieved ${tags.length} recent repo tags`);
2178
- return tags;
2179
- } catch {
2180
- return [];
2181
- }
2067
+ },
2068
+ logLevel: "default",
2069
+ safetyCheck: true
2070
+ };
2182
2071
  }
2183
- async function getAllRecentPackageTags({
2184
- packageName,
2185
- limit = 50,
2186
- logLevel,
2187
- cwd
2188
- }) {
2189
- try {
2190
- const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
2191
- const { stdout } = await execPromise(
2192
- `git tag --sort=-creatordate | grep -E '^${escapedPackageName}@' | head -n ${limit}`,
2193
- {
2194
- logLevel,
2195
- noStderr: true,
2196
- noStdout: true,
2197
- noSuccess: true,
2198
- cwd
2199
- }
2200
- );
2201
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
2202
- logger.debug(`Retrieved ${tags.length} recent tags for package ${packageName}`);
2203
- return tags;
2204
- } catch {
2205
- return [];
2072
+ function setupLogger(logLevel) {
2073
+ if (logLevel) {
2074
+ logger.setLevel(logLevel);
2075
+ logger.debug(`Log level set to: ${logLevel}`);
2206
2076
  }
2207
2077
  }
2208
- function filterCompatibleTags({
2209
- tags,
2210
- currentVersion,
2211
- onlyStable,
2212
- packageName
2213
- }) {
2214
- const filtered = tags.filter((tag) => {
2215
- const tagVersion = extractVersionFromTag(tag, packageName);
2216
- if (!tagVersion) {
2217
- logger.debug(`Skipping tag ${tag}: cannot extract version`);
2218
- return false;
2219
- }
2220
- if (onlyStable && isPrerelease(tagVersion)) {
2221
- logger.debug(`Skipping tag ${tag}: prerelease version ${tagVersion} (onlyStable=${onlyStable})`);
2222
- return false;
2223
- }
2224
- if (!isTagVersionCompatibleWithCurrent(tagVersion, currentVersion)) {
2225
- logger.debug(`Skipping tag ${tag}: version ${tagVersion} has higher major than current ${currentVersion}`);
2226
- return false;
2227
- }
2228
- logger.debug(`Tag ${tag} with version ${tagVersion} is compatible`);
2229
- return true;
2230
- });
2231
- logger.debug(`Filtered ${tags.length} tags down to ${filtered.length} compatible tags`);
2232
- return filtered;
2233
- }
2234
- function getLastRepoTag(options) {
2235
- if (options?.currentVersion) {
2236
- return getLastRepoTagWithFiltering({
2237
- currentVersion: options.currentVersion,
2238
- onlyStable: options.onlyStable ?? false,
2239
- logLevel: options.logLevel,
2240
- cwd: options.cwd
2241
- });
2078
+ async function resolveConfig(config, cwd) {
2079
+ if (!config.repo) {
2080
+ const resolvedRepoConfig = await resolveRepoConfig(cwd);
2081
+ config.repo = {
2082
+ ...resolvedRepoConfig,
2083
+ provider: resolvedRepoConfig.provider
2084
+ };
2242
2085
  }
2243
- if (options?.onlyStable) {
2244
- return getLastStableTag({ logLevel: options?.logLevel, cwd: options?.cwd });
2086
+ if (typeof config.repo === "string") {
2087
+ const resolvedRepoConfig = getRepoConfig(config.repo);
2088
+ config.repo = {
2089
+ ...resolvedRepoConfig,
2090
+ provider: resolvedRepoConfig.provider
2091
+ };
2245
2092
  }
2246
- return getLastTag({ logLevel: options?.logLevel, cwd: options?.cwd });
2093
+ return config;
2247
2094
  }
2248
- async function getLastRepoTagWithFiltering({
2249
- currentVersion,
2250
- onlyStable,
2251
- logLevel,
2252
- cwd
2253
- }) {
2254
- const recentTags = await getAllRecentRepoTags({ limit: 50, logLevel, cwd });
2255
- if (recentTags.length === 0) {
2256
- logger.debug("No tags found in repository");
2257
- return null;
2258
- }
2259
- const compatibleTags = filterCompatibleTags({
2260
- tags: recentTags,
2261
- currentVersion,
2262
- onlyStable
2095
+ async function loadRelizyConfig(options) {
2096
+ const cwd = options?.overrides?.cwd ?? process$1.cwd();
2097
+ await setupDotenv({ cwd });
2098
+ const configFile = options?.configFile ?? "relizy";
2099
+ const defaultConfig = getDefaultConfig();
2100
+ const overridesConfig = defu(options?.overrides, options?.baseConfig);
2101
+ const results = await loadConfig({
2102
+ dotenv: true,
2103
+ cwd,
2104
+ name: configFile,
2105
+ packageJson: true,
2106
+ defaults: defaultConfig,
2107
+ overrides: overridesConfig
2263
2108
  });
2264
- if (compatibleTags.length === 0) {
2265
- logger.debug("No compatible tags found");
2266
- return null;
2109
+ if (typeof results._configFile !== "string") {
2110
+ logger.debug(`No config file found with name "${configFile}"`);
2111
+ if (options?.configFile) {
2112
+ logger.error(`No config file found with name "${configFile}"`);
2113
+ process$1.exit(1);
2114
+ }
2267
2115
  }
2268
- const lastTag = compatibleTags[0];
2269
- logger.debug(`Last compatible repo tag: ${lastTag}`);
2270
- return lastTag;
2116
+ setupLogger(options?.overrides?.logLevel || results.config.logLevel);
2117
+ logger.verbose("User config:", formatJson(results.config.changelog));
2118
+ const resolvedConfig = await resolveConfig(results.config, cwd);
2119
+ logger.debug("Resolved config:", formatJson(resolvedConfig));
2120
+ return resolvedConfig;
2271
2121
  }
2272
- async function getLastPackageTag({
2273
- packageName,
2274
- onlyStable,
2275
- currentVersion,
2276
- logLevel,
2277
- cwd
2122
+ function defineConfig(config) {
2123
+ return config;
2124
+ }
2125
+
2126
+ async function githubIndependentMode({
2127
+ config,
2128
+ dryRun,
2129
+ bumpResult,
2130
+ force,
2131
+ suffix
2278
2132
  }) {
2279
- if (currentVersion) {
2280
- return getLastPackageTagWithFiltering({
2281
- packageName,
2282
- currentVersion,
2283
- onlyStable: onlyStable ?? false,
2284
- logLevel,
2285
- cwd
2286
- });
2133
+ const repoConfig = config.repo;
2134
+ if (!repoConfig) {
2135
+ throw new Error("No repository configuration found. Please check your changelog config.");
2287
2136
  }
2288
- try {
2289
- const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
2290
- let grepPattern;
2291
- if (onlyStable) {
2292
- grepPattern = `^${escapedPackageName}@[0-9]+\\.[0-9]+\\.[0-9]+$`;
2293
- } else {
2294
- grepPattern = `^${escapedPackageName}@`;
2137
+ logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
2138
+ if (!config.tokens.github && !config.repo?.token) {
2139
+ throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
2140
+ }
2141
+ const packages = await getPackagesOrBumpedPackages({
2142
+ config,
2143
+ bumpResult,
2144
+ suffix,
2145
+ force
2146
+ });
2147
+ logger.info(`Creating ${packages.length} GitHub release(s)`);
2148
+ const postedReleases = [];
2149
+ for (const pkg of packages) {
2150
+ const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
2151
+ const from = config.from || pkg.fromTag;
2152
+ const to = config.to || getIndependentTag({ version: newVersion, name: pkg.name });
2153
+ if (!from) {
2154
+ logger.warn(`No from tag found for ${pkg.name}, skipping release`);
2155
+ continue;
2295
2156
  }
2296
- const { stdout } = await execPromise(
2297
- `git tag --sort=-creatordate | grep -E '${grepPattern}' | sed -n '1p'`,
2298
- {
2299
- logLevel,
2300
- noStderr: true,
2301
- noStdout: true,
2302
- noSuccess: true,
2303
- cwd
2304
- }
2305
- );
2306
- const tag = stdout.trim();
2307
- return tag || null;
2308
- } catch {
2309
- return null;
2157
+ const toTag = dryRun ? "HEAD" : to;
2158
+ logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${toTag}`);
2159
+ const changelog = await generateChangelog({
2160
+ pkg,
2161
+ config,
2162
+ dryRun,
2163
+ newVersion
2164
+ });
2165
+ const releaseBody = changelog.split("\n").slice(2).join("\n");
2166
+ const release = {
2167
+ tag_name: to,
2168
+ name: to,
2169
+ body: releaseBody,
2170
+ prerelease: isPrerelease(newVersion)
2171
+ };
2172
+ logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
2173
+ if (dryRun) {
2174
+ logger.info(`[dry-run] Publish GitHub release for ${to}`);
2175
+ postedReleases.push({
2176
+ name: pkg.name,
2177
+ tag: release.tag_name,
2178
+ version: newVersion,
2179
+ prerelease: release.prerelease
2180
+ });
2181
+ } else {
2182
+ logger.debug(`Publishing release ${to} to GitHub...`);
2183
+ await createGithubRelease({
2184
+ ...config,
2185
+ from,
2186
+ to,
2187
+ repo: repoConfig
2188
+ }, release);
2189
+ postedReleases.push({
2190
+ name: pkg.name,
2191
+ tag: release.tag_name,
2192
+ version: newVersion,
2193
+ prerelease: release.prerelease
2194
+ });
2195
+ }
2196
+ }
2197
+ if (postedReleases.length === 0) {
2198
+ logger.warn("No releases created");
2199
+ } else {
2200
+ logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitHub!`);
2310
2201
  }
2202
+ return postedReleases;
2311
2203
  }
2312
- async function getLastPackageTagWithFiltering({
2313
- packageName,
2314
- currentVersion,
2315
- onlyStable,
2316
- logLevel,
2317
- cwd
2204
+ async function githubUnified({
2205
+ config,
2206
+ dryRun,
2207
+ rootPackage,
2208
+ bumpResult
2318
2209
  }) {
2319
- const recentTags = await getAllRecentPackageTags({
2320
- packageName,
2321
- limit: 50,
2322
- logLevel,
2323
- cwd
2324
- });
2325
- if (recentTags.length === 0) {
2326
- logger.debug(`No tags found for package ${packageName}`);
2327
- return null;
2210
+ const repoConfig = config.repo;
2211
+ if (!repoConfig) {
2212
+ throw new Error("No repository configuration found. Please check your changelog config.");
2328
2213
  }
2329
- const compatibleTags = filterCompatibleTags({
2330
- tags: recentTags,
2331
- currentVersion,
2332
- onlyStable,
2333
- packageName
2334
- });
2335
- if (compatibleTags.length === 0) {
2336
- logger.debug(`No compatible tags found for package ${packageName}`);
2337
- return null;
2214
+ logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
2215
+ if (!config.tokens.github && !config.repo?.token) {
2216
+ throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
2338
2217
  }
2339
- const lastTag = compatibleTags[0];
2340
- logger.debug(`Last compatible package tag for ${packageName}: ${lastTag}`);
2341
- return lastTag;
2342
- }
2343
- async function resolveFromTagIndependent({
2344
- cwd,
2345
- packageName,
2346
- currentVersion,
2347
- graduating,
2348
- logLevel
2349
- }) {
2350
- const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
2351
- const onlyStable = graduating || filterPrereleases;
2352
- const lastPackageTag = await getLastPackageTag({
2353
- packageName,
2354
- currentVersion,
2355
- onlyStable,
2356
- logLevel,
2357
- cwd
2218
+ const newVersion = bumpResult?.newVersion || rootPackage.version;
2219
+ const to = config.to || config.templates.tagBody.replace("{{newVersion}}", newVersion);
2220
+ const changelog = await generateChangelog({
2221
+ pkg: rootPackage,
2222
+ config,
2223
+ dryRun,
2224
+ newVersion
2358
2225
  });
2359
- if (!lastPackageTag) {
2360
- return getFirstCommit(cwd);
2226
+ const releaseBody = changelog.split("\n").slice(2).join("\n");
2227
+ const release = {
2228
+ tag_name: to,
2229
+ name: to,
2230
+ body: releaseBody,
2231
+ prerelease: isPrerelease(to)
2232
+ };
2233
+ logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
2234
+ logger.debug("Release details:", formatJson({
2235
+ tag_name: release.tag_name,
2236
+ name: release.name,
2237
+ prerelease: release.prerelease
2238
+ }));
2239
+ if (dryRun) {
2240
+ logger.info("[dry-run] Publish GitHub release for", release.tag_name);
2241
+ } else {
2242
+ logger.debug("Publishing release to GitHub...");
2243
+ const releaseConfig = {
2244
+ ...config,
2245
+ from: bumpResult?.bumped && bumpResult.fromTag || "v0.0.0",
2246
+ to,
2247
+ repo: repoConfig
2248
+ };
2249
+ await createGithubRelease(releaseConfig, release);
2361
2250
  }
2362
- return lastPackageTag;
2251
+ logger.success(`Release ${to} published to GitHub!`);
2252
+ return [{
2253
+ name: to,
2254
+ tag: to,
2255
+ version: to,
2256
+ prerelease: release.prerelease
2257
+ }];
2363
2258
  }
2364
- async function resolveFromTagUnified({
2365
- config,
2366
- currentVersion,
2367
- graduating,
2368
- logLevel
2369
- }) {
2370
- const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
2371
- const onlyStable = graduating || filterPrereleases;
2372
- const from = await getLastRepoTag({
2373
- currentVersion,
2374
- onlyStable,
2375
- logLevel,
2376
- cwd: config.cwd
2377
- }) || getFirstCommit(config.cwd);
2378
- return from;
2259
+ async function github(options) {
2260
+ logger.debug("Config:", options);
2261
+ try {
2262
+ const dryRun = options.dryRun ?? false;
2263
+ logger.debug(`Dry run: ${dryRun}`);
2264
+ const config = await loadRelizyConfig({
2265
+ configFile: options.configName,
2266
+ baseConfig: options.config,
2267
+ overrides: {
2268
+ from: options.from,
2269
+ to: options.to,
2270
+ logLevel: options.logLevel,
2271
+ tokens: {
2272
+ github: options.token
2273
+ }
2274
+ }
2275
+ });
2276
+ if (config.monorepo?.versionMode === "independent") {
2277
+ return await githubIndependentMode({
2278
+ config,
2279
+ dryRun,
2280
+ bumpResult: options.bumpResult,
2281
+ force: options.force ?? false,
2282
+ suffix: options.suffix
2283
+ });
2284
+ }
2285
+ const rootPackageBase = readPackageJson(config.cwd);
2286
+ if (!rootPackageBase) {
2287
+ throw new Error("Failed to read root package.json");
2288
+ }
2289
+ const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
2290
+ const { from, to } = await resolveTags({
2291
+ config,
2292
+ step: "provider-release",
2293
+ newVersion,
2294
+ pkg: rootPackageBase
2295
+ });
2296
+ const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
2297
+ config,
2298
+ force: options.force ?? false,
2299
+ suffix: options.suffix,
2300
+ changelog: true,
2301
+ from,
2302
+ to
2303
+ });
2304
+ return await githubUnified({
2305
+ config,
2306
+ dryRun,
2307
+ rootPackage,
2308
+ bumpResult: options.bumpResult
2309
+ });
2310
+ } catch (error) {
2311
+ logger.error("Error publishing GitHub release:", error);
2312
+ throw error;
2313
+ }
2379
2314
  }
2380
- async function resolveFromTag({
2315
+
2316
+ async function createGitlabRelease({
2381
2317
  config,
2382
- versionMode,
2383
- step,
2384
- packageName,
2385
- currentVersion,
2386
- graduating,
2387
- logLevel
2318
+ release,
2319
+ dryRun
2388
2320
  }) {
2389
- let from;
2390
- if (versionMode === "independent") {
2391
- if (!packageName) {
2392
- throw new Error("Package name is required for independent version mode");
2321
+ const token = config.tokens.gitlab || config.repo?.token;
2322
+ if (!token && !dryRun) {
2323
+ throw new Error(
2324
+ "No GitLab token found. Set GITLAB_TOKEN or CI_JOB_TOKEN environment variable or configure tokens.gitlab"
2325
+ );
2326
+ }
2327
+ const repoConfig = config.repo?.repo;
2328
+ if (!repoConfig) {
2329
+ throw new Error("No repository URL found in config");
2330
+ }
2331
+ logger.debug(`Parsed repository URL: ${repoConfig}`);
2332
+ const projectPath = encodeURIComponent(repoConfig);
2333
+ const gitlabDomain = config.repo?.domain || "gitlab.com";
2334
+ const apiUrl = `https://${gitlabDomain}/api/v4/projects/${projectPath}/releases`;
2335
+ logger.info(`Creating GitLab release at: ${apiUrl}`);
2336
+ const payload = {
2337
+ tag_name: release.tag_name,
2338
+ name: release.name || release.tag_name,
2339
+ description: release.description || "",
2340
+ ref: release.ref || "main"
2341
+ };
2342
+ try {
2343
+ if (dryRun) {
2344
+ logger.info("[dry-run] GitLab release:", formatJson(payload));
2345
+ return {
2346
+ tag_name: release.tag_name,
2347
+ name: release.name || release.tag_name,
2348
+ description: release.description || "",
2349
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2350
+ released_at: (/* @__PURE__ */ new Date()).toISOString(),
2351
+ _links: {
2352
+ self: `${apiUrl}/${encodeURIComponent(release.tag_name)}`
2353
+ }
2354
+ };
2393
2355
  }
2394
- from = await resolveFromTagIndependent({
2395
- cwd: config.cwd,
2396
- packageName,
2397
- currentVersion,
2398
- graduating,
2399
- logLevel
2400
- });
2401
- } else {
2402
- from = await resolveFromTagUnified({
2403
- config,
2404
- currentVersion,
2405
- graduating,
2406
- logLevel
2356
+ logger.debug(`POST GitLab release to ${apiUrl} with payload: ${formatJson(payload)}`);
2357
+ const response = await fetch(apiUrl, {
2358
+ method: "POST",
2359
+ headers: {
2360
+ "Content-Type": "application/json",
2361
+ "PRIVATE-TOKEN": token || ""
2362
+ },
2363
+ body: JSON.stringify(payload)
2407
2364
  });
2365
+ if (!response.ok) {
2366
+ const errorText = await response.text();
2367
+ throw new Error(`GitLab API error (${response.status}): ${errorText}`);
2368
+ }
2369
+ const result = await response.json();
2370
+ logger.debug(`Created GitLab release: ${result._links.self}`);
2371
+ return result;
2372
+ } catch (error) {
2373
+ logger.error("Failed to create GitLab release:", error);
2374
+ throw error;
2408
2375
  }
2409
- logger.debug(`[${versionMode}](${step}) Using from tag: ${from}`);
2410
- return config.from || from;
2411
2376
  }
2412
- function resolveToTag({
2377
+ async function gitlabIndependentMode({
2413
2378
  config,
2414
- versionMode,
2415
- newVersion,
2416
- step,
2417
- packageName
2379
+ dryRun,
2380
+ bumpResult,
2381
+ suffix,
2382
+ force
2418
2383
  }) {
2419
- const isUntaggedStep = step === "bump" || step === "changelog";
2420
- let to;
2421
- if (isUntaggedStep) {
2422
- to = getCurrentGitRef(config.cwd);
2423
- } else if (versionMode === "independent") {
2424
- if (!packageName) {
2425
- throw new Error("Package name is required for independent version mode");
2384
+ logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
2385
+ const packages = await getPackagesOrBumpedPackages({
2386
+ config,
2387
+ bumpResult,
2388
+ suffix,
2389
+ force
2390
+ });
2391
+ logger.info(`Creating ${packages.length} GitLab release(s) for independent packages`);
2392
+ logger.debug("Getting current branch...");
2393
+ const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
2394
+ noSuccess: true,
2395
+ noStdout: true,
2396
+ logLevel: config.logLevel,
2397
+ cwd: config.cwd
2398
+ });
2399
+ const postedReleases = [];
2400
+ for (const pkg of packages) {
2401
+ const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
2402
+ const from = config.from || pkg.fromTag;
2403
+ const to = getIndependentTag({ version: newVersion, name: pkg.name });
2404
+ if (!from) {
2405
+ logger.warn(`No from tag found for ${pkg.name}, skipping release`);
2406
+ continue;
2426
2407
  }
2427
- if (!newVersion) {
2428
- throw new Error("New version is required for independent version mode");
2408
+ logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${to}`);
2409
+ const changelog = await generateChangelog({
2410
+ pkg,
2411
+ config,
2412
+ dryRun,
2413
+ newVersion
2414
+ });
2415
+ if (!changelog) {
2416
+ logger.warn(`No changelog found for ${pkg.name}`);
2417
+ continue;
2429
2418
  }
2430
- to = getIndependentTag({ version: newVersion, name: packageName });
2419
+ const releaseBody = changelog.split("\n").slice(2).join("\n");
2420
+ const release = {
2421
+ tag_name: to,
2422
+ name: to,
2423
+ description: releaseBody,
2424
+ ref: currentBranch.trim()
2425
+ };
2426
+ logger.debug(`Creating release for ${to} (ref: ${release.ref})`);
2427
+ if (dryRun) {
2428
+ logger.info(`[dry-run] Publish GitLab release for ${to}`);
2429
+ } else {
2430
+ logger.debug(`Publishing release ${to} to GitLab...`);
2431
+ await createGitlabRelease({
2432
+ config,
2433
+ release,
2434
+ dryRun
2435
+ });
2436
+ postedReleases.push({
2437
+ name: pkg.name,
2438
+ tag: release.tag_name,
2439
+ version: newVersion,
2440
+ prerelease: isPrerelease(newVersion)
2441
+ });
2442
+ }
2443
+ }
2444
+ if (postedReleases.length === 0) {
2445
+ logger.warn("No releases created");
2431
2446
  } else {
2432
- to = newVersion ? config.templates.tagBody.replace("{{newVersion}}", newVersion) : getCurrentGitRef(config.cwd);
2447
+ logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitLab!`);
2433
2448
  }
2434
- logger.debug(`[${versionMode}](${step}) Using to tag: ${to}`);
2435
- return config.to || to;
2449
+ return postedReleases;
2436
2450
  }
2437
- async function resolveTags({
2451
+ async function gitlabUnified({
2438
2452
  config,
2439
- step,
2440
- pkg,
2441
- newVersion
2453
+ dryRun,
2454
+ rootPackage,
2455
+ bumpResult
2442
2456
  }) {
2443
- const versionMode = config.monorepo?.versionMode || "standalone";
2444
- const logLevel = config.logLevel;
2445
- logger.debug(`[${versionMode}](${step}) Resolving tags`);
2446
- const releaseType = config.bump.type;
2447
- const graduating = typeof newVersion === "string" ? isGraduatingToStableBetweenVersion(pkg.version, newVersion) : isGraduating(pkg.version, releaseType);
2448
- const from = await resolveFromTag({
2457
+ logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
2458
+ const newVersion = bumpResult?.newVersion || rootPackage.newVersion || rootPackage.version;
2459
+ const to = config.templates.tagBody.replace("{{newVersion}}", newVersion);
2460
+ const changelog = await generateChangelog({
2461
+ pkg: rootPackage,
2449
2462
  config,
2450
- versionMode,
2451
- step,
2452
- packageName: pkg.name,
2453
- currentVersion: pkg.version,
2454
- graduating,
2455
- logLevel
2463
+ dryRun,
2464
+ newVersion
2456
2465
  });
2457
- const to = resolveToTag({
2458
- config,
2459
- versionMode,
2460
- newVersion,
2461
- step,
2462
- packageName: pkg.name
2466
+ const releaseBody = changelog.split("\n").slice(2).join("\n");
2467
+ logger.debug("Getting current branch...");
2468
+ const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
2469
+ noSuccess: true,
2470
+ noStdout: true,
2471
+ logLevel: config.logLevel,
2472
+ cwd: config.cwd
2463
2473
  });
2464
- logger.debug(`[${versionMode}](${step}) Using tags: ${from} \u2192 ${to}`);
2465
- return { from, to };
2474
+ const release = {
2475
+ tag_name: to,
2476
+ name: to,
2477
+ description: releaseBody,
2478
+ ref: currentBranch.trim()
2479
+ };
2480
+ logger.info(`Creating release for ${to} (ref: ${release.ref})`);
2481
+ logger.debug("Release details:", formatJson({
2482
+ tag_name: release.tag_name,
2483
+ name: release.name,
2484
+ ref: release.ref
2485
+ }));
2486
+ if (dryRun) {
2487
+ logger.info("[dry-run] Publish GitLab release for", release.tag_name);
2488
+ } else {
2489
+ logger.debug("Publishing release to GitLab...");
2490
+ await createGitlabRelease({
2491
+ config,
2492
+ release,
2493
+ dryRun
2494
+ });
2495
+ }
2496
+ logger.success(`Release ${to} published to GitLab!`);
2497
+ return [{
2498
+ name: to,
2499
+ tag: to,
2500
+ version: to,
2501
+ prerelease: isPrerelease(newVersion)
2502
+ }];
2503
+ }
2504
+ async function gitlab(options = {}) {
2505
+ try {
2506
+ const dryRun = options.dryRun ?? false;
2507
+ logger.debug(`Dry run: ${dryRun}`);
2508
+ const config = await loadRelizyConfig({
2509
+ configFile: options.configName,
2510
+ baseConfig: options.config,
2511
+ overrides: {
2512
+ from: options.from,
2513
+ to: options.to,
2514
+ logLevel: options.logLevel,
2515
+ tokens: {
2516
+ gitlab: options.token
2517
+ }
2518
+ }
2519
+ });
2520
+ if (config.monorepo?.versionMode === "independent") {
2521
+ return await gitlabIndependentMode({
2522
+ config,
2523
+ dryRun,
2524
+ bumpResult: options.bumpResult,
2525
+ suffix: options.suffix,
2526
+ force: options.force ?? false
2527
+ });
2528
+ }
2529
+ const rootPackageBase = readPackageJson(config.cwd);
2530
+ if (!rootPackageBase) {
2531
+ throw new Error("Failed to read root package.json");
2532
+ }
2533
+ const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
2534
+ const { from, to } = await resolveTags({
2535
+ config,
2536
+ step: "provider-release",
2537
+ newVersion,
2538
+ pkg: rootPackageBase
2539
+ });
2540
+ const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
2541
+ config,
2542
+ force: options.force ?? false,
2543
+ suffix: options.suffix,
2544
+ changelog: true,
2545
+ from,
2546
+ to
2547
+ });
2548
+ logger.debug(`Root package: ${getIndependentTag({ name: rootPackage.name, version: newVersion })}`);
2549
+ return await gitlabUnified({
2550
+ config,
2551
+ dryRun,
2552
+ rootPackage,
2553
+ bumpResult: options.bumpResult
2554
+ });
2555
+ } catch (error) {
2556
+ logger.error("Error publishing GitLab release:", error);
2557
+ throw error;
2558
+ }
2466
2559
  }
2467
2560
 
2468
2561
  let sessionOtp;
@@ -2570,7 +2663,7 @@ function getCommandArgs({
2570
2663
  args.push("--registry", registry);
2571
2664
  }
2572
2665
  const isPnpmOrNpm = packageManager === "pnpm" || packageManager === "npm";
2573
- const publishToken = config.publish.token;
2666
+ const publishToken = config.publish.token || config.tokens.registry;
2574
2667
  if (publishToken) {
2575
2668
  if (!registry) {
2576
2669
  logger.warn("Publish token provided but no registry specified");
@@ -2611,7 +2704,18 @@ function isOtpError(error) {
2611
2704
  if (typeof error !== "object" || error === null)
2612
2705
  return false;
2613
2706
  const errorMessage = "message" in error && typeof error.message === "string" ? error.message.toLowerCase() : "";
2614
- return errorMessage.includes("otp") || errorMessage.includes("one-time password") || errorMessage.includes("eotp");
2707
+ const fullErrorString = String(error).toLowerCase();
2708
+ const searchText = `${errorMessage} ${fullErrorString}`;
2709
+ const otpPatterns = [
2710
+ "otp",
2711
+ "one-time password",
2712
+ "eotp",
2713
+ "two-factor authentication",
2714
+ "2fa",
2715
+ "two factor"
2716
+ ];
2717
+ const isOtp = otpPatterns.some((pattern) => searchText.includes(pattern));
2718
+ return isOtp;
2615
2719
  }
2616
2720
  function promptOtpWithTimeout(timeout = 9e4) {
2617
2721
  return new Promise((resolve, reject) => {
@@ -2640,7 +2744,7 @@ async function handleOtpError() {
2640
2744
  logger.debug("OTP received, retrying publish...");
2641
2745
  return otp;
2642
2746
  } catch (promptError) {
2643
- logger.error("Failed to get OTP:", promptError);
2747
+ logger.fail("Failed to get OTP");
2644
2748
  throw promptError;
2645
2749
  }
2646
2750
  }
@@ -2663,6 +2767,7 @@ async function executePublishCommand({
2663
2767
  noStderr: true,
2664
2768
  noStdout: true,
2665
2769
  noSuccess: true,
2770
+ noError: true,
2666
2771
  logLevel: config.logLevel,
2667
2772
  cwd: pkg.path
2668
2773
  });
@@ -2743,9 +2848,302 @@ async function publishPackage({
2743
2848
  logger.error(`Failed to publish ${packageNameAndVersion}:`, error);
2744
2849
  throw error;
2745
2850
  }
2746
- } finally {
2747
- process.chdir(config.cwd);
2851
+ } finally {
2852
+ process.chdir(config.cwd);
2853
+ }
2854
+ }
2855
+ }
2856
+
2857
+ function extractChangelogSummary(changelog, maxLength = 150) {
2858
+ if (changelog.trim() === "") {
2859
+ return "";
2860
+ }
2861
+ const cleaned = changelog.split("\n").filter((line) => !line.startsWith("#")).join("\n").trim();
2862
+ let cleanedResult = cleaned;
2863
+ if (cleanedResult.endsWith("?") || cleanedResult.endsWith("!") || cleanedResult.endsWith(".")) {
2864
+ cleanedResult = cleanedResult.slice(0, -1);
2865
+ }
2866
+ const sentences = cleanedResult.split(/[.!?]\s+/);
2867
+ let summary = "";
2868
+ for (const sentence of sentences) {
2869
+ if ((summary + sentence).length > maxLength || sentence.trim() === "") {
2870
+ break;
2871
+ }
2872
+ summary += `${sentence}. `;
2873
+ }
2874
+ return summary.trim() || cleaned.substring(0, maxLength);
2875
+ }
2876
+ function getReleaseUrl(config, tag) {
2877
+ const repo = config.repo;
2878
+ if (!repo?.domain || !repo?.repo) {
2879
+ return void 0;
2880
+ }
2881
+ const provider = repo.provider || "github";
2882
+ if (provider === "github") {
2883
+ return `https://${repo.domain}/${repo.repo}/releases/tag/${tag}`;
2884
+ } else if (provider === "gitlab") {
2885
+ return `https://${repo.domain}/${repo.repo}/-/releases/${tag}`;
2886
+ } else if (provider === "bitbucket") {
2887
+ return `https://${repo.domain}/${repo.repo}/commits/tag/${tag}`;
2888
+ }
2889
+ return void 0;
2890
+ }
2891
+
2892
+ function getSlackToken(options) {
2893
+ const { socialCredentials, tokenCredential } = options;
2894
+ const token = socialCredentials?.token || tokenCredential;
2895
+ if (!token) {
2896
+ return null;
2897
+ }
2898
+ return token;
2899
+ }
2900
+ function formatChangelogForSlack(changelog, maxLength = 500) {
2901
+ let formatted = changelog.replace(/^### (.+)$/gm, "*$1*").replace(/^## (.+)$/gm, "*$1*").replace(/^# (.+)$/gm, "*$1*").replace(/\*\*(.+?)\*\*/g, "*$1*");
2902
+ const linkPattern = /\[([^\]]*)]\(([^)]*)\)/g;
2903
+ formatted = formatted.replace(linkPattern, (_, text, url) => `<${url}|${text}>`);
2904
+ if (formatted.length > maxLength) {
2905
+ formatted = `${formatted.substring(0, maxLength - 3)}...`;
2906
+ }
2907
+ return formatted;
2908
+ }
2909
+ function formatSlackMessage({ projectName, version, changelog, releaseUrl, changelogUrl, template }) {
2910
+ if (template) {
2911
+ const summary = extractChangelogSummary(changelog, 500);
2912
+ let message = template.replace("{{projectName}}", projectName).replace("{{version}}", version).replace("{{changelog}}", summary);
2913
+ if (releaseUrl) {
2914
+ message = message.replace("{{releaseUrl}}", releaseUrl);
2915
+ } else {
2916
+ message = message.replace("{{releaseUrl}}", "").trim();
2917
+ }
2918
+ if (changelogUrl) {
2919
+ message = message.replace("{{changelogUrl}}", changelogUrl);
2920
+ } else {
2921
+ message = message.replace("{{changelogUrl}}", "").trim();
2922
+ }
2923
+ return [
2924
+ {
2925
+ type: "section",
2926
+ text: {
2927
+ type: "mrkdwn",
2928
+ text: message
2929
+ }
2930
+ }
2931
+ ];
2932
+ }
2933
+ const blocks = [
2934
+ {
2935
+ type: "header",
2936
+ text: {
2937
+ type: "plain_text",
2938
+ text: `\u{1F680} ${projectName} ${version} is out!`,
2939
+ emoji: true
2940
+ }
2941
+ }
2942
+ ];
2943
+ const formattedChangelog = formatChangelogForSlack(changelog, 500);
2944
+ if (formattedChangelog) {
2945
+ blocks.push({
2946
+ type: "section",
2947
+ text: {
2948
+ type: "mrkdwn",
2949
+ text: formattedChangelog
2950
+ }
2951
+ });
2952
+ }
2953
+ blocks.push({
2954
+ type: "divider"
2955
+ });
2956
+ const elements = [];
2957
+ if (releaseUrl) {
2958
+ elements.push({
2959
+ type: "button",
2960
+ text: {
2961
+ type: "plain_text",
2962
+ text: "\u{1F4E6} View Release",
2963
+ emoji: true
2964
+ },
2965
+ url: releaseUrl,
2966
+ action_id: "view_release"
2967
+ });
2968
+ }
2969
+ if (changelogUrl) {
2970
+ elements.push({
2971
+ type: "button",
2972
+ text: {
2973
+ type: "plain_text",
2974
+ text: "\u{1F4CB} Full Changelog",
2975
+ emoji: true
2976
+ },
2977
+ url: changelogUrl,
2978
+ action_id: "view_changelog"
2979
+ });
2980
+ }
2981
+ if (elements.length > 0) {
2982
+ blocks.push({
2983
+ type: "actions",
2984
+ elements
2985
+ });
2986
+ }
2987
+ return blocks;
2988
+ }
2989
+ async function postReleaseToSlack({
2990
+ version,
2991
+ projectName,
2992
+ changelog,
2993
+ releaseUrl,
2994
+ changelogUrl,
2995
+ channel,
2996
+ token,
2997
+ template,
2998
+ dryRun = false
2999
+ }) {
3000
+ logger.debug("Preparing Slack post...");
3001
+ const blocks = formatSlackMessage({
3002
+ template,
3003
+ projectName,
3004
+ version,
3005
+ changelog,
3006
+ releaseUrl,
3007
+ changelogUrl
3008
+ });
3009
+ logger.debug(`Message blocks (${blocks.length} blocks)`);
3010
+ if (dryRun) {
3011
+ logger.info("[dry-run] Would post to Slack:", JSON.stringify(blocks, null, 2));
3012
+ return;
3013
+ }
3014
+ try {
3015
+ const { WebClient } = await import('@slack/web-api');
3016
+ const client = new WebClient(token);
3017
+ logger.debug(`Posting message to Slack channel: ${channel}`);
3018
+ const result = await client.chat.postMessage({
3019
+ channel,
3020
+ blocks,
3021
+ text: `${projectName} ${version} is out!`
3022
+ // Fallback text for notifications
3023
+ });
3024
+ logger.success(`Message posted successfully! Channel: ${result.channel}, Timestamp: ${result.ts}`);
3025
+ return result;
3026
+ } catch (error) {
3027
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.message?.includes("@slack/web-api")) {
3028
+ logger.error("Slack Web API dependency not found. Please install it with: pnpm add @slack/web-api");
3029
+ throw new Error("Missing dependency: @slack/web-api. Install it with: pnpm add @slack/web-api");
3030
+ }
3031
+ logger.error("Failed to post message:", error.message || error);
3032
+ if (error.data) {
3033
+ logger.error("Slack API error:", error.data.error);
3034
+ switch (error.data.error) {
3035
+ case "channel_not_found":
3036
+ throw new Error("Slack channel not found. Make sure the channel ID or name is correct.");
3037
+ case "not_in_channel":
3038
+ throw new Error("Bot is not in the channel. Invite the bot to the channel first.");
3039
+ case "invalid_auth":
3040
+ throw new Error("Invalid Slack token. Check your credentials.");
3041
+ case "missing_scope":
3042
+ throw new Error('Missing required OAuth scope. The bot needs "chat:write" permission.');
3043
+ default:
3044
+ throw error;
3045
+ }
3046
+ }
3047
+ throw error;
3048
+ }
3049
+ }
3050
+
3051
+ function getTwitterCredentials({ socialCredentials, tokenCredentials }) {
3052
+ const apiKey = socialCredentials?.apiKey || tokenCredentials?.apiKey;
3053
+ const apiKeySecret = socialCredentials?.apiKeySecret || tokenCredentials?.apiKeySecret;
3054
+ const accessToken = socialCredentials?.accessToken || tokenCredentials?.accessToken;
3055
+ const accessTokenSecret = socialCredentials?.accessTokenSecret || tokenCredentials?.accessTokenSecret;
3056
+ if (!apiKey || !apiKeySecret || !accessToken || !accessTokenSecret) {
3057
+ logger.warn("Twitter is enabled but credentials are missing.");
3058
+ logger.log("Set the following environment variables or configure them in social.twitter.credentials or tokens.twitter:");
3059
+ logger.log(" - TWITTER_API_KEY or RELIZY_TWITTER_API_KEY");
3060
+ logger.log(" - TWITTER_API_KEY_SECRET or RELIZY_TWITTER_API_KEY_SECRET");
3061
+ logger.log(" - TWITTER_ACCESS_TOKEN or RELIZY_TWITTER_ACCESS_TOKEN");
3062
+ logger.log(" - TWITTER_ACCESS_TOKEN_SECRET or RELIZY_TWITTER_ACCESS_TOKEN_SECRET");
3063
+ logger.info("Skipping Twitter post");
3064
+ return null;
3065
+ }
3066
+ return {
3067
+ apiKey,
3068
+ apiKeySecret,
3069
+ accessToken,
3070
+ accessTokenSecret
3071
+ };
3072
+ }
3073
+ function formatTweetMessage({ template, projectName, version, changelog, releaseUrl, changelogUrl }) {
3074
+ const TWITTER_MAX_LENGTH = 280;
3075
+ const ELLIPSIS = "...";
3076
+ let templateWithValues = template.replace("{{projectName}}", projectName).replace("{{version}}", version);
3077
+ if (releaseUrl) {
3078
+ templateWithValues = templateWithValues.replace("{{releaseUrl}}", releaseUrl);
3079
+ } else {
3080
+ templateWithValues = templateWithValues.replace("{{releaseUrl}}", "");
3081
+ }
3082
+ if (changelogUrl) {
3083
+ templateWithValues = templateWithValues.replace("{{changelogUrl}}", changelogUrl);
3084
+ } else {
3085
+ templateWithValues = templateWithValues.replace("{{changelogUrl}}", "");
3086
+ }
3087
+ const templateWithoutChangelog = templateWithValues.replace("{{changelog}}", "");
3088
+ const availableForChangelog = TWITTER_MAX_LENGTH - templateWithoutChangelog.length;
3089
+ let finalChangelog = changelog;
3090
+ if (changelog.length > availableForChangelog) {
3091
+ const maxLength = Math.max(0, availableForChangelog - ELLIPSIS.length);
3092
+ finalChangelog = changelog.substring(0, maxLength) + ELLIPSIS;
3093
+ }
3094
+ let message = templateWithValues.replace("{{changelog}}", finalChangelog).trim();
3095
+ if (message.length > TWITTER_MAX_LENGTH) {
3096
+ message = message.substring(0, TWITTER_MAX_LENGTH - ELLIPSIS.length) + ELLIPSIS;
3097
+ }
3098
+ return message;
3099
+ }
3100
+ async function postReleaseToTwitter({
3101
+ version,
3102
+ projectName,
3103
+ changelog,
3104
+ releaseUrl,
3105
+ changelogUrl,
3106
+ credentials,
3107
+ template,
3108
+ dryRun = false
3109
+ }) {
3110
+ logger.debug("Preparing Twitter post...");
3111
+ const changelogSummary = extractChangelogSummary(changelog, 150);
3112
+ const message = formatTweetMessage({
3113
+ template,
3114
+ projectName,
3115
+ version,
3116
+ changelog: changelogSummary,
3117
+ releaseUrl,
3118
+ changelogUrl
3119
+ });
3120
+ logger.debug(`Tweet message (${message.length} chars):
3121
+ ${message}`);
3122
+ if (dryRun) {
3123
+ logger.info("[dry-run] Would post tweet:", message);
3124
+ return;
3125
+ }
3126
+ try {
3127
+ const { TwitterApi } = await import('twitter-api-v2');
3128
+ const client = new TwitterApi({
3129
+ appKey: credentials.apiKey,
3130
+ appSecret: credentials.apiKeySecret,
3131
+ accessToken: credentials.accessToken,
3132
+ accessSecret: credentials.accessTokenSecret
3133
+ });
3134
+ const rwClient = client.readWrite;
3135
+ logger.debug(`Posting tweet: ${message}`);
3136
+ const tweet = await rwClient.v2.tweet(message);
3137
+ logger.info(`Tweet posted successfully! Tweet ID: ${tweet.data.id}`);
3138
+ logger.info(`Tweet URL: https://twitter.com/i/web/status/${tweet.data.id}`);
3139
+ return tweet;
3140
+ } catch (error) {
3141
+ if (error.code === "ERR_MODULE_NOT_FOUND" || error.message?.includes("twitter-api-v2")) {
3142
+ logger.error("Twitter API dependency not found. Please install it with: pnpm add twitter-api-v2");
3143
+ throw new Error("Missing dependency: twitter-api-v2. Install it with: pnpm add twitter-api-v2");
2748
3144
  }
3145
+ logger.error("Failed to post tweet:", error.message || error);
3146
+ throw error;
2749
3147
  }
2750
3148
  }
2751
3149
 
@@ -2783,7 +3181,6 @@ async function bumpUnifiedMode({
2783
3181
  logger.debug(`${currentVersion} \u2192 ${newVersion} (${config.monorepo?.versionMode || "standalone"} mode)`);
2784
3182
  const packages = await getPackages({
2785
3183
  config,
2786
- patterns: config.monorepo?.packages,
2787
3184
  suffix,
2788
3185
  force
2789
3186
  });
@@ -2804,7 +3201,8 @@ async function bumpUnifiedMode({
2804
3201
  } else {
2805
3202
  logger.info(`${packages.length === 1 ? packages[0].name : packages.length} package(s) bumped from ${currentVersion} to ${newVersion} (${config.monorepo?.versionMode || "standalone"} mode)`);
2806
3203
  }
2807
- for (const pkg of [rootPackage, ...packages]) {
3204
+ const packagesToWrite = [rootPackage, ...packages];
3205
+ for (const pkg of packagesToWrite) {
2808
3206
  writeVersion(pkg.path, newVersion, dryRun);
2809
3207
  }
2810
3208
  updateLernaVersion({
@@ -2865,7 +3263,6 @@ async function bumpSelectiveMode({
2865
3263
  logger.debug("Determining packages to bump...");
2866
3264
  const packages = await getPackages({
2867
3265
  config,
2868
- patterns: config.monorepo?.packages,
2869
3266
  suffix,
2870
3267
  force
2871
3268
  });
@@ -2896,7 +3293,8 @@ async function bumpSelectiveMode({
2896
3293
  }
2897
3294
  }
2898
3295
  logger.debug(`Writing version to ${packages.length} package(s)`);
2899
- for (const pkg of [rootPackage, ...packages]) {
3296
+ const packagesToWrite = [rootPackage, ...packages];
3297
+ for (const pkg of packagesToWrite) {
2900
3298
  writeVersion(pkg.path, newVersion, dryRun);
2901
3299
  }
2902
3300
  updateLernaVersion({
@@ -2933,7 +3331,6 @@ async function bumpIndependentMode({
2933
3331
  logger.debug("Starting bump in independent mode");
2934
3332
  const packagesToBump = await getPackages({
2935
3333
  config,
2936
- patterns: config.monorepo?.packages,
2937
3334
  suffix,
2938
3335
  force
2939
3336
  });
@@ -3005,7 +3402,7 @@ async function bump(options = {}) {
3005
3402
  checkGitStatusIfDirty();
3006
3403
  }
3007
3404
  await fetchGitTags(config.cwd);
3008
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3405
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3009
3406
  const packages = readPackages({
3010
3407
  cwd: config.cwd,
3011
3408
  patterns: config.monorepo?.packages,
@@ -3031,7 +3428,7 @@ async function bump(options = {}) {
3031
3428
  logger.success(`${dryRun ? "[dry-run] " : ""}Version bump completed (${resultLog} package${resultLog === 1 || typeof resultLog === "string" ? "" : "s"} bumped)`);
3032
3429
  } else {
3033
3430
  logger.fail("No packages to bump, no relevant commits found");
3034
- exit(1);
3431
+ process$1.exit(1);
3035
3432
  }
3036
3433
  await executeHook("success:bump", config, dryRun);
3037
3434
  return result;
@@ -3154,20 +3551,20 @@ async function changelog(options = {}) {
3154
3551
  });
3155
3552
  const dryRun = options.dryRun ?? false;
3156
3553
  logger.debug(`Dry run: ${dryRun}`);
3157
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3554
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3158
3555
  try {
3159
3556
  await executeHook("before:changelog", config, dryRun);
3160
3557
  logger.start("Start generating changelogs");
3558
+ const packages = await getPackagesOrBumpedPackages({
3559
+ config,
3560
+ bumpResult: options.bumpResult,
3561
+ suffix: options.suffix,
3562
+ force: options.force ?? false
3563
+ });
3161
3564
  if (config.changelog?.rootChangelog && config.monorepo) {
3162
3565
  if (config.monorepo.versionMode === "independent") {
3163
- const packages2 = await getPackagesOrBumpedPackages({
3164
- config,
3165
- bumpResult: options.bumpResult,
3166
- suffix: options.suffix,
3167
- force: options.force ?? false
3168
- });
3169
3566
  await generateIndependentRootChangelog({
3170
- packages: packages2,
3567
+ packages,
3171
3568
  config,
3172
3569
  dryRun
3173
3570
  });
@@ -3184,12 +3581,6 @@ async function changelog(options = {}) {
3184
3581
  logger.debug("Skipping root changelog generation");
3185
3582
  }
3186
3583
  logger.debug("Generating package changelogs...");
3187
- const packages = await getPackagesOrBumpedPackages({
3188
- config,
3189
- bumpResult: options.bumpResult,
3190
- suffix: options.suffix,
3191
- force: options.force ?? false
3192
- });
3193
3584
  logger.debug(`Processing ${packages.length} package(s)`);
3194
3585
  let generatedCount = 0;
3195
3586
  for await (const pkg of packages) {
@@ -3236,22 +3627,28 @@ async function changelog(options = {}) {
3236
3627
 
3237
3628
  function providerReleaseSafetyCheck({ config, provider }) {
3238
3629
  if (!config.safetyCheck || !config.release.providerRelease) {
3630
+ logger.debug("Safety check disabled or provider release disabled");
3239
3631
  return;
3240
3632
  }
3633
+ logger.debug("Start checking provider release config");
3241
3634
  const internalProvider = provider || config.repo?.provider || detectGitProvider();
3635
+ if (internalProvider === "bitbucket") {
3636
+ logger.warn("Bitbucket does not support releases via API");
3637
+ logger.info("Relizy will skip the release creation step for Bitbucket");
3638
+ return;
3639
+ }
3242
3640
  let token;
3243
3641
  if (internalProvider === "github") {
3244
3642
  token = config.tokens?.github || config.repo?.token;
3245
3643
  } else if (internalProvider === "gitlab") {
3246
3644
  token = config.tokens?.gitlab || config.repo?.token;
3247
3645
  } else {
3248
- logger.error(`[provider-release-safety-check] Unsupported Git provider: ${internalProvider || "unknown"}`);
3249
- process.exit(1);
3646
+ throw new Error(`Unsupported Git provider: ${internalProvider || "unknown"}`);
3250
3647
  }
3251
3648
  if (!token) {
3252
- logger.error(`[provider-release-safety-check] No token provided for ${internalProvider || "unknown"} - The release will not be published - Please refer to the documentation: https://louismazel.github.io/relizy/guide/installation#environment-setup`);
3253
- process.exit(1);
3649
+ throw new Error(`No token provided for ${internalProvider || "unknown"} - The release will not be published - Please refer to the documentation: https://louismazel.github.io/relizy/guide/installation#environment-setup`);
3254
3650
  }
3651
+ logger.info("provider release config checked successfully");
3255
3652
  }
3256
3653
  async function providerRelease(options = {}) {
3257
3654
  const config = await loadRelizyConfig({
@@ -3270,9 +3667,10 @@ async function providerRelease(options = {}) {
3270
3667
  });
3271
3668
  const dryRun = options.dryRun ?? false;
3272
3669
  logger.debug(`Dry run: ${dryRun}`);
3273
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3670
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3671
+ let detectedProvider = null;
3274
3672
  try {
3275
- const detectedProvider = options.provider || detectGitProvider();
3673
+ detectedProvider = options.provider || detectGitProvider();
3276
3674
  providerReleaseSafetyCheck({ config, provider: detectedProvider });
3277
3675
  await executeHook("before:provider-release", config, dryRun);
3278
3676
  logger.start("Start provider release");
@@ -3285,6 +3683,16 @@ async function providerRelease(options = {}) {
3285
3683
  );
3286
3684
  }
3287
3685
  let postedReleases = [];
3686
+ if (detectedProvider === "bitbucket") {
3687
+ logger.warn("\u26A0\uFE0F Bitbucket does not support releases via API");
3688
+ logger.info("Skipping release creation for Bitbucket");
3689
+ logger.info("Git tags will still be created during the commit step");
3690
+ await executeHook("success:provider-release", config, dryRun);
3691
+ return {
3692
+ detectedProvider,
3693
+ postedReleases: []
3694
+ };
3695
+ }
3288
3696
  const payload = {
3289
3697
  from: config.from || options.bumpResult?.fromTag,
3290
3698
  to: config.to,
@@ -3312,20 +3720,24 @@ async function providerRelease(options = {}) {
3312
3720
  logger.error("Error publishing releases!\n\n", error);
3313
3721
  }
3314
3722
  await executeHook("error:provider-release", config, dryRun);
3315
- throw error;
3723
+ const errorMessage = error instanceof Error ? error.message : String(error);
3724
+ return {
3725
+ detectedProvider: detectedProvider || "github",
3726
+ postedReleases: [],
3727
+ error: errorMessage
3728
+ };
3316
3729
  }
3317
3730
  }
3318
3731
 
3319
3732
  async function publishSafetyCheck({ config }) {
3320
- logger.debug("[publish-safety-check] Running publish safety check");
3321
3733
  if (!config.safetyCheck || !config.release.publish || !config.publish.safetyCheck) {
3322
- logger.debug("[publish-safety-check] Safety check disabled or publish disabled");
3734
+ logger.debug("Safety check disabled or publish disabled");
3323
3735
  return;
3324
3736
  }
3737
+ logger.debug("Start checking auth config to package registry");
3325
3738
  const packageManager = config.publish.packageManager || detectPackageManager(config.cwd);
3326
3739
  if (!packageManager) {
3327
- logger.error("[publish-safety-check] Unable to detect package manager");
3328
- process.exit(1);
3740
+ throw new Error("Unable to detect package manager");
3329
3741
  }
3330
3742
  const isPnpmOrNpm = packageManager === "pnpm" || packageManager === "npm";
3331
3743
  if (isPnpmOrNpm) {
@@ -3335,7 +3747,7 @@ async function publishSafetyCheck({ config }) {
3335
3747
  otp: config.publish.otp
3336
3748
  });
3337
3749
  try {
3338
- logger.debug("[publish-safety-check] Authenticating to package registry...");
3750
+ logger.debug("Authenticating to package registry...");
3339
3751
  await execPromise(authCommand, {
3340
3752
  cwd: config.cwd,
3341
3753
  noStderr: true,
@@ -3343,10 +3755,9 @@ async function publishSafetyCheck({ config }) {
3343
3755
  logLevel: config.logLevel,
3344
3756
  noSuccess: true
3345
3757
  });
3346
- logger.info("[publish-safety-check] Successfully authenticated to package registry");
3758
+ logger.info("Successfully authenticated to package registry");
3347
3759
  } catch (error) {
3348
- logger.error("[publish-safety-check] Failed to authenticate to package registry:", error);
3349
- process.exit(1);
3760
+ throw new Error("Failed to authenticate to package registry", { cause: error });
3350
3761
  }
3351
3762
  }
3352
3763
  }
@@ -3371,7 +3782,7 @@ async function publish(options = {}) {
3371
3782
  logger.debug(`Dry run: ${dryRun}`);
3372
3783
  const packageManager = config.publish.packageManager || detectPackageManager(config.cwd);
3373
3784
  logger.debug(`Package manager: ${packageManager}`);
3374
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3785
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3375
3786
  if (config.publish.registry) {
3376
3787
  logger.debug(`Registry: ${config.publish.registry}`);
3377
3788
  }
@@ -3418,7 +3829,7 @@ async function publish(options = {}) {
3418
3829
  config,
3419
3830
  dryRun
3420
3831
  });
3421
- for (const pkg of sortedPackages) {
3832
+ for await (const pkg of sortedPackages) {
3422
3833
  if (publishedPackages.some((p) => p.name === pkg.name)) {
3423
3834
  logger.debug(`Publishing ${getIndependentTag({ name: pkg.name, version: pkg.newVersion || pkg.version })}...`);
3424
3835
  await publishPackage({
@@ -3446,6 +3857,286 @@ async function publish(options = {}) {
3446
3857
  }
3447
3858
  }
3448
3859
 
3860
+ function socialSafetyCheck({ config }) {
3861
+ const socialMediaDisabled = !config.release.social && !config.social.twitter.enabled && !config.social.slack.enabled;
3862
+ if (!config.safetyCheck || socialMediaDisabled) {
3863
+ logger.debug("Safety check disabled or social disabled");
3864
+ return;
3865
+ }
3866
+ logger.debug("Start checking social config");
3867
+ const errors = {
3868
+ twitter: false,
3869
+ slack: false
3870
+ };
3871
+ const twitterConfig = config.social?.twitter;
3872
+ if (twitterConfig?.enabled) {
3873
+ const credentials = getTwitterCredentials({
3874
+ socialCredentials: twitterConfig.credentials,
3875
+ tokenCredentials: config.tokens?.twitter
3876
+ });
3877
+ if (!credentials) {
3878
+ errors.twitter = true;
3879
+ }
3880
+ }
3881
+ const slackConfig = config.social?.slack;
3882
+ if (slackConfig?.enabled) {
3883
+ const token = getSlackToken({
3884
+ socialCredentials: slackConfig.credentials,
3885
+ tokenCredential: config.tokens?.slack
3886
+ });
3887
+ if (!token) {
3888
+ logger.log("Slack is enabled but credentials are missing.");
3889
+ logger.log("Set the following environment variables or configure them in social.slack.credentials or tokens.slack:");
3890
+ logger.log(" - SLACK_TOKEN or RELIZY_SLACK_TOKEN");
3891
+ errors.slack = true;
3892
+ }
3893
+ if (!slackConfig.channel) {
3894
+ logger.warn("Slack is enabled but no channel is configured.");
3895
+ logger.log('Set the channel in social.slack.channel (e.g., "#releases" or "C1234567890")');
3896
+ errors.slack = true;
3897
+ }
3898
+ }
3899
+ if (errors.twitter || errors.slack) {
3900
+ throw new Error("Social config checked with errors");
3901
+ }
3902
+ logger.info("Social config checked successfully");
3903
+ }
3904
+ async function handleTwitterPost({
3905
+ config,
3906
+ changelog,
3907
+ dryRun,
3908
+ newVersion,
3909
+ tag
3910
+ }) {
3911
+ const twitterConfig = config.social?.twitter;
3912
+ if (!twitterConfig?.enabled) {
3913
+ logger.debug("Twitter posting is disabled in configuration");
3914
+ return { success: true, response: void 0 };
3915
+ }
3916
+ logger.debug("Twitter posting is enabled");
3917
+ try {
3918
+ const credentials = getTwitterCredentials({
3919
+ socialCredentials: twitterConfig.credentials,
3920
+ tokenCredentials: config.tokens?.twitter
3921
+ });
3922
+ if (!credentials) {
3923
+ return { success: false, error: "Twitter credentials not found" };
3924
+ }
3925
+ logger.debug("Credentials found \u2713");
3926
+ logger.debug("Preparing tweet for release");
3927
+ const onlyStable = twitterConfig.onlyStable;
3928
+ if (onlyStable && isPrerelease(newVersion)) {
3929
+ logger.info(`Skipping Twitter post for prerelease version ${newVersion} (social.twitter.onlyStable is enabled)`);
3930
+ return { success: true, response: void 0 };
3931
+ }
3932
+ await executeHook("before:twitter", config, dryRun);
3933
+ try {
3934
+ const rootPackageBase = readPackageJson(config.cwd);
3935
+ if (!rootPackageBase) {
3936
+ throw new Error("Failed to read root package.json");
3937
+ }
3938
+ logger.debug(`Project: ${rootPackageBase.name}`);
3939
+ const releaseUrl = getReleaseUrl(config, tag);
3940
+ logger.debug(`Release URL: ${releaseUrl || "none"}`);
3941
+ const changelogUrl = config.social?.changelogUrl;
3942
+ logger.debug(`Changelog URL: ${changelogUrl || "none"}`);
3943
+ logger.debug(`Changelog generated (${changelog.length} chars)`);
3944
+ const changelogSummary = extractChangelogSummary(changelog, 150);
3945
+ logger.debug(`Changelog summary: ${changelogSummary.substring(0, 50)}...`);
3946
+ const response = await postReleaseToTwitter({
3947
+ template: config.social.twitter.template || config.templates.twitterMessage,
3948
+ version: newVersion,
3949
+ projectName: rootPackageBase.name,
3950
+ changelog: changelogSummary,
3951
+ releaseUrl,
3952
+ changelogUrl,
3953
+ credentials,
3954
+ dryRun
3955
+ });
3956
+ await executeHook("success:twitter", config, dryRun);
3957
+ return { success: true, response };
3958
+ } catch (error) {
3959
+ await executeHook("error:twitter", config, dryRun);
3960
+ logger.error("Error posting to Twitter:", error);
3961
+ const errorMessage = error instanceof Error ? error.message : String(error);
3962
+ return { success: false, error: `Error posting to Twitter: ${errorMessage}` };
3963
+ }
3964
+ } catch (error) {
3965
+ logger.error("Error during Twitter posting:", error);
3966
+ const errorMessage = error instanceof Error ? error.message : String(error);
3967
+ return { success: false, error: `Error during Twitter posting: ${errorMessage}` };
3968
+ }
3969
+ }
3970
+ async function handleSlackPost({
3971
+ config,
3972
+ changelog,
3973
+ dryRun,
3974
+ newVersion,
3975
+ tag
3976
+ }) {
3977
+ const slackConfig = config.social?.slack;
3978
+ if (!slackConfig?.enabled) {
3979
+ logger.debug("Slack posting is disabled in configuration");
3980
+ return { success: true, response: void 0 };
3981
+ }
3982
+ logger.debug("Slack posting is enabled");
3983
+ try {
3984
+ const token = getSlackToken({
3985
+ socialCredentials: slackConfig.credentials,
3986
+ tokenCredential: config.tokens?.slack
3987
+ });
3988
+ if (!token) {
3989
+ logger.warn("Slack token not found. Set SLACK_TOKEN or RELIZY_SLACK_TOKEN environment variable or configure it in social.slack.credentials or tokens.slack.");
3990
+ logger.info("Skipping Slack post");
3991
+ return { success: false, error: "Slack token not found" };
3992
+ }
3993
+ logger.debug("Token found \u2713");
3994
+ if (!slackConfig.channel) {
3995
+ logger.warn("Slack channel not configured. Set it in social.slack.channel.");
3996
+ logger.info("Skipping Slack post");
3997
+ return { success: false, error: "Slack channel not configured" };
3998
+ }
3999
+ logger.debug(`Channel configured: ${slackConfig.channel}`);
4000
+ logger.debug(`Preparing Slack message for release: ${tag} (${newVersion})`);
4001
+ const onlyStable = slackConfig.onlyStable ?? true;
4002
+ if (onlyStable && isPrerelease(newVersion)) {
4003
+ logger.info(`Skipping Slack post for prerelease version ${newVersion} (social.slack.onlyStable is enabled)`);
4004
+ return { success: true, response: void 0 };
4005
+ }
4006
+ try {
4007
+ await executeHook("before:slack", config, dryRun);
4008
+ const rootPackageBase = readPackageJson(config.cwd);
4009
+ if (!rootPackageBase) {
4010
+ throw new Error("Failed to read root package.json");
4011
+ }
4012
+ logger.debug(`Project: ${rootPackageBase.name}`);
4013
+ const releaseUrl = getReleaseUrl(config, tag);
4014
+ logger.debug(`Release URL: ${releaseUrl || "none"}`);
4015
+ const changelogUrl = config.social?.changelogUrl;
4016
+ logger.debug(`Changelog URL: ${changelogUrl || "none"}`);
4017
+ logger.debug(`Changelog generated (${changelog.length} chars)`);
4018
+ const template = slackConfig.template || config.templates.slackMessage;
4019
+ const response = await postReleaseToSlack({
4020
+ version: newVersion,
4021
+ projectName: rootPackageBase.name,
4022
+ changelog,
4023
+ releaseUrl,
4024
+ changelogUrl,
4025
+ channel: slackConfig.channel,
4026
+ token,
4027
+ template,
4028
+ dryRun
4029
+ });
4030
+ await executeHook("success:slack", config, dryRun);
4031
+ return { success: true, response };
4032
+ } catch (error) {
4033
+ await executeHook("error:slack", config, dryRun);
4034
+ logger.error("Error posting to Slack:", error);
4035
+ const errorMessage = error instanceof Error ? error.message : String(error);
4036
+ return { success: false, error: `Error posting to Slack: ${errorMessage}` };
4037
+ }
4038
+ } catch (error) {
4039
+ logger.error("Error during Slack posting:", error);
4040
+ const errorMessage = error instanceof Error ? error.message : String(error);
4041
+ return { success: false, error: `Error during Slack posting: ${errorMessage}` };
4042
+ }
4043
+ }
4044
+ async function social(options = {}) {
4045
+ try {
4046
+ const dryRun = options.dryRun ?? false;
4047
+ logger.debug(`Dry run: ${dryRun}`);
4048
+ const config = await loadRelizyConfig({
4049
+ configFile: options.configName,
4050
+ baseConfig: options.config,
4051
+ overrides: {
4052
+ from: options.from,
4053
+ to: options.to,
4054
+ logLevel: options.logLevel
4055
+ }
4056
+ });
4057
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
4058
+ socialSafetyCheck({ config });
4059
+ if (!config.release.social && !config.social?.twitter?.enabled && !config.social?.slack?.enabled) {
4060
+ logger.warn("Social media posting is disabled in configuration.");
4061
+ logger.info("Enable it with release.social: true or social.twitter.enabled: true or social.slack.enabled: true");
4062
+ return { results: [], hasErrors: false };
4063
+ }
4064
+ await executeHook("before:social", config, dryRun);
4065
+ const rootPackageRead = readPackageJson(config.cwd);
4066
+ if (!rootPackageRead) {
4067
+ throw new Error("Failed to read root package.json");
4068
+ }
4069
+ const newVersion = options.bumpResult?.newVersion || rootPackageRead.version;
4070
+ const { from, to } = await resolveTags({
4071
+ config,
4072
+ step: "social",
4073
+ newVersion,
4074
+ pkg: rootPackageRead
4075
+ });
4076
+ const fromTag = options.bumpResult?.fromTag || from;
4077
+ const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
4078
+ config,
4079
+ force: false,
4080
+ suffix: void 0,
4081
+ changelog: true,
4082
+ from: fromTag,
4083
+ to
4084
+ });
4085
+ const changelog = await generateChangelog({
4086
+ pkg: rootPackage,
4087
+ config,
4088
+ dryRun,
4089
+ newVersion,
4090
+ minify: true
4091
+ });
4092
+ const twitterResponse = await handleTwitterPost({
4093
+ config,
4094
+ changelog,
4095
+ dryRun,
4096
+ newVersion,
4097
+ tag: to
4098
+ });
4099
+ const slackResponse = await handleSlackPost({
4100
+ config,
4101
+ changelog,
4102
+ dryRun,
4103
+ newVersion,
4104
+ tag: to
4105
+ });
4106
+ const results = [];
4107
+ if (config.social?.twitter?.enabled) {
4108
+ results.push({
4109
+ platform: "twitter",
4110
+ success: twitterResponse.success,
4111
+ error: twitterResponse.success ? void 0 : twitterResponse.error
4112
+ });
4113
+ }
4114
+ if (config.social?.slack?.enabled) {
4115
+ results.push({
4116
+ platform: "slack",
4117
+ success: slackResponse.success,
4118
+ error: slackResponse.success ? void 0 : slackResponse.error
4119
+ });
4120
+ }
4121
+ const hasErrors = results.some((r) => !r.success);
4122
+ if (hasErrors) {
4123
+ await executeHook("error:social", config, dryRun);
4124
+ logger.warn("Some social media posts failed");
4125
+ } else {
4126
+ logger.success("Social media posts completed!");
4127
+ await executeHook("success:social", config, dryRun);
4128
+ }
4129
+ return { results, hasErrors };
4130
+ } catch (error) {
4131
+ logger.error("Error during social media posting:", error);
4132
+ const errorMessage = error instanceof Error ? error.message : String(error);
4133
+ return {
4134
+ results: [{ platform: "unknown", success: false, error: errorMessage }],
4135
+ hasErrors: true
4136
+ };
4137
+ }
4138
+ }
4139
+
3449
4140
  function getReleaseConfig(options = {}) {
3450
4141
  return loadRelizyConfig({
3451
4142
  configFile: options.configName,
@@ -3483,7 +4174,8 @@ function getReleaseConfig(options = {}) {
3483
4174
  noVerify: options.noVerify,
3484
4175
  providerRelease: options.providerRelease,
3485
4176
  clean: options.clean,
3486
- gitTag: options.gitTag
4177
+ gitTag: options.gitTag,
4178
+ social: options.social
3487
4179
  },
3488
4180
  safetyCheck: options.safetyCheck
3489
4181
  }
@@ -3496,8 +4188,19 @@ async function releaseSafetyCheck({
3496
4188
  if (!config.safetyCheck) {
3497
4189
  return;
3498
4190
  }
3499
- providerReleaseSafetyCheck({ config, provider });
3500
- await publishSafetyCheck({ config });
4191
+ logger.box("Safety checks");
4192
+ logger.start("Start safety checks");
4193
+ try {
4194
+ await Promise.all([
4195
+ providerReleaseSafetyCheck({ config, provider }),
4196
+ publishSafetyCheck({ config }),
4197
+ socialSafetyCheck({ config })
4198
+ ]);
4199
+ logger.success("Safety checks passed");
4200
+ } catch (error) {
4201
+ logger.error("Safety checks failed");
4202
+ throw error;
4203
+ }
3501
4204
  }
3502
4205
  async function release(options = {}) {
3503
4206
  const dryRun = options.dryRun ?? false;
@@ -3510,7 +4213,7 @@ async function release(options = {}) {
3510
4213
  await releaseSafetyCheck({ config, provider: options.provider });
3511
4214
  try {
3512
4215
  await executeHook("before:release", config, dryRun);
3513
- logger.box("Step 1/6: Bump versions");
4216
+ logger.box("Bump versions");
3514
4217
  const bumpResult = await bump({
3515
4218
  type: config.bump.type,
3516
4219
  preid: config.bump.preid,
@@ -3525,7 +4228,7 @@ async function release(options = {}) {
3525
4228
  logger.debug("No packages bumped");
3526
4229
  return;
3527
4230
  }
3528
- logger.box("Step 2/6: Generate changelogs");
4231
+ logger.box("Generate changelogs");
3529
4232
  if (config.release.changelog) {
3530
4233
  await changelog({
3531
4234
  from: config.from,
@@ -3543,7 +4246,32 @@ async function release(options = {}) {
3543
4246
  } else {
3544
4247
  logger.info("Skipping changelog generation (--no-changelog)");
3545
4248
  }
3546
- logger.box("Step 3/6: Commit changes and create tag");
4249
+ logger.box("Publish packages to registry");
4250
+ let publishResponse;
4251
+ if (config.release.publish) {
4252
+ try {
4253
+ publishResponse = await publish({
4254
+ registry: config.publish.registry,
4255
+ tag: config.publish.tag,
4256
+ access: config.publish.access,
4257
+ otp: config.publish.otp,
4258
+ bumpResult,
4259
+ dryRun,
4260
+ config,
4261
+ configName: options.configName,
4262
+ suffix: options.suffix,
4263
+ force,
4264
+ safetyCheck: false
4265
+ });
4266
+ } catch (error) {
4267
+ logger.fail("Publish failed, rolling back modified files...");
4268
+ await rollbackModifiedFiles({ config });
4269
+ throw error;
4270
+ }
4271
+ } else {
4272
+ logger.info("Skipping publish (--no-publish)");
4273
+ }
4274
+ logger.box("Commit changes and create tag");
3547
4275
  let createdTags = [];
3548
4276
  if (config.release.commit) {
3549
4277
  createdTags = await createCommitAndTags({
@@ -3557,7 +4285,7 @@ async function release(options = {}) {
3557
4285
  } else {
3558
4286
  logger.info("Skipping commit and tag (--no-commit)");
3559
4287
  }
3560
- logger.box("Step 4/6: Push changes and tags");
4288
+ logger.box("Push changes and tags");
3561
4289
  if (config.release.push && config.release.commit) {
3562
4290
  await executeHook("before:push", config, dryRun);
3563
4291
  try {
@@ -3575,60 +4303,77 @@ async function release(options = {}) {
3575
4303
  } else {
3576
4304
  logger.info("Skipping push (--no-push or --no-commit)");
3577
4305
  }
3578
- logger.box("Step 5/6: Publish packages to registry");
3579
- let publishResponse;
3580
- if (config.release.publish) {
3581
- publishResponse = await publish({
3582
- registry: config.publish.registry,
3583
- tag: config.publish.tag,
3584
- access: config.publish.access,
3585
- otp: config.publish.otp,
3586
- bumpResult,
4306
+ let provider = config.repo?.provider;
4307
+ let postedReleases = [];
4308
+ let providerError;
4309
+ logger.box("Publish Git release");
4310
+ if (config.release.providerRelease) {
4311
+ logger.debug(`Provider from config: ${provider}`);
4312
+ const response = await providerRelease({
4313
+ from: config.from,
4314
+ to: config.to,
4315
+ token: options.token,
4316
+ provider,
3587
4317
  dryRun,
3588
4318
  config,
4319
+ logLevel: config.logLevel,
4320
+ bumpResult,
3589
4321
  configName: options.configName,
4322
+ force,
3590
4323
  suffix: options.suffix,
3591
- force
4324
+ safetyCheck: false
3592
4325
  });
4326
+ provider = response.detectedProvider;
4327
+ postedReleases = response.postedReleases;
4328
+ providerError = response.error;
3593
4329
  } else {
3594
- logger.info("Skipping publish (--no-publish)");
4330
+ logger.info("Skipping release (--no-provider-release)");
3595
4331
  }
3596
- let provider = config.repo?.provider;
3597
- let postedReleases = [];
3598
- logger.box("Step 6/6: Publish Git release");
3599
- if (config.release.providerRelease) {
3600
- logger.debug(`Provider from config: ${provider}`);
3601
- try {
3602
- const response = await providerRelease({
3603
- from: config.from,
3604
- to: config.to,
3605
- token: options.token,
3606
- provider,
3607
- dryRun,
3608
- config,
3609
- logLevel: config.logLevel,
3610
- bumpResult,
3611
- configName: options.configName,
3612
- force,
3613
- suffix: options.suffix
3614
- });
3615
- provider = response.detectedProvider;
3616
- postedReleases = response.postedReleases;
3617
- } catch (error) {
3618
- logger.error("Error during release publication:", error);
3619
- }
4332
+ logger.box("Post release to social media");
4333
+ let socialResults;
4334
+ if (config.release.social && (config.social?.twitter?.enabled || config.social?.slack?.enabled)) {
4335
+ socialResults = await social({
4336
+ from: config.from,
4337
+ to: config.to,
4338
+ config,
4339
+ configName: options.configName,
4340
+ bumpResult,
4341
+ dryRun,
4342
+ logLevel: config.logLevel,
4343
+ safetyCheck: false
4344
+ // Already checked in releaseSafetyCheck
4345
+ });
3620
4346
  } else {
3621
- logger.info("Skipping release (--no-provider-release)");
4347
+ logger.info("Skipping social media posts (--no-social or no social media enabled)");
3622
4348
  }
3623
4349
  const publishedPackageCount = publishResponse?.publishedPackages.length ?? 0;
3624
4350
  const versionDisplay = config.monorepo?.versionMode === "independent" ? `${bumpResult.bumpedPackages.length} packages bumped independently` : bumpResult.newVersion || readPackageJson(config.cwd)?.version;
4351
+ let providerDisplay = "Disabled";
4352
+ if (config.release.providerRelease) {
4353
+ if (providerError) {
4354
+ providerDisplay = `Failed: ${providerError}`;
4355
+ } else {
4356
+ providerDisplay = `${postedReleases.length} release${postedReleases.length !== 1 ? "s" : ""}`;
4357
+ }
4358
+ }
4359
+ let socialDisplay = "Disabled";
4360
+ if (config.release.social && socialResults) {
4361
+ if (socialResults.hasErrors) {
4362
+ const failed = socialResults.results.filter((r) => !r.success).map((r) => r.platform);
4363
+ const succeeded = socialResults.results.filter((r) => r.success).map((r) => r.platform);
4364
+ socialDisplay = `${succeeded.length} succeeded, ${failed.length} failed (${failed.join(", ")})`;
4365
+ } else {
4366
+ socialDisplay = `${socialResults.results.length} succeeded`;
4367
+ }
4368
+ }
3625
4369
  logger.box(`Release workflow completed!
3626
4370
 
3627
4371
  Version: ${versionDisplay ?? "Unknown"}
3628
- Tag(s): ${createdTags.length ? createdTags.join(", ") : "No"}
4372
+ Tag(s): ${createdTags?.length ? createdTags.join(", ") : "None"}
3629
4373
  Pushed: ${config.release.push ? "Yes" : "Disabled"}
3630
4374
  Published packages: ${config.release.publish ? publishedPackageCount : "Disabled"}
3631
- Published release: ${config.release.providerRelease ? postedReleases.length : "Disabled"}
4375
+ Provider release: ${providerDisplay}
4376
+ Social media: ${socialDisplay}
3632
4377
  Git provider: ${provider}`);
3633
4378
  await executeHook("success:release", config, dryRun);
3634
4379
  } catch (error) {
@@ -3638,4 +4383,4 @@ Git provider: ${provider}`);
3638
4383
  }
3639
4384
  }
3640
4385
 
3641
- export { getPackagesOrBumpedPackages as $, github as A, createGitlabRelease as B, gitlab as C, detectPackageManager as D, determinePublishTag as E, getPackagesToPublishInSelectiveMode as F, getPackagesToPublishInIndependentMode as G, getAuthCommand as H, publishPackage as I, readPackageJson as J, getRootPackage as K, readPackages as L, getPackages as M, getPackageCommits as N, hasLernaJson as O, getIndependentTag as P, getLastStableTag as Q, getLastTag as R, getLastRepoTag as S, getLastPackageTag as T, resolveTags as U, executeHook as V, isInCI as W, getCIName as X, executeFormatCmd as Y, executeBuildCmd as Z, isBumpedPackage as _, providerRelease as a, isGraduatingToStableBetweenVersion as a0, determineSemverChange as a1, determineReleaseType as a2, writeVersion as a3, getPackageNewVersion as a4, updateLernaVersion as a5, extractVersionFromPackageTag as a6, isPrerelease as a7, isStableReleaseType as a8, isPrereleaseReleaseType as a9, isGraduating as aa, getPreid as ab, isChangedPreid as ac, getBumpedPackageIndependently as ad, confirmBump as ae, getBumpedIndependentPackages as af, shouldFilterPrereleaseTags as ag, extractVersionFromTag as ah, isTagVersionCompatibleWithCurrent as ai, bump as b, changelog as c, publishSafetyCheck as d, publish as e, getDefaultConfig as f, generateChangelog as g, defineConfig as h, getPackageDependencies as i, getDependentsOf as j, expandPackagesToBumpWithDependents as k, loadRelizyConfig as l, getGitStatus as m, checkGitStatusIfDirty as n, fetchGitTags as o, providerReleaseSafetyCheck as p, detectGitProvider as q, release as r, parseGitRemoteUrl as s, topologicalSort as t, createCommitAndTags as u, pushCommitAndTags as v, writeChangelogToFile as w, getFirstCommit as x, getCurrentGitBranch as y, getCurrentGitRef as z };
4386
+ export { getLastTag as $, rollbackModifiedFiles as A, getFirstCommit as B, getCurrentGitBranch as C, getCurrentGitRef as D, github as E, createGitlabRelease as F, gitlab as G, detectPackageManager as H, determinePublishTag as I, getPackagesToPublishInSelectiveMode as J, getPackagesToPublishInIndependentMode as K, getAuthCommand as L, publishPackage as M, readPackageJson as N, getRootPackage as O, readPackages as P, getPackages as Q, getPackageCommits as R, hasLernaJson as S, getSlackToken as T, formatChangelogForSlack as U, formatSlackMessage as V, postReleaseToSlack as W, extractChangelogSummary as X, getReleaseUrl as Y, getIndependentTag as Z, getLastStableTag as _, providerRelease as a, getLastRepoTag as a0, getLastPackageTag as a1, resolveTags as a2, getTwitterCredentials as a3, formatTweetMessage as a4, postReleaseToTwitter as a5, executeHook as a6, isInCI as a7, getCIName as a8, executeFormatCmd as a9, executeBuildCmd as aa, isBumpedPackage as ab, getPackagesOrBumpedPackages as ac, isGraduatingToStableBetweenVersion as ad, determineSemverChange as ae, determineReleaseType as af, writeVersion as ag, getPackageNewVersion as ah, updateLernaVersion as ai, extractVersionFromPackageTag as aj, isPrerelease as ak, isStableReleaseType as al, isPrereleaseReleaseType as am, isGraduating as an, getPreid as ao, isChangedPreid as ap, getBumpedPackageIndependently as aq, confirmBump as ar, getBumpedIndependentPackages as as, shouldFilterPrereleaseTags as at, extractVersionFromTag as au, isTagVersionCompatibleWithCurrent as av, bump as b, changelog as c, publishSafetyCheck as d, publish as e, social as f, generateChangelog as g, getDefaultConfig as h, defineConfig as i, getPackageDependencies as j, getDependentsOf as k, loadRelizyConfig as l, expandPackagesToBumpWithDependents as m, getGitStatus as n, checkGitStatusIfDirty as o, providerReleaseSafetyCheck as p, fetchGitTags as q, release as r, socialSafetyCheck as s, topologicalSort as t, detectGitProvider as u, parseGitRemoteUrl as v, writeChangelogToFile as w, getModifiedReleaseFilePatterns as x, createCommitAndTags as y, pushCommitAndTags as z };