relizy 0.3.0 → 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,
@@ -476,7 +476,7 @@ async function executeHook(hook, config, dryRun, params) {
476
476
  logger.info(`Executing hook ${hook}`);
477
477
  const result = await hookInput(config, dryRun, params);
478
478
  if (result)
479
- logger.debug(`Hook ${hook} returned: ${result}`);
479
+ logger.debug(`Hook ${hook} returned: ${typeof result === "object" ? JSON.stringify(result) : result}`);
480
480
  logger.info(`Hook ${hook} executed`);
481
481
  return result;
482
482
  }
@@ -489,62 +489,62 @@ async function executeHook(hook, config, dryRun, params) {
489
489
  noStdout: true
490
490
  });
491
491
  if (result)
492
- logger.debug(`Hook ${hook} returned: ${result}`);
492
+ logger.debug(`Hook ${hook} returned: ${typeof result === "object" ? JSON.stringify(result) : result}`);
493
493
  logger.info(`Hook ${hook} executed`);
494
494
  return result;
495
495
  }
496
496
  }
497
497
  function isInCI() {
498
498
  return Boolean(
499
- 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"
500
500
  );
501
501
  }
502
502
  function getCIName() {
503
- if (process.env.GITHUB_ACTIONS === "true")
503
+ if (process$1.env.GITHUB_ACTIONS === "true")
504
504
  return "GitHub Actions";
505
- if (process.env.GITLAB_CI === "true")
505
+ if (process$1.env.GITLAB_CI === "true")
506
506
  return "GitLab CI";
507
- if (process.env.CIRCLECI === "true")
507
+ if (process$1.env.CIRCLECI === "true")
508
508
  return "CircleCI";
509
- if (process.env.TRAVIS === "true")
509
+ if (process$1.env.TRAVIS === "true")
510
510
  return "Travis CI";
511
- if (process.env.JENKINS_HOME || process.env.JENKINS_URL)
511
+ if (process$1.env.JENKINS_HOME || process$1.env.JENKINS_URL)
512
512
  return "Jenkins";
513
- if (process.env.TF_BUILD === "True")
513
+ if (process$1.env.TF_BUILD === "True")
514
514
  return "Azure Pipelines";
515
- if (process.env.TEAMCITY_VERSION)
515
+ if (process$1.env.TEAMCITY_VERSION)
516
516
  return "TeamCity";
517
- if (process.env.BITBUCKET_BUILD_NUMBER)
517
+ if (process$1.env.BITBUCKET_BUILD_NUMBER)
518
518
  return "Bitbucket Pipelines";
519
- if (process.env.DRONE === "true")
519
+ if (process$1.env.DRONE === "true")
520
520
  return "Drone";
521
- if (process.env.APPVEYOR)
521
+ if (process$1.env.APPVEYOR)
522
522
  return "AppVeyor";
523
- if (process.env.BUILDKITE === "true")
523
+ if (process$1.env.BUILDKITE === "true")
524
524
  return "Buildkite";
525
- if (process.env.CODEBUILD_BUILD_ID)
525
+ if (process$1.env.CODEBUILD_BUILD_ID)
526
526
  return "AWS CodeBuild";
527
- if (process.env.NETLIFY === "true")
527
+ if (process$1.env.NETLIFY === "true")
528
528
  return "Netlify";
529
- if (process.env.VERCEL === "1")
529
+ if (process$1.env.VERCEL === "1")
530
530
  return "Vercel";
531
- if (process.env.HEROKU_TEST_RUN_ID)
531
+ if (process$1.env.HEROKU_TEST_RUN_ID)
532
532
  return "Heroku CI";
533
- if (process.env.BUDDY === "true")
533
+ if (process$1.env.BUDDY === "true")
534
534
  return "Buddy";
535
- if (process.env.SEMAPHORE === "true")
535
+ if (process$1.env.SEMAPHORE === "true")
536
536
  return "Semaphore";
537
- if (process.env.CF_BUILD_ID)
537
+ if (process$1.env.CF_BUILD_ID)
538
538
  return "Codefresh";
539
- if (process.env.bamboo_buildKey)
539
+ if (process$1.env.bamboo_buildKey)
540
540
  return "Bamboo";
541
- if (process.env.BUILD_ID && process.env.PROJECT_ID)
541
+ if (process$1.env.BUILD_ID && process$1.env.PROJECT_ID)
542
542
  return "Google Cloud Build";
543
- if (process.env.SCREWDRIVER === "true")
543
+ if (process$1.env.SCREWDRIVER === "true")
544
544
  return "Screwdriver";
545
- if (process.env.STRIDER === "true")
545
+ if (process$1.env.STRIDER === "true")
546
546
  return "Strider";
547
- if (process.env.CI === "true")
547
+ if (process$1.env.CI === "true")
548
548
  return "Unknown CI";
549
549
  return null;
550
550
  }
@@ -610,7 +610,6 @@ async function getPackagesOrBumpedPackages({
610
610
  }
611
611
  return await getPackages({
612
612
  config,
613
- patterns: config.monorepo?.packages,
614
613
  suffix,
615
614
  force
616
615
  });
@@ -663,6 +662,9 @@ function detectGitProvider(cwd = process.cwd()) {
663
662
  if (remoteUrl.includes("gitlab.com") || remoteUrl.includes("gitlab")) {
664
663
  return "gitlab";
665
664
  }
665
+ if (remoteUrl.includes("bitbucket.org") || remoteUrl.includes("bitbucket")) {
666
+ return "bitbucket";
667
+ }
666
668
  return null;
667
669
  } catch {
668
670
  return null;
@@ -670,7 +672,7 @@ function detectGitProvider(cwd = process.cwd()) {
670
672
  }
671
673
  function parseGitRemoteUrl(remoteUrl) {
672
674
  const sshRegex = /git@[\w.-]+:([\w.-]+)\/([\w.-]+?)(?:\.git)?$/;
673
- const httpsRegex = /https?:\/\/[\w.-]+\/([\w.-]+)\/([\w.-]+?)(?:\.git)?$/;
675
+ const httpsRegex = /https?:\/\/[\w.-]+\/(.+?)\/([^/]+?)(?:\.git)?$/;
674
676
  const sshMatch = remoteUrl.match(sshRegex);
675
677
  if (sshMatch) {
676
678
  return {
@@ -906,7 +908,8 @@ async function generateMarkDown({
906
908
  config,
907
909
  from,
908
910
  to,
909
- isFirstCommit
911
+ isFirstCommit,
912
+ minify
910
913
  }) {
911
914
  const typeGroups = groupBy(commits, "type");
912
915
  const markdown = [];
@@ -919,7 +922,7 @@ async function generateMarkDown({
919
922
  const versionTitle = updatedConfig.to;
920
923
  const changelogTitle = `${updatedConfig.from}...${updatedConfig.to}`;
921
924
  markdown.push("", `## ${changelogTitle}`, "");
922
- if (updatedConfig.repo && updatedConfig.from && versionTitle) {
925
+ if (updatedConfig.repo && updatedConfig.from && versionTitle && !minify) {
923
926
  const formattedCompareLink = formatCompareChanges(versionTitle, {
924
927
  ...updatedConfig,
925
928
  from: isFirstCommit ? getFirstCommit(updatedConfig.cwd) : updatedConfig.from
@@ -936,7 +939,11 @@ async function generateMarkDown({
936
939
  }
937
940
  markdown.push("", `### ${updatedConfig.types[type]?.title}`, "");
938
941
  for (const commit of group.reverse()) {
939
- const line = formatCommit(commit, updatedConfig);
942
+ const line = formatCommit({
943
+ commit,
944
+ config: updatedConfig,
945
+ minify
946
+ });
940
947
  markdown.push(line);
941
948
  if (commit.isBreaking) {
942
949
  breakingChanges.push(line);
@@ -948,7 +955,7 @@ async function generateMarkDown({
948
955
  }
949
956
  const _authors = /* @__PURE__ */ new Map();
950
957
  for (const commit of commits) {
951
- if (!commit.author) {
958
+ if (!commit.author || minify) {
952
959
  continue;
953
960
  }
954
961
  const name = formatName(commit.author.name);
@@ -1028,9 +1035,9 @@ function getCommitBody(commit) {
1028
1035
  ${indentedBody}
1029
1036
  `;
1030
1037
  }
1031
- function formatCommit(commit, config) {
1032
- const body = config.changelog.includeCommitBody ? getCommitBody(commit) : "";
1033
- 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}`;
1034
1041
  }
1035
1042
  function formatReferences(references, config) {
1036
1043
  const pr = references.filter((ref) => ref.type === "pull-request");
@@ -1055,1475 +1062,1500 @@ function groupBy(items, key) {
1055
1062
  return groups;
1056
1063
  }
1057
1064
 
1058
- function fromTagIsFirstCommit(fromTag, cwd) {
1059
- 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;
1060
1070
  }
1061
- async function generateChangelog({
1062
- pkg,
1063
- config,
1064
- dryRun,
1065
- 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
1066
1157
  }) {
1067
- let fromTag = config.from || pkg.fromTag || getFirstCommit(config.cwd);
1068
- const isFirstCommit = fromTagIsFirstCommit(fromTag, config.cwd);
1069
- if (isFirstCommit) {
1070
- 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}`);
1071
1164
  }
1072
- let toTag = config.to;
1073
- if (!toTag) {
1074
- 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');
1075
1177
  }
1076
- if (!toTag) {
1077
- 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;
1078
1182
  }
1079
- logger.debug(`Generating changelog for ${pkg.name} - from ${fromTag} to ${toTag}`);
1080
- try {
1081
- config = {
1082
- ...config,
1083
- from: fromTag,
1084
- to: toTag
1085
- };
1086
- const generatedChangelog = await generateMarkDown({
1087
- commits: pkg.commits,
1088
- config,
1089
- from: fromTag,
1090
- isFirstCommit,
1091
- to: toTag
1092
- });
1093
- let changelog = generatedChangelog;
1094
- if (pkg.commits.length === 0) {
1095
- changelog = `${changelog}
1096
-
1097
- ${config.templates.emptyChangelogContent}`;
1183
+ const isCurrentPrerelease = isPrerelease(currentVersion);
1184
+ if (!isCurrentPrerelease) {
1185
+ if (releaseType === "release") {
1186
+ return handleStableVersionWithReleaseType(commits, types, force);
1098
1187
  }
1099
- const changelogResult = await executeHook("generate:changelog", config, dryRun, {
1100
- commits: pkg.commits,
1101
- changelog
1102
- });
1103
- changelog = changelogResult || changelog;
1104
- logger.verbose(`Output changelog for ${pkg.name}:
1105
- ${changelog}`);
1106
- logger.debug(`Changelog generated for ${pkg.name} (${pkg.commits.length} commits)`);
1107
- logger.verbose(`Final changelog for ${pkg.name}:
1108
-
1109
- ${changelog}
1110
-
1111
- `);
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;
1112
1209
  if (dryRun) {
1113
- logger.info(`[dry-run] ${pkg.name} - Generate changelog ${fromTag}...${toTag}`);
1210
+ logger.debug(`[dry-run] Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1211
+ return;
1114
1212
  }
1115
- return changelog;
1213
+ writeFileSync(packageJsonPath, `${formatJson(packageJson)}
1214
+ `, "utf8");
1215
+ logger.debug(`Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1116
1216
  } catch (error) {
1117
- throw new Error(`Error generating changelog for ${pkg.name} (${fromTag}...${toTag}): ${error}`);
1217
+ throw new Error(`Unable to write version to ${packageJsonPath}: ${error}`);
1118
1218
  }
1119
1219
  }
1120
- function writeChangelogToFile({
1121
- cwd,
1122
- pkg,
1123
- changelog,
1124
- dryRun = false
1220
+ function getPackageNewVersion({
1221
+ name,
1222
+ currentVersion,
1223
+ releaseType,
1224
+ preid,
1225
+ suffix
1125
1226
  }) {
1126
- const changelogPath = join(pkg.path, "CHANGELOG.md");
1127
- let existingChangelog = "";
1128
- if (existsSync(changelogPath)) {
1129
- 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)`);
1130
1232
  }
1131
- const lines = existingChangelog.split("\n");
1132
- const titleIndex = lines.findIndex((line) => line.startsWith("# "));
1133
- let updatedChangelog;
1134
- if (titleIndex !== -1) {
1135
- const beforeTitle = lines.slice(0, titleIndex + 1);
1136
- const afterTitle = lines.slice(titleIndex + 1);
1137
- updatedChangelog = [...beforeTitle, "", changelog, "", ...afterTitle].join("\n");
1138
- } else {
1139
- const title = "# Changelog\n";
1140
- updatedChangelog = `${title}
1141
- ${changelog}
1142
- ${existingChangelog}`;
1233
+ if (isPrereleaseReleaseType(releaseType) && suffix) {
1234
+ newVersion = newVersion.replace(/\.(\d+)$/, `.${suffix}`);
1143
1235
  }
1144
- if (dryRun) {
1145
- const relativeChangelogPath = relative(cwd, changelogPath);
1146
- logger.info(`[dry-run] ${pkg.name} - Write changelog to ${relativeChangelogPath}`);
1147
- } else {
1148
- logger.debug(`Writing changelog to ${changelogPath}`);
1149
- writeFileSync(changelogPath, updatedChangelog, "utf8");
1150
- logger.info(`Changelog updated for ${pkg.name} (${"newVersion" in pkg && pkg.newVersion || pkg.version})`);
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`);
1151
1239
  }
1240
+ if (isGraduating(currentVersion, releaseType)) {
1241
+ logger.info(`Graduating "${name}" from prerelease ${currentVersion} to stable ${newVersion}`);
1242
+ }
1243
+ if (isChangedPreid(currentVersion, preid)) {
1244
+ logger.debug(`Graduating "${name}" from ${getPreid(currentVersion)} to ${preid}`);
1245
+ }
1246
+ return newVersion;
1152
1247
  }
1153
-
1154
- function getDefaultConfig() {
1155
- return {
1156
- cwd: process$1.cwd(),
1157
- types: {
1158
- feat: { title: "\u{1F680} Enhancements", semver: "minor" },
1159
- perf: { title: "\u{1F525} Performance", semver: "patch" },
1160
- fix: { title: "\u{1FA79} Fixes", semver: "patch" },
1161
- refactor: { title: "\u{1F485} Refactors", semver: "patch" },
1162
- docs: { title: "\u{1F4D6} Documentation", semver: "patch" },
1163
- build: { title: "\u{1F4E6} Build", semver: "patch" },
1164
- types: { title: "\u{1F30A} Types", semver: "patch" },
1165
- chore: { title: "\u{1F3E1} Chore" },
1166
- examples: { title: "\u{1F3C0} Examples" },
1167
- test: { title: "\u2705 Tests" },
1168
- style: { title: "\u{1F3A8} Styles" },
1169
- ci: { title: "\u{1F916} CI" }
1170
- },
1171
- templates: {
1172
- commitMessage: "chore(release): bump version to {{newVersion}}",
1173
- tagMessage: "Bump version to {{newVersion}}",
1174
- tagBody: "v{{newVersion}}",
1175
- emptyChangelogContent: "No relevant changes for this release"
1176
- },
1177
- excludeAuthors: [],
1178
- noAuthors: false,
1179
- bump: {
1180
- type: "release",
1181
- clean: true,
1182
- dependencyTypes: ["dependencies"],
1183
- yes: false
1184
- },
1185
- changelog: {
1186
- rootChangelog: true,
1187
- includeCommitBody: true
1188
- },
1189
- publish: {
1190
- private: false,
1191
- args: [],
1192
- token: process$1.env.RELIZY_NPM_TOKEN || process$1.env.NPM_TOKEN || process$1.env.NODE_AUTH_TOKEN,
1193
- registry: "https://registry.npmjs.org/",
1194
- safetyCheck: false
1195
- },
1196
- tokens: {
1197
- registry: process$1.env.RELIZY_NPM_TOKEN || process$1.env.NPM_TOKEN || process$1.env.NODE_AUTH_TOKEN,
1198
- gitlab: process$1.env.RELIZY_GITLAB_TOKEN || process$1.env.GITLAB_TOKEN || process$1.env.GITLAB_API_TOKEN || process$1.env.CI_JOB_TOKEN,
1199
- github: process$1.env.RELIZY_GITHUB_TOKEN || process$1.env.GITHUB_TOKEN || process$1.env.GH_TOKEN
1200
- },
1201
- scopeMap: {},
1202
- release: {
1203
- commit: true,
1204
- publish: true,
1205
- changelog: true,
1206
- push: true,
1207
- clean: true,
1208
- providerRelease: true,
1209
- noVerify: false,
1210
- gitTag: true
1211
- },
1212
- logLevel: "default",
1213
- safetyCheck: true
1214
- };
1215
- }
1216
- function setupLogger(logLevel) {
1217
- if (logLevel) {
1218
- logger.setLevel(logLevel);
1219
- logger.debug(`Log level set to: ${logLevel}`);
1248
+ function updateLernaVersion({
1249
+ rootDir,
1250
+ versionMode,
1251
+ version,
1252
+ dryRun = false
1253
+ }) {
1254
+ const lernaJsonExists = hasLernaJson(rootDir);
1255
+ if (!lernaJsonExists) {
1256
+ return;
1220
1257
  }
1221
- }
1222
- async function resolveConfig(config, cwd) {
1223
- if (!config.repo) {
1224
- const resolvedRepoConfig = await resolveRepoConfig(cwd);
1225
- config.repo = {
1226
- ...resolvedRepoConfig,
1227
- provider: resolvedRepoConfig.provider
1228
- };
1258
+ const lernaJsonPath = join(rootDir, "lerna.json");
1259
+ if (!existsSync(lernaJsonPath)) {
1260
+ return;
1229
1261
  }
1230
- if (typeof config.repo === "string") {
1231
- const resolvedRepoConfig = getRepoConfig(config.repo);
1232
- config.repo = {
1233
- ...resolvedRepoConfig,
1234
- provider: resolvedRepoConfig.provider
1235
- };
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;
1270
+ }
1271
+ lernaJson.version = version;
1272
+ if (dryRun) {
1273
+ logger.info(`[dry-run] update lerna.json: ${oldVersion} \u2192 ${version}`);
1274
+ return;
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}`);
1236
1281
  }
1237
- return config;
1238
1282
  }
1239
- async function loadRelizyConfig(options) {
1240
- const cwd = options?.overrides?.cwd ?? process$1.cwd();
1241
- await setupDotenv({ cwd });
1242
- const configFile = options?.configFile ?? "relizy";
1243
- const defaultConfig = getDefaultConfig();
1244
- const overridesConfig = defu(options?.overrides, options?.baseConfig);
1245
- const results = await loadConfig({
1246
- dotenv: true,
1247
- cwd,
1248
- name: configFile,
1249
- packageJson: true,
1250
- defaults: defaultConfig,
1251
- overrides: overridesConfig
1252
- });
1253
- if (typeof results._configFile !== "string") {
1254
- logger.debug(`No config file found with name "${configFile}"`);
1255
- if (options?.configFile) {
1256
- logger.error(`No config file found with name "${configFile}"`);
1257
- process$1.exit(1);
1258
- }
1283
+ function extractVersionFromPackageTag(tag) {
1284
+ const atIndex = tag.lastIndexOf("@");
1285
+ if (atIndex === -1) {
1286
+ return null;
1259
1287
  }
1260
- setupLogger(options?.overrides?.logLevel || results.config.logLevel);
1261
- logger.verbose("User config:", formatJson(results.config.changelog));
1262
- const resolvedConfig = await resolveConfig(results.config, cwd);
1263
- logger.debug("Resolved config:", formatJson(resolvedConfig));
1264
- return resolvedConfig;
1288
+ return tag.slice(atIndex + 1);
1265
1289
  }
1266
- function defineConfig(config) {
1267
- return config;
1290
+ function isPrerelease(version) {
1291
+ if (!version)
1292
+ return false;
1293
+ const prerelease = semver.prerelease(version);
1294
+ return prerelease ? prerelease.length > 0 : false;
1268
1295
  }
1269
-
1270
- async function githubIndependentMode({
1271
- config,
1272
- dryRun,
1273
- bumpResult,
1274
- force,
1275
- suffix
1276
- }) {
1277
- const repoConfig = config.repo;
1278
- if (!repoConfig) {
1279
- throw new Error("No repository configuration found. Please check your changelog config.");
1280
- }
1281
- logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1282
- if (!config.tokens.github && !config.repo?.token) {
1283
- throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
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;
1284
1313
  }
1285
- const packages = await getPackagesOrBumpedPackages({
1286
- config,
1287
- bumpResult,
1288
- suffix,
1289
- force
1290
- });
1291
- logger.info(`Creating ${packages.length} GitHub release(s)`);
1292
- const postedReleases = [];
1293
- for (const pkg of packages) {
1294
- const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
1295
- const from = config.from || pkg.fromTag;
1296
- const to = config.to || getIndependentTag({ version: newVersion, name: pkg.name });
1297
- if (!from) {
1298
- logger.warn(`No from tag found for ${pkg.name}, skipping release`);
1299
- continue;
1300
- }
1301
- const toTag = dryRun ? "HEAD" : to;
1302
- logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${toTag}`);
1303
- const changelog = await generateChangelog({
1304
- pkg,
1305
- config,
1306
- dryRun,
1307
- newVersion
1308
- });
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(newVersion)
1315
- };
1316
- logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
1317
- if (dryRun) {
1318
- logger.info(`[dry-run] Publish GitHub release for ${to}`);
1319
- postedReleases.push({
1320
- name: pkg.name,
1321
- tag: release.tag_name,
1322
- version: newVersion,
1323
- prerelease: release.prerelease
1324
- });
1325
- } else {
1326
- logger.debug(`Publishing release ${to} to GitHub...`);
1327
- await createGithubRelease({
1328
- ...config,
1329
- from,
1330
- to,
1331
- repo: repoConfig
1332
- }, release);
1333
- postedReleases.push({
1334
- name: pkg.name,
1335
- tag: release.tag_name,
1336
- version: newVersion,
1337
- prerelease: release.prerelease
1338
- });
1339
- }
1314
+ return prerelease[0];
1315
+ }
1316
+ function isChangedPreid(currentVersion, targetPreid) {
1317
+ if (!targetPreid || !isPrerelease(currentVersion)) {
1318
+ return false;
1340
1319
  }
1341
- if (postedReleases.length === 0) {
1342
- logger.warn("No releases created");
1343
- } else {
1344
- logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitHub!`);
1320
+ const currentPreid = getPreid(currentVersion);
1321
+ if (!currentPreid) {
1322
+ return false;
1345
1323
  }
1346
- return postedReleases;
1324
+ return currentPreid !== targetPreid;
1347
1325
  }
1348
- async function githubUnified({
1349
- config,
1350
- dryRun,
1351
- rootPackage,
1352
- bumpResult
1326
+ function getBumpedPackageIndependently({
1327
+ pkg,
1328
+ dryRun
1353
1329
  }) {
1354
- const repoConfig = config.repo;
1355
- if (!repoConfig) {
1356
- throw new Error("No repository configuration found. Please check your changelog config.");
1357
- }
1358
- logger.debug(`GitHub token: ${config.tokens.github || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1359
- if (!config.tokens.github && !config.repo?.token) {
1360
- throw new Error("No GitHub token specified. Set GITHUB_TOKEN or GH_TOKEN environment variable.");
1361
- }
1362
- const newVersion = bumpResult?.newVersion || rootPackage.version;
1363
- const to = config.to || config.templates.tagBody.replace("{{newVersion}}", newVersion);
1364
- const changelog = await generateChangelog({
1365
- pkg: rootPackage,
1366
- config,
1367
- dryRun,
1368
- newVersion
1369
- });
1370
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1371
- const release = {
1372
- tag_name: to,
1373
- name: to,
1374
- body: releaseBody,
1375
- prerelease: isPrerelease(to)
1376
- };
1377
- logger.debug(`Creating release for ${to}${release.prerelease ? " (prerelease)" : ""}`);
1378
- logger.debug("Release details:", formatJson({
1379
- tag_name: release.tag_name,
1380
- name: release.name,
1381
- prerelease: release.prerelease
1382
- }));
1383
- if (dryRun) {
1384
- logger.info("[dry-run] Publish GitHub release for", release.tag_name);
1385
- } else {
1386
- logger.debug("Publishing release to GitHub...");
1387
- await createGithubRelease({
1388
- ...config,
1389
- from: bumpResult?.bumped && bumpResult.fromTag || "v0.0.0",
1390
- to,
1391
- repo: repoConfig
1392
- }, release);
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 };
1393
1335
  }
1394
- logger.success(`Release ${to} published to GitHub!`);
1395
- return [{
1396
- name: to,
1397
- tag: to,
1398
- version: to,
1399
- prerelease: release.prerelease
1400
- }];
1336
+ logger.debug(`Bumping ${pkg.name} from ${currentVersion} to ${newVersion}`);
1337
+ writeVersion(pkg.path, newVersion, dryRun);
1338
+ return { bumped: true, newVersion, oldVersion: currentVersion };
1401
1339
  }
1402
- async function github(options) {
1403
- try {
1404
- const dryRun = options.dryRun ?? false;
1405
- logger.debug(`Dry run: ${dryRun}`);
1406
- const config = await loadRelizyConfig({
1407
- configFile: options.configName,
1408
- baseConfig: options.config,
1409
- overrides: {
1410
- from: options.from,
1411
- to: options.to,
1412
- logLevel: options.logLevel,
1413
- tokens: {
1414
- github: options.token
1415
- }
1416
- }
1417
- });
1418
- if (config.monorepo?.versionMode === "independent") {
1419
- return await githubIndependentMode({
1420
- config,
1421
- dryRun,
1422
- bumpResult: options.bumpResult,
1423
- force: options.force ?? false,
1424
- suffix: options.suffix
1425
- });
1426
- }
1427
- const rootPackageBase = readPackageJson(config.cwd);
1428
- if (!rootPackageBase) {
1429
- throw new Error("Failed to read root package.json");
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("");
1430
1353
  }
1431
- const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
1432
- const { from, to } = await resolveTags({
1433
- config,
1434
- step: "provider-release",
1435
- newVersion,
1436
- pkg: rootPackageBase
1437
- });
1438
- const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
1439
- config,
1440
- force: options.force ?? false,
1441
- suffix: options.suffix,
1442
- changelog: true,
1443
- from,
1444
- to
1445
- });
1446
- return await githubUnified({
1447
- config,
1448
- dryRun,
1449
- rootPackage,
1450
- bumpResult: options.bumpResult
1451
- });
1452
- } catch (error) {
1453
- logger.error("Error publishing GitHub release:", error);
1454
- throw error;
1455
1354
  }
1456
1355
  }
1457
-
1458
- async function createGitlabRelease({
1459
- config,
1460
- release,
1461
- dryRun
1356
+ function displayUnifiedModePackages({
1357
+ packages,
1358
+ newVersion,
1359
+ force
1462
1360
  }) {
1463
- const token = config.tokens.gitlab || config.repo?.token;
1464
- if (!token && !dryRun) {
1465
- throw new Error(
1466
- "No GitLab token found. Set GITLAB_TOKEN or CI_JOB_TOKEN environment variable or configure tokens.gitlab"
1467
- );
1468
- }
1469
- const repoConfig = config.repo?.repo;
1470
- if (!repoConfig) {
1471
- throw new Error("No repository URL found in config");
1472
- }
1473
- logger.debug(`Parsed repository URL: ${repoConfig}`);
1474
- const projectPath = encodeURIComponent(repoConfig);
1475
- const gitlabDomain = config.repo?.domain || "gitlab.com";
1476
- const apiUrl = `https://${gitlabDomain}/api/v4/projects/${projectPath}/releases`;
1477
- logger.info(`Creating GitLab release at: ${apiUrl}`);
1478
- const payload = {
1479
- tag_name: release.tag_name,
1480
- name: release.name || release.tag_name,
1481
- description: release.description || "",
1482
- ref: release.ref || "main"
1483
- };
1484
- try {
1485
- if (dryRun) {
1486
- logger.info("[dry-run] GitLab release:", formatJson(payload));
1487
- return {
1488
- tag_name: release.tag_name,
1489
- name: release.name || release.tag_name,
1490
- description: release.description || "",
1491
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1492
- released_at: (/* @__PURE__ */ new Date()).toISOString(),
1493
- _links: {
1494
- self: `${apiUrl}/${encodeURIComponent(release.tag_name)}`
1495
- }
1496
- };
1497
- }
1498
- logger.debug(`POST GitLab release to ${apiUrl} with payload: ${formatJson(payload)}`);
1499
- const response = await fetch(apiUrl, {
1500
- method: "POST",
1501
- headers: {
1502
- "Content-Type": "application/json",
1503
- "PRIVATE-TOKEN": token || ""
1504
- },
1505
- body: JSON.stringify(payload)
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)" : ""}`);
1364
+ });
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)`);
1506
1376
  });
1507
- if (!response.ok) {
1508
- const errorText = await response.text();
1509
- throw new Error(`GitLab API error (${response.status}): ${errorText}`);
1377
+ logger.log("");
1378
+ } else {
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("");
1510
1402
  }
1511
- const result = await response.json();
1512
- logger.debug(`Created GitLab release: ${result._links.self}`);
1513
- return result;
1514
- } catch (error) {
1515
- logger.error("Failed to create GitLab release:", error);
1516
- throw error;
1517
1403
  }
1518
1404
  }
1519
- async function gitlabIndependentMode({
1520
- config,
1521
- dryRun,
1522
- bumpResult,
1523
- suffix,
1405
+ function displayIndependentModePackages({
1406
+ packages,
1524
1407
  force
1525
1408
  }) {
1526
- logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1527
- const packages = await getPackagesOrBumpedPackages({
1528
- config,
1529
- bumpResult,
1530
- suffix,
1531
- force
1532
- });
1533
- logger.info(`Creating ${packages.length} GitLab release(s) for independent packages`);
1534
- logger.debug("Getting current branch...");
1535
- const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
1536
- noSuccess: true,
1537
- noStdout: true,
1538
- logLevel: config.logLevel,
1539
- cwd: config.cwd
1540
- });
1541
- const postedReleases = [];
1542
- for (const pkg of packages) {
1543
- const newVersion = isBumpedPackage(pkg) && pkg.newVersion || pkg.version;
1544
- const from = config.from || pkg.fromTag;
1545
- const to = getIndependentTag({ version: newVersion, name: pkg.name });
1546
- if (!from) {
1547
- logger.warn(`No from tag found for ${pkg.name}, skipping release`);
1548
- continue;
1549
- }
1550
- logger.debug(`Processing ${pkg.name}: ${from} \u2192 ${to}`);
1551
- const changelog = await generateChangelog({
1552
- pkg,
1553
- config,
1554
- dryRun,
1555
- newVersion
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)`);
1556
1413
  });
1557
- if (!changelog) {
1558
- logger.warn(`No changelog found for ${pkg.name}`);
1559
- continue;
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)" : ""}`);
1423
+ });
1424
+ logger.log("");
1560
1425
  }
1561
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1562
- const release = {
1563
- tag_name: to,
1564
- name: to,
1565
- description: releaseBody,
1566
- ref: currentBranch.trim()
1567
- };
1568
- logger.debug(`Creating release for ${to} (ref: ${release.ref})`);
1569
- if (dryRun) {
1570
- logger.info(`[dry-run] Publish GitLab release for ${to}`);
1571
- } else {
1572
- logger.debug(`Publishing release ${to} to GitLab...`);
1573
- await createGitlabRelease({
1574
- config,
1575
- release,
1576
- dryRun
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)" : ""}`);
1577
1430
  });
1578
- postedReleases.push({
1579
- name: pkg.name,
1580
- tag: release.tag_name,
1581
- version: newVersion,
1582
- prerelease: isPrerelease(newVersion)
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)" : ""}`);
1583
1437
  });
1438
+ logger.log("");
1584
1439
  }
1585
1440
  }
1586
- if (postedReleases.length === 0) {
1587
- logger.warn("No releases created");
1588
- } else {
1589
- logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitLab!`);
1590
- }
1591
- return postedReleases;
1592
1441
  }
1593
- async function gitlabUnified({
1442
+ async function confirmBump({
1443
+ versionMode,
1594
1444
  config,
1595
- dryRun,
1596
- rootPackage,
1597
- bumpResult
1445
+ packages,
1446
+ force,
1447
+ currentVersion,
1448
+ newVersion,
1449
+ dryRun
1598
1450
  }) {
1599
- logger.debug(`GitLab token: ${config.tokens.gitlab || config.repo?.token ? "\u2713 provided" : "\u2717 missing"}`);
1600
- const newVersion = bumpResult?.newVersion || rootPackage.newVersion || rootPackage.version;
1601
- const to = config.templates.tagBody.replace("{{newVersion}}", newVersion);
1602
- const changelog = await generateChangelog({
1603
- pkg: rootPackage,
1604
- config,
1605
- dryRun,
1606
- newVersion
1607
- });
1608
- const releaseBody = changelog.split("\n").slice(2).join("\n");
1609
- logger.debug("Getting current branch...");
1610
- const { stdout: currentBranch } = await execPromise("git rev-parse --abbrev-ref HEAD", {
1611
- noSuccess: true,
1612
- noStdout: true,
1613
- logLevel: config.logLevel,
1614
- cwd: config.cwd
1615
- });
1616
- const release = {
1617
- tag_name: to,
1618
- name: to,
1619
- description: releaseBody,
1620
- ref: currentBranch.trim()
1621
- };
1622
- logger.info(`Creating release for ${to} (ref: ${release.ref})`);
1623
- logger.debug("Release details:", formatJson({
1624
- tag_name: release.tag_name,
1625
- name: release.name,
1626
- ref: release.ref
1627
- }));
1628
- if (dryRun) {
1629
- logger.info("[dry-run] Publish GitLab release for", release.tag_name);
1630
- } else {
1631
- logger.debug("Publishing release to GitLab...");
1632
- await createGitlabRelease({
1633
- config,
1634
- release,
1635
- dryRun
1636
- });
1451
+ if (packages.length === 0) {
1452
+ logger.debug("No packages to bump");
1453
+ return;
1637
1454
  }
1638
- logger.success(`Release ${to} published to GitLab!`);
1639
- return [{
1640
- name: to,
1641
- tag: to,
1642
- version: to,
1643
- prerelease: isPrerelease(newVersion)
1644
- }];
1645
- }
1646
- async function gitlab(options = {}) {
1647
- try {
1648
- const dryRun = options.dryRun ?? false;
1649
- logger.debug(`Dry run: ${dryRun}`);
1650
- const config = await loadRelizyConfig({
1651
- configFile: options.configName,
1652
- baseConfig: options.config,
1653
- overrides: {
1654
- from: options.from,
1655
- to: options.to,
1656
- logLevel: options.logLevel,
1657
- tokens: {
1658
- gitlab: options.token
1659
- }
1660
- }
1661
- });
1662
- if (config.monorepo?.versionMode === "independent") {
1663
- return await gitlabIndependentMode({
1664
- config,
1665
- dryRun,
1666
- bumpResult: options.bumpResult,
1667
- suffix: options.suffix,
1668
- force: options.force ?? false
1669
- });
1670
- }
1671
- const rootPackageBase = readPackageJson(config.cwd);
1672
- if (!rootPackageBase) {
1673
- throw new Error("Failed to read root package.json");
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");
1674
1469
  }
1675
- const newVersion = options.bumpResult?.newVersion || rootPackageBase.version;
1676
- const { from, to } = await resolveTags({
1677
- config,
1678
- step: "provider-release",
1679
- newVersion,
1680
- pkg: rootPackageBase
1681
- });
1682
- const rootPackage = options.bumpResult?.rootPackage || await getRootPackage({
1683
- config,
1684
- force: options.force ?? false,
1685
- suffix: options.suffix,
1686
- changelog: true,
1687
- from,
1688
- to
1689
- });
1690
- logger.debug(`Root package: ${getIndependentTag({ name: rootPackage.name, version: newVersion })}`);
1691
- return await gitlabUnified({
1692
- config,
1693
- dryRun,
1694
- rootPackage,
1695
- bumpResult: options.bumpResult
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 });
1478
+ }
1479
+ try {
1480
+ const confirmed = await confirm({
1481
+ message: `${dryRun ? "[dry-run] " : ""}Do you want to proceed with these version updates?`,
1482
+ default: true
1696
1483
  });
1484
+ if (!confirmed) {
1485
+ logger.log("");
1486
+ logger.fail("Bump refused");
1487
+ process.exit(0);
1488
+ }
1697
1489
  } catch (error) {
1698
- logger.error("Error publishing GitLab release:", error);
1699
- 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);
1700
1498
  }
1499
+ logger.log("");
1701
1500
  }
1702
-
1703
- function isGraduatingToStableBetweenVersion(version, newVersion) {
1704
- const isSameBase = semver.major(version) === semver.major(newVersion) && semver.minor(version) === semver.minor(newVersion) && semver.patch(version) === semver.patch(newVersion);
1705
- const fromPrerelease = semver.prerelease(version) !== null;
1706
- const toStable = semver.prerelease(newVersion) === null;
1707
- return isSameBase && fromPrerelease && toStable;
1708
- }
1709
- function determineSemverChange(commits, types) {
1710
- let [hasMajor, hasMinor, hasPatch] = [false, false, false];
1711
- for (const commit of commits) {
1712
- const commitType = types[commit.type];
1713
- if (!commitType) {
1714
- continue;
1715
- }
1716
- const semverType = commitType.semver;
1717
- if (semverType === "major" || commit.isBreaking) {
1718
- hasMajor = true;
1719
- } else if (semverType === "minor") {
1720
- hasMinor = true;
1721
- } else if (semverType === "patch") {
1722
- hasPatch = true;
1501
+ function getBumpedIndependentPackages({
1502
+ packages,
1503
+ dryRun
1504
+ }) {
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
1511
+ });
1512
+ if (result.bumped) {
1513
+ bumpedPackages.push({
1514
+ ...pkgToBump,
1515
+ version: result.oldVersion
1516
+ });
1723
1517
  }
1724
1518
  }
1725
- return hasMajor ? "major" : hasMinor ? "minor" : hasPatch ? "patch" : void 0;
1519
+ return bumpedPackages;
1726
1520
  }
1727
- function detectReleaseTypeFromCommits(commits, types) {
1728
- return determineSemverChange(commits, types);
1521
+ function shouldFilterPrereleaseTags(currentVersion, graduating) {
1522
+ return !isPrerelease(currentVersion) && !graduating;
1729
1523
  }
1730
- function validatePrereleaseDowngrade(currentVersion, targetPreid, configuredType) {
1731
- if (configuredType !== "prerelease" || !targetPreid || !isPrerelease(currentVersion)) {
1732
- return;
1524
+ function extractVersionFromTag(tag, packageName) {
1525
+ if (!tag) {
1526
+ return null;
1733
1527
  }
1734
- const testVersion = semver.inc(currentVersion, "prerelease", targetPreid);
1735
- const isNotUpgrade = testVersion && !semver.gt(testVersion, currentVersion);
1736
- if (isNotUpgrade) {
1737
- throw new Error(`Unable to graduate from ${currentVersion} to ${testVersion}, it's not a valid prerelease`);
1528
+ if (packageName) {
1529
+ const prefix = `${packageName}@`;
1530
+ if (tag.startsWith(prefix)) {
1531
+ return tag.slice(prefix.length);
1532
+ }
1738
1533
  }
1739
- }
1740
- function handleStableVersionWithReleaseType(commits, types, force) {
1741
- if (!commits?.length && !force) {
1742
- logger.debug('No commits found for stable version with "release" type, skipping bump');
1743
- return void 0;
1534
+ const atIndex = tag.lastIndexOf("@");
1535
+ if (atIndex !== -1) {
1536
+ return tag.slice(atIndex + 1);
1744
1537
  }
1745
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1746
- if (!detectedType && !force) {
1747
- logger.debug("No significant commits found, skipping bump");
1748
- return void 0;
1538
+ if (tag.startsWith("v") && /^v\d/.test(tag)) {
1539
+ return tag.slice(1);
1749
1540
  }
1750
- logger.debug(`Auto-detected release type from commits: ${detectedType}`);
1751
- return detectedType;
1752
- }
1753
- function handleStableVersionWithPrereleaseType(commits, types, force) {
1754
- if (!commits?.length && !force) {
1755
- logger.debug('No commits found for stable version with "prerelease" type, skipping bump');
1756
- return void 0;
1541
+ if (/^\d+\.\d+\.\d+/.test(tag)) {
1542
+ return tag;
1757
1543
  }
1758
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types) : void 0;
1759
- if (!detectedType) {
1760
- logger.debug("No significant commits found, using prepatch as default");
1761
- return "prepatch";
1544
+ return null;
1545
+ }
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;
1762
1553
  }
1763
- const prereleaseType = `pre${detectedType}`;
1764
- logger.debug(`Auto-detected prerelease type from commits: ${prereleaseType}`);
1765
- return prereleaseType;
1766
1554
  }
1767
- function handlePrereleaseVersionToStable(currentVersion) {
1768
- logger.debug(`Graduating from prerelease ${currentVersion} to stable release`);
1769
- return "release";
1555
+
1556
+ function getIndependentTag({ version, name }) {
1557
+ return `${name}@${version}`;
1770
1558
  }
1771
- function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force }) {
1772
- const currentPreid = getPreid(currentVersion);
1773
- const hasChangedPreid = preid && currentPreid && currentPreid !== preid;
1774
- if (hasChangedPreid) {
1775
- const testVersion = semver.inc(currentVersion, "prerelease", preid);
1776
- if (!testVersion) {
1777
- throw new Error(`Unable to change preid from ${currentPreid} to ${preid} for version ${currentVersion}`);
1778
- }
1779
- const isUpgrade = semver.gt(testVersion, currentVersion);
1780
- if (!isUpgrade) {
1781
- throw new Error(`Unable to change preid from ${currentVersion} to ${testVersion}, it's not a valid upgrade (cannot downgrade from ${currentPreid} to ${preid})`);
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
1782
1568
  }
1783
- return "prerelease";
1784
- }
1785
- if (!commits?.length && !force) {
1786
- logger.debug("No commits found for prerelease version, skipping bump");
1787
- return void 0;
1788
- }
1789
- logger.debug(`Incrementing prerelease version: ${currentVersion}`);
1790
- return "prerelease";
1569
+ );
1570
+ const lastTag = stdout.trim();
1571
+ logger.debug("Last stable tag:", lastTag || "No stable tags found");
1572
+ return lastTag;
1791
1573
  }
1792
- function handleExplicitReleaseType({
1793
- releaseType,
1794
- currentVersion
1795
- }) {
1796
- const isCurrentPrerelease = isPrerelease(currentVersion);
1797
- const isGraduatingToStable = isCurrentPrerelease && isStableReleaseType(releaseType);
1798
- if (isGraduatingToStable) {
1799
- logger.debug(`Graduating from prerelease ${currentVersion} to stable with type: ${releaseType}`);
1800
- } else {
1801
- logger.debug(`Using explicit release type: ${releaseType}`);
1574
+ async function getLastTag({ logLevel, cwd }) {
1575
+ const { stdout } = await execPromise(`git tag --sort=-creatordate | head -n 1`, {
1576
+ logLevel,
1577
+ noStderr: true,
1578
+ noStdout: true,
1579
+ noSuccess: true,
1580
+ cwd
1581
+ });
1582
+ const lastTag = stdout.trim();
1583
+ logger.debug("Last tag:", lastTag || "No tags found");
1584
+ return lastTag;
1585
+ }
1586
+ async function getAllRecentRepoTags(options) {
1587
+ const limit = options?.limit;
1588
+ try {
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 [];
1802
1604
  }
1803
- return releaseType;
1804
1605
  }
1805
- function determineReleaseType({
1806
- currentVersion,
1807
- commits,
1808
- releaseType,
1809
- preid,
1810
- types,
1811
- force
1606
+ async function getAllRecentPackageTags({
1607
+ packageName,
1608
+ limit = 50,
1609
+ logLevel,
1610
+ cwd
1812
1611
  }) {
1813
- if (releaseType === "release" && preid) {
1814
- throw new Error('You cannot use a "release" type with a "preid", to use a preid you must use a "prerelease" type');
1815
- }
1816
- validatePrereleaseDowngrade(currentVersion, preid, releaseType);
1817
- if (force) {
1818
- logger.debug(`Force flag enabled, using configured type: ${releaseType}`);
1819
- return releaseType;
1820
- }
1821
- const isCurrentPrerelease = isPrerelease(currentVersion);
1822
- if (!isCurrentPrerelease) {
1823
- if (releaseType === "release") {
1824
- return handleStableVersionWithReleaseType(commits, types, force);
1825
- }
1826
- if (releaseType === "prerelease") {
1827
- return handleStableVersionWithPrereleaseType(commits, types, force);
1828
- }
1829
- return handleExplicitReleaseType({ releaseType, currentVersion });
1830
- }
1831
- if (releaseType === "release") {
1832
- return handlePrereleaseVersionToStable(currentVersion);
1833
- }
1834
- if (releaseType === "prerelease") {
1835
- return handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force });
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 [];
1836
1629
  }
1837
- return handleExplicitReleaseType({ releaseType, currentVersion });
1838
1630
  }
1839
- function writeVersion(pkgPath, newVersion, dryRun = false) {
1840
- const packageJsonPath = join(pkgPath, "package.json");
1841
- try {
1842
- logger.debug(`Writing ${newVersion} to ${pkgPath}`);
1843
- const content = readFileSync(packageJsonPath, "utf8");
1844
- const packageJson = JSON.parse(content);
1845
- const oldVersion = packageJson.version;
1846
- packageJson.version = newVersion;
1847
- if (dryRun) {
1848
- logger.info(`[dry-run] Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1849
- return;
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;
1850
1642
  }
1851
- writeFileSync(packageJsonPath, `${formatJson(packageJson)}
1852
- `, "utf8");
1853
- logger.info(`Updated ${packageJson.name}: ${oldVersion} \u2192 ${newVersion}`);
1854
- } catch (error) {
1855
- throw new Error(`Unable to write version to ${packageJsonPath}: ${error}`);
1643
+ if (onlyStable && isPrerelease(tagVersion)) {
1644
+ logger.debug(`Skipping tag ${tag}: prerelease version ${tagVersion} (onlyStable=${onlyStable})`);
1645
+ return false;
1646
+ }
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;
1656
+ }
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
+ });
1856
1665
  }
1666
+ if (options?.onlyStable) {
1667
+ return getLastStableTag({ logLevel: options?.logLevel, cwd: options?.cwd });
1668
+ }
1669
+ return getLastTag({ logLevel: options?.logLevel, cwd: options?.cwd });
1857
1670
  }
1858
- function getPackageNewVersion({
1859
- name,
1671
+ async function getLastRepoTagWithFiltering({
1860
1672
  currentVersion,
1861
- releaseType,
1862
- preid,
1863
- suffix
1673
+ onlyStable,
1674
+ logLevel,
1675
+ cwd
1864
1676
  }) {
1865
- let newVersion = semver.inc(currentVersion, releaseType, preid);
1866
- if (!newVersion) {
1867
- throw new Error(`Unable to bump "${name}" version "${currentVersion}" with release type "${releaseType}"
1868
-
1869
- You should use an explicit release type (use flag: --major, --minor, --patch, --premajor, --preminor, --prepatch, --prerelease)`);
1870
- }
1871
- if (isPrereleaseReleaseType(releaseType) && suffix) {
1872
- newVersion = newVersion.replace(/\.(\d+)$/, `.${suffix}`);
1873
- }
1874
- const isValidVersion = semver.gt(newVersion, currentVersion);
1875
- if (!isValidVersion) {
1876
- throw new Error(`Unable to bump "${name}" version "${currentVersion}" to "${newVersion}", new version is not greater than current version`);
1877
- }
1878
- if (isGraduating(currentVersion, releaseType)) {
1879
- logger.info(`Graduating "${name}" from prerelease ${currentVersion} to stable ${newVersion}`);
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;
1880
1681
  }
1881
- if (isChangedPreid(currentVersion, preid)) {
1882
- logger.debug(`Graduating "${name}" from ${getPreid(currentVersion)} to ${preid}`);
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;
1883
1690
  }
1884
- return newVersion;
1691
+ const lastTag = compatibleTags[0];
1692
+ logger.debug(`Last compatible repo tag: ${lastTag}`);
1693
+ return lastTag;
1885
1694
  }
1886
- function updateLernaVersion({
1887
- rootDir,
1888
- versionMode,
1889
- version,
1890
- dryRun = false
1695
+ async function getLastPackageTag({
1696
+ packageName,
1697
+ onlyStable,
1698
+ currentVersion,
1699
+ logLevel,
1700
+ cwd
1891
1701
  }) {
1892
- const lernaJsonExists = hasLernaJson(rootDir);
1893
- if (!lernaJsonExists) {
1894
- return;
1895
- }
1896
- const lernaJsonPath = join(rootDir, "lerna.json");
1897
- if (!existsSync(lernaJsonPath)) {
1898
- return;
1702
+ if (currentVersion) {
1703
+ return getLastPackageTagWithFiltering({
1704
+ packageName,
1705
+ currentVersion,
1706
+ onlyStable: onlyStable ?? false,
1707
+ logLevel,
1708
+ cwd
1709
+ });
1899
1710
  }
1900
1711
  try {
1901
- logger.debug("Updating lerna.json version");
1902
- const content = readFileSync(lernaJsonPath, "utf8");
1903
- const lernaJson = JSON.parse(content);
1904
- const oldVersion = lernaJson.version;
1905
- if (lernaJson.version === "independent" || versionMode === "independent") {
1906
- logger.debug("Lerna version is independent or version mode is independent, skipping update");
1907
- return;
1908
- }
1909
- lernaJson.version = version;
1910
- if (dryRun) {
1911
- logger.info(`[dry-run] update lerna.json: ${oldVersion} \u2192 ${version}`);
1912
- return;
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}@`;
1913
1718
  }
1914
- writeFileSync(lernaJsonPath, `${formatJson(lernaJson)}
1915
- `, "utf8");
1916
- logger.success(`Updated lerna.json: ${oldVersion} \u2192 ${version}`);
1917
- } catch (error) {
1918
- logger.fail(`Unable to update lerna.json: ${error}`);
1919
- }
1920
- }
1921
- function extractVersionFromPackageTag(tag) {
1922
- const atIndex = tag.lastIndexOf("@");
1923
- if (atIndex === -1) {
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 {
1924
1732
  return null;
1925
1733
  }
1926
- return tag.slice(atIndex + 1);
1927
- }
1928
- function isPrerelease(version) {
1929
- if (!version)
1930
- return false;
1931
- const prerelease = semver.prerelease(version);
1932
- return prerelease ? prerelease.length > 0 : false;
1933
- }
1934
- function isStableReleaseType(releaseType) {
1935
- const stableTypes = ["release", "major", "minor", "patch"];
1936
- return stableTypes.includes(releaseType);
1937
- }
1938
- function isPrereleaseReleaseType(releaseType) {
1939
- const prereleaseTypes = ["prerelease", "premajor", "preminor", "prepatch"];
1940
- return prereleaseTypes.includes(releaseType);
1941
1734
  }
1942
- function isGraduating(currentVersion, releaseType) {
1943
- return isPrerelease(currentVersion) && isStableReleaseType(releaseType);
1944
- }
1945
- function getPreid(version) {
1946
- if (!version)
1947
- return null;
1948
- const prerelease = semver.prerelease(version);
1949
- if (!prerelease || prerelease.length === 0) {
1735
+ async function getLastPackageTagWithFiltering({
1736
+ packageName,
1737
+ currentVersion,
1738
+ onlyStable,
1739
+ logLevel,
1740
+ cwd
1741
+ }) {
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}`);
1950
1750
  return null;
1951
1751
  }
1952
- return prerelease[0];
1953
- }
1954
- function isChangedPreid(currentVersion, targetPreid) {
1955
- if (!targetPreid || !isPrerelease(currentVersion)) {
1956
- return false;
1957
- }
1958
- const currentPreid = getPreid(currentVersion);
1959
- if (!currentPreid) {
1960
- return false;
1961
- }
1962
- return currentPreid !== targetPreid;
1963
- }
1964
- function getBumpedPackageIndependently({
1965
- pkg,
1966
- dryRun
1967
- }) {
1968
- logger.debug(`Analyzing ${pkg.name}`);
1969
- const currentVersion = pkg.version || "0.0.0";
1970
- const newVersion = pkg.newVersion;
1971
- if (!newVersion) {
1972
- return { bumped: false };
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;
1973
1761
  }
1974
- logger.debug(`Bumping ${pkg.name} from ${currentVersion} to ${newVersion}`);
1975
- writeVersion(pkg.path, newVersion, dryRun);
1976
- return { bumped: true, newVersion, oldVersion: currentVersion };
1762
+ const lastTag = compatibleTags[0];
1763
+ logger.debug(`Last compatible package tag for ${packageName}: ${lastTag}`);
1764
+ return lastTag;
1977
1765
  }
1978
- function displayRootAndLernaUpdates({
1979
- versionMode,
1766
+ async function resolveFromTagIndependent({
1767
+ cwd,
1768
+ packageName,
1980
1769
  currentVersion,
1981
- newVersion,
1982
- dryRun,
1983
- lernaJsonExists
1770
+ graduating,
1771
+ logLevel
1984
1772
  }) {
1985
- if (versionMode !== "independent" && currentVersion && newVersion) {
1986
- logger.log(`${dryRun ? "[dry-run] " : ""}Root package.json: ${currentVersion} \u2192 ${newVersion}`);
1987
- logger.log("");
1988
- if (lernaJsonExists) {
1989
- logger.log(`${dryRun ? "[dry-run] " : ""}lerna.json: ${currentVersion} \u2192 ${newVersion}`);
1990
- logger.log("");
1991
- }
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);
1992
1784
  }
1785
+ return lastPackageTag;
1993
1786
  }
1994
- function displayUnifiedModePackages({
1995
- packages,
1996
- newVersion,
1997
- force
1787
+ async function resolveFromTagUnified({
1788
+ config,
1789
+ currentVersion,
1790
+ graduating,
1791
+ logLevel
1998
1792
  }) {
1999
- logger.log(`${packages.length} package(s):`);
2000
- packages.forEach((pkg) => {
2001
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
2002
- });
2003
- logger.log("");
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;
2004
1802
  }
2005
- function displaySelectiveModePackages({
2006
- packages,
2007
- newVersion,
2008
- force
1803
+ async function resolveFromTag({
1804
+ config,
1805
+ versionMode,
1806
+ step,
1807
+ packageName,
1808
+ currentVersion,
1809
+ graduating,
1810
+ logLevel
2009
1811
  }) {
2010
- if (force) {
2011
- logger.log(`${packages.length} package(s):`);
2012
- packages.forEach((pkg) => {
2013
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (force)`);
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
2014
1823
  });
2015
- logger.log("");
2016
1824
  } else {
2017
- const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
2018
- const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
2019
- const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
2020
- if (packagesWithCommits.length > 0) {
2021
- logger.log(`${packagesWithCommits.length} package(s) with commits:`);
2022
- packagesWithCommits.forEach((pkg) => {
2023
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
2024
- });
2025
- logger.log("");
2026
- }
2027
- if (packagesAsDependents.length > 0) {
2028
- logger.log(`${packagesAsDependents.length} dependent package(s):`);
2029
- packagesAsDependents.forEach((pkg) => {
2030
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
2031
- });
2032
- logger.log("");
2033
- }
2034
- if (packagesAsGraduation.length > 0) {
2035
- logger.log(`${packagesAsGraduation.length} graduation package(s):`);
2036
- packagesAsGraduation.forEach((pkg) => {
2037
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${newVersion} ${force ? "(force)" : ""}`);
2038
- });
2039
- logger.log("");
2040
- }
1825
+ from = await resolveFromTagUnified({
1826
+ config,
1827
+ currentVersion,
1828
+ graduating,
1829
+ logLevel
1830
+ });
2041
1831
  }
1832
+ logger.debug(`[${versionMode}](${step}) Using from tag: ${from}`);
1833
+ return config.from || from;
2042
1834
  }
2043
- function displayIndependentModePackages({
2044
- packages,
2045
- force
1835
+ function resolveToTag({
1836
+ config,
1837
+ versionMode,
1838
+ newVersion,
1839
+ step,
1840
+ packageName
2046
1841
  }) {
2047
- if (force) {
2048
- logger.log(`${packages.length} package(s):`);
2049
- packages.forEach((pkg) => {
2050
- logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (force)`);
2051
- });
2052
- logger.log("");
2053
- } else {
2054
- const packagesWithCommits = packages.filter((p) => "reason" in p && p.reason === "commits");
2055
- const packagesAsDependents = packages.filter((p) => "reason" in p && p.reason === "dependency");
2056
- const packagesAsGraduation = packages.filter((p) => "reason" in p && p.reason === "graduation");
2057
- if (packagesWithCommits.length > 0) {
2058
- logger.log(`${packagesWithCommits.length} package(s) with commits:`);
2059
- packagesWithCommits.forEach((pkg) => {
2060
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} (${pkg.commits.length} commits) ${force ? "(force)" : ""}`);
2061
- });
2062
- logger.log("");
2063
- }
2064
- if (packagesAsDependents.length > 0) {
2065
- logger.log(`${packagesAsDependents.length} dependent package(s):`);
2066
- packagesAsDependents.forEach((pkg) => {
2067
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
2068
- });
2069
- logger.log("");
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");
2070
1849
  }
2071
- if (packagesAsGraduation.length > 0) {
2072
- logger.log(`${packagesAsGraduation.length} graduation package(s):`);
2073
- packagesAsGraduation.forEach((pkg) => {
2074
- pkg.newVersion && logger.log(` \u2022 ${pkg.name}: ${pkg.version} \u2192 ${pkg.newVersion} ${force ? "(force)" : ""}`);
2075
- });
2076
- logger.log("");
1850
+ if (!newVersion) {
1851
+ throw new Error("New version is required for independent version mode");
2077
1852
  }
1853
+ to = getIndependentTag({ version: newVersion, name: packageName });
1854
+ } else {
1855
+ to = newVersion ? config.templates.tagBody.replace("{{newVersion}}", newVersion) : getCurrentGitRef(config.cwd);
2078
1856
  }
1857
+ logger.debug(`[${versionMode}](${step}) Using to tag: ${to}`);
1858
+ return config.to || to;
2079
1859
  }
2080
- async function confirmBump({
2081
- versionMode,
1860
+ async function resolveTags({
2082
1861
  config,
2083
- packages,
2084
- force,
2085
- currentVersion,
2086
- newVersion,
2087
- dryRun
1862
+ step,
1863
+ pkg,
1864
+ newVersion
2088
1865
  }) {
2089
- if (packages.length === 0) {
2090
- logger.debug("No packages to bump");
2091
- return;
2092
- }
2093
- const lernaJsonExists = hasLernaJson(config.cwd);
2094
- logger.log("");
2095
- logger.info(`${dryRun ? "[dry-run] " : ""}The following packages will be updated:
2096
- `);
2097
- displayRootAndLernaUpdates({
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
1879
+ });
1880
+ const to = resolveToTag({
1881
+ config,
2098
1882
  versionMode,
2099
- currentVersion,
2100
1883
  newVersion,
2101
- lernaJsonExists,
2102
- dryRun
1884
+ step,
1885
+ packageName: pkg.name
2103
1886
  });
2104
- if (versionMode === "unified") {
2105
- if (!newVersion) {
2106
- throw new Error("Cannot confirm bump in unified mode without a new version");
2107
- }
2108
- displayUnifiedModePackages({ packages, newVersion, force });
2109
- } else if (versionMode === "selective") {
2110
- if (!newVersion) {
2111
- throw new Error("Cannot confirm bump in selective mode without a new version");
2112
- }
2113
- displaySelectiveModePackages({ packages, newVersion, force });
2114
- } else if (versionMode === "independent") {
2115
- displayIndependentModePackages({ packages, force });
1887
+ logger.debug(`[${versionMode}](${step}) Using tags: ${from} \u2192 ${to}`);
1888
+ return { from, to };
1889
+ }
1890
+
1891
+ function fromTagIsFirstCommit(fromTag, cwd) {
1892
+ return fromTag === getFirstCommit(cwd);
1893
+ }
1894
+ async function generateChangelog({
1895
+ pkg,
1896
+ config,
1897
+ dryRun,
1898
+ newVersion,
1899
+ minify
1900
+ }) {
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");
1905
+ }
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);
2116
1909
  }
1910
+ if (!toTag) {
1911
+ throw new Error(`No tag found for ${pkg.name}`);
1912
+ }
1913
+ logger.debug(`Generating changelog for ${pkg.name} - from ${fromTag} to ${toTag}`);
2117
1914
  try {
2118
- const confirmed = await confirm({
2119
- message: `${dryRun ? "[dry-run] " : ""}Do you want to proceed with these version updates?`,
2120
- 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
2121
1927
  });
2122
- if (!confirmed) {
2123
- logger.log("");
2124
- logger.fail("Bump refused");
2125
- process.exit(0);
1928
+ let changelog = generatedChangelog;
1929
+ if (pkg.commits.length === 0) {
1930
+ changelog = `${changelog}
1931
+
1932
+ ${config.templates.emptyChangelogContent}`;
1933
+ }
1934
+ const changelogResult = await executeHook("generate:changelog", config, dryRun, {
1935
+ commits: pkg.commits,
1936
+ changelog
1937
+ });
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}`);
2126
1949
  }
1950
+ return changelog;
2127
1951
  } catch (error) {
2128
- const userHasExited = error instanceof Error && error.name === "ExitPromptError";
2129
- if (userHasExited) {
2130
- logger.log("");
2131
- logger.fail("Bump cancelled");
2132
- process.exit(0);
2133
- }
2134
- logger.fail("Error while confirming bump");
2135
- process.exit(1);
1952
+ throw new Error(`Error generating changelog for ${pkg.name} (${fromTag}...${toTag}): ${error}`);
2136
1953
  }
2137
- logger.log("");
2138
1954
  }
2139
- function getBumpedIndependentPackages({
2140
- packages,
2141
- dryRun
1955
+ function writeChangelogToFile({
1956
+ cwd,
1957
+ pkg,
1958
+ changelog,
1959
+ dryRun = false
2142
1960
  }) {
2143
- const bumpedPackages = [];
2144
- for (const pkgToBump of packages) {
2145
- logger.debug(`Bumping ${pkgToBump.name} from ${pkgToBump.version} to ${pkgToBump.newVersion} (reason: ${pkgToBump.reason})`);
2146
- const result = getBumpedPackageIndependently({
2147
- pkg: pkgToBump,
2148
- dryRun
2149
- });
2150
- if (result.bumped) {
2151
- bumpedPackages.push({
2152
- ...pkgToBump,
2153
- version: result.oldVersion
2154
- });
2155
- }
2156
- }
2157
- return bumpedPackages;
2158
- }
2159
- function shouldFilterPrereleaseTags(currentVersion, graduating) {
2160
- return !isPrerelease(currentVersion) && !graduating;
2161
- }
2162
- function extractVersionFromTag(tag, packageName) {
2163
- if (!tag) {
2164
- return null;
2165
- }
2166
- if (packageName) {
2167
- const prefix = `${packageName}@`;
2168
- if (tag.startsWith(prefix)) {
2169
- return tag.slice(prefix.length);
2170
- }
2171
- }
2172
- const atIndex = tag.lastIndexOf("@");
2173
- if (atIndex !== -1) {
2174
- return tag.slice(atIndex + 1);
2175
- }
2176
- if (tag.startsWith("v") && /^v\d/.test(tag)) {
2177
- return tag.slice(1);
1961
+ const changelogPath = join(pkg.path, "CHANGELOG.md");
1962
+ let existingChangelog = "";
1963
+ if (existsSync(changelogPath)) {
1964
+ existingChangelog = readFileSync(changelogPath, "utf8");
2178
1965
  }
2179
- if (/^\d+\.\d+\.\d+/.test(tag)) {
2180
- 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}`;
2181
1978
  }
2182
- return null;
2183
- }
2184
- function isTagVersionCompatibleWithCurrent(tagVersion, currentVersion) {
2185
- try {
2186
- const tagMajor = semver.major(tagVersion);
2187
- const currentMajor = semver.major(currentVersion);
2188
- return tagMajor <= currentMajor;
2189
- } catch {
2190
- 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})`);
2191
1986
  }
2192
1987
  }
2193
1988
 
2194
- function getIndependentTag({ version, name }) {
2195
- return `${name}@${version}`;
2196
- }
2197
- async function getLastStableTag({ logLevel, cwd }) {
2198
- const { stdout } = await execPromise(
2199
- `git tag --sort=-creatordate | grep -E '^[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+$' | head -n 1`,
2200
- {
2201
- logLevel,
2202
- noStderr: true,
2203
- noStdout: true,
2204
- noSuccess: true,
2205
- cwd
2206
- }
2207
- );
2208
- const lastTag = stdout.trim();
2209
- logger.debug("Last stable tag:", lastTag || "No stable tags found");
2210
- return lastTag;
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
2066
+ }
2067
+ },
2068
+ logLevel: "default",
2069
+ safetyCheck: true
2070
+ };
2211
2071
  }
2212
- async function getLastTag({ logLevel, cwd }) {
2213
- const { stdout } = await execPromise(`git tag --sort=-creatordate | head -n 1`, {
2214
- logLevel,
2215
- noStderr: true,
2216
- noStdout: true,
2217
- noSuccess: true,
2218
- cwd
2219
- });
2220
- const lastTag = stdout.trim();
2221
- logger.debug("Last tag:", lastTag || "No tags found");
2222
- return lastTag;
2072
+ function setupLogger(logLevel) {
2073
+ if (logLevel) {
2074
+ logger.setLevel(logLevel);
2075
+ logger.debug(`Log level set to: ${logLevel}`);
2076
+ }
2223
2077
  }
2224
- async function getAllRecentRepoTags(options) {
2225
- const limit = options?.limit;
2226
- try {
2227
- const { stdout } = await execPromise(
2228
- `git tag --sort=-creatordate | head -n ${limit}`,
2229
- {
2230
- logLevel: options?.logLevel,
2231
- noStderr: true,
2232
- noStdout: true,
2233
- noSuccess: true,
2234
- cwd: options?.cwd
2235
- }
2236
- );
2237
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
2238
- logger.debug(`Retrieved ${tags.length} recent repo tags`);
2239
- return tags;
2240
- } catch {
2241
- return [];
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
+ };
2085
+ }
2086
+ if (typeof config.repo === "string") {
2087
+ const resolvedRepoConfig = getRepoConfig(config.repo);
2088
+ config.repo = {
2089
+ ...resolvedRepoConfig,
2090
+ provider: resolvedRepoConfig.provider
2091
+ };
2242
2092
  }
2093
+ return config;
2243
2094
  }
2244
- async function getAllRecentPackageTags({
2245
- packageName,
2246
- limit = 50,
2247
- logLevel,
2248
- cwd
2249
- }) {
2250
- try {
2251
- const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
2252
- const { stdout } = await execPromise(
2253
- `git tag --sort=-creatordate | grep -E '^${escapedPackageName}@' | head -n ${limit}`,
2254
- {
2255
- logLevel,
2256
- noStderr: true,
2257
- noStdout: true,
2258
- noSuccess: true,
2259
- cwd
2260
- }
2261
- );
2262
- const tags = stdout.trim().split("\n").filter((tag) => tag.length > 0);
2263
- logger.debug(`Retrieved ${tags.length} recent tags for package ${packageName}`);
2264
- return tags;
2265
- } catch {
2266
- return [];
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
2108
+ });
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
  }
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;
2268
2121
  }
2269
- function filterCompatibleTags({
2270
- tags,
2271
- currentVersion,
2272
- onlyStable,
2273
- packageName
2122
+ function defineConfig(config) {
2123
+ return config;
2124
+ }
2125
+
2126
+ async function githubIndependentMode({
2127
+ config,
2128
+ dryRun,
2129
+ bumpResult,
2130
+ force,
2131
+ suffix
2274
2132
  }) {
2275
- const filtered = tags.filter((tag) => {
2276
- const tagVersion = extractVersionFromTag(tag, packageName);
2277
- if (!tagVersion) {
2278
- logger.debug(`Skipping tag ${tag}: cannot extract version`);
2279
- return false;
2280
- }
2281
- if (onlyStable && isPrerelease(tagVersion)) {
2282
- logger.debug(`Skipping tag ${tag}: prerelease version ${tagVersion} (onlyStable=${onlyStable})`);
2283
- return false;
2284
- }
2285
- if (!isTagVersionCompatibleWithCurrent(tagVersion, currentVersion)) {
2286
- logger.debug(`Skipping tag ${tag}: version ${tagVersion} has higher major than current ${currentVersion}`);
2287
- return false;
2288
- }
2289
- logger.debug(`Tag ${tag} with version ${tagVersion} is compatible`);
2290
- return true;
2133
+ const repoConfig = config.repo;
2134
+ if (!repoConfig) {
2135
+ throw new Error("No repository configuration found. Please check your changelog config.");
2136
+ }
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
2291
2146
  });
2292
- logger.debug(`Filtered ${tags.length} tags down to ${filtered.length} compatible tags`);
2293
- return filtered;
2294
- }
2295
- function getLastRepoTag(options) {
2296
- if (options?.currentVersion) {
2297
- return getLastRepoTagWithFiltering({
2298
- currentVersion: options.currentVersion,
2299
- onlyStable: options.onlyStable ?? false,
2300
- logLevel: options.logLevel,
2301
- cwd: options.cwd
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;
2156
+ }
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
2302
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
+ }
2303
2196
  }
2304
- if (options?.onlyStable) {
2305
- return getLastStableTag({ logLevel: options?.logLevel, cwd: options?.cwd });
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!`);
2306
2201
  }
2307
- return getLastTag({ logLevel: options?.logLevel, cwd: options?.cwd });
2202
+ return postedReleases;
2308
2203
  }
2309
- async function getLastRepoTagWithFiltering({
2310
- currentVersion,
2311
- onlyStable,
2312
- logLevel,
2313
- cwd
2204
+ async function githubUnified({
2205
+ config,
2206
+ dryRun,
2207
+ rootPackage,
2208
+ bumpResult
2314
2209
  }) {
2315
- const recentTags = await getAllRecentRepoTags({ limit: 50, logLevel, cwd });
2316
- if (recentTags.length === 0) {
2317
- logger.debug("No tags found in repository");
2318
- return null;
2210
+ const repoConfig = config.repo;
2211
+ if (!repoConfig) {
2212
+ throw new Error("No repository configuration found. Please check your changelog config.");
2319
2213
  }
2320
- const compatibleTags = filterCompatibleTags({
2321
- tags: recentTags,
2322
- currentVersion,
2323
- onlyStable
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.");
2217
+ }
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
2324
2225
  });
2325
- if (compatibleTags.length === 0) {
2326
- logger.debug("No compatible tags found");
2327
- return null;
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);
2328
2250
  }
2329
- const lastTag = compatibleTags[0];
2330
- logger.debug(`Last compatible repo tag: ${lastTag}`);
2331
- return lastTag;
2251
+ logger.success(`Release ${to} published to GitHub!`);
2252
+ return [{
2253
+ name: to,
2254
+ tag: to,
2255
+ version: to,
2256
+ prerelease: release.prerelease
2257
+ }];
2332
2258
  }
2333
- async function getLastPackageTag({
2334
- packageName,
2335
- onlyStable,
2336
- currentVersion,
2337
- logLevel,
2338
- cwd
2339
- }) {
2340
- if (currentVersion) {
2341
- return getLastPackageTagWithFiltering({
2342
- packageName,
2343
- currentVersion,
2344
- onlyStable: onlyStable ?? false,
2345
- logLevel,
2346
- cwd
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
2347
2309
  });
2310
+ } catch (error) {
2311
+ logger.error("Error publishing GitHub release:", error);
2312
+ throw error;
2313
+ }
2314
+ }
2315
+
2316
+ async function createGitlabRelease({
2317
+ config,
2318
+ release,
2319
+ dryRun
2320
+ }) {
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");
2348
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
+ };
2349
2342
  try {
2350
- const escapedPackageName = packageName.replace(/[@/]/g, "\\$&");
2351
- let grepPattern;
2352
- if (onlyStable) {
2353
- grepPattern = `^${escapedPackageName}@[0-9]+\\.[0-9]+\\.[0-9]+$`;
2354
- } else {
2355
- grepPattern = `^${escapedPackageName}@`;
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
+ };
2356
2355
  }
2357
- const { stdout } = await execPromise(
2358
- `git tag --sort=-creatordate | grep -E '${grepPattern}' | sed -n '1p'`,
2359
- {
2360
- logLevel,
2361
- noStderr: true,
2362
- noStdout: true,
2363
- noSuccess: true,
2364
- cwd
2365
- }
2366
- );
2367
- const tag = stdout.trim();
2368
- return tag || null;
2369
- } catch {
2370
- return null;
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)
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;
2371
2375
  }
2372
2376
  }
2373
- async function getLastPackageTagWithFiltering({
2374
- packageName,
2375
- currentVersion,
2376
- onlyStable,
2377
- logLevel,
2378
- cwd
2377
+ async function gitlabIndependentMode({
2378
+ config,
2379
+ dryRun,
2380
+ bumpResult,
2381
+ suffix,
2382
+ force
2379
2383
  }) {
2380
- const recentTags = await getAllRecentPackageTags({
2381
- packageName,
2382
- limit: 50,
2383
- logLevel,
2384
- cwd
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
2385
2390
  });
2386
- if (recentTags.length === 0) {
2387
- logger.debug(`No tags found for package ${packageName}`);
2388
- return null;
2389
- }
2390
- const compatibleTags = filterCompatibleTags({
2391
- tags: recentTags,
2392
- currentVersion,
2393
- onlyStable,
2394
- packageName
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
2395
2398
  });
2396
- if (compatibleTags.length === 0) {
2397
- logger.debug(`No compatible tags found for package ${packageName}`);
2398
- return null;
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;
2407
+ }
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;
2418
+ }
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
+ }
2399
2443
  }
2400
- const lastTag = compatibleTags[0];
2401
- logger.debug(`Last compatible package tag for ${packageName}: ${lastTag}`);
2402
- return lastTag;
2403
- }
2404
- async function resolveFromTagIndependent({
2405
- cwd,
2406
- packageName,
2407
- currentVersion,
2408
- graduating,
2409
- logLevel
2410
- }) {
2411
- const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
2412
- const onlyStable = graduating || filterPrereleases;
2413
- const lastPackageTag = await getLastPackageTag({
2414
- packageName,
2415
- currentVersion,
2416
- onlyStable,
2417
- logLevel,
2418
- cwd
2419
- });
2420
- if (!lastPackageTag) {
2421
- return getFirstCommit(cwd);
2444
+ if (postedReleases.length === 0) {
2445
+ logger.warn("No releases created");
2446
+ } else {
2447
+ logger.success(`Releases ${postedReleases.map((r) => r.tag).join(", ")} published to GitLab!`);
2422
2448
  }
2423
- return lastPackageTag;
2449
+ return postedReleases;
2424
2450
  }
2425
- async function resolveFromTagUnified({
2451
+ async function gitlabUnified({
2426
2452
  config,
2427
- currentVersion,
2428
- graduating,
2429
- logLevel
2453
+ dryRun,
2454
+ rootPackage,
2455
+ bumpResult
2430
2456
  }) {
2431
- const filterPrereleases = shouldFilterPrereleaseTags(currentVersion, graduating);
2432
- const onlyStable = graduating || filterPrereleases;
2433
- const from = await getLastRepoTag({
2434
- currentVersion,
2435
- onlyStable,
2436
- logLevel,
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,
2462
+ config,
2463
+ dryRun,
2464
+ newVersion
2465
+ });
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,
2437
2472
  cwd: config.cwd
2438
- }) || getFirstCommit(config.cwd);
2439
- return from;
2440
- }
2441
- async function resolveFromTag({
2442
- config,
2443
- versionMode,
2444
- step,
2445
- packageName,
2446
- currentVersion,
2447
- graduating,
2448
- logLevel
2449
- }) {
2450
- let from;
2451
- if (versionMode === "independent") {
2452
- if (!packageName) {
2453
- throw new Error("Package name is required for independent version mode");
2454
- }
2455
- from = await resolveFromTagIndependent({
2456
- cwd: config.cwd,
2457
- packageName,
2458
- currentVersion,
2459
- graduating,
2460
- logLevel
2461
- });
2473
+ });
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);
2462
2488
  } else {
2463
- from = await resolveFromTagUnified({
2489
+ logger.debug("Publishing release to GitLab...");
2490
+ await createGitlabRelease({
2464
2491
  config,
2465
- currentVersion,
2466
- graduating,
2467
- logLevel
2492
+ release,
2493
+ dryRun
2468
2494
  });
2469
2495
  }
2470
- logger.debug(`[${versionMode}](${step}) Using from tag: ${from}`);
2471
- return config.from || from;
2496
+ logger.success(`Release ${to} published to GitLab!`);
2497
+ return [{
2498
+ name: to,
2499
+ tag: to,
2500
+ version: to,
2501
+ prerelease: isPrerelease(newVersion)
2502
+ }];
2472
2503
  }
2473
- function resolveToTag({
2474
- config,
2475
- versionMode,
2476
- newVersion,
2477
- step,
2478
- packageName
2479
- }) {
2480
- const isUntaggedStep = step === "bump" || step === "changelog";
2481
- let to;
2482
- if (isUntaggedStep) {
2483
- to = getCurrentGitRef(config.cwd);
2484
- } else if (versionMode === "independent") {
2485
- if (!packageName) {
2486
- throw new Error("Package name is required for independent version mode");
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
+ });
2487
2528
  }
2488
- if (!newVersion) {
2489
- throw new Error("New version is required for independent version mode");
2529
+ const rootPackageBase = readPackageJson(config.cwd);
2530
+ if (!rootPackageBase) {
2531
+ throw new Error("Failed to read root package.json");
2490
2532
  }
2491
- to = getIndependentTag({ version: newVersion, name: packageName });
2492
- } else {
2493
- to = newVersion ? config.templates.tagBody.replace("{{newVersion}}", newVersion) : getCurrentGitRef(config.cwd);
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;
2494
2558
  }
2495
- logger.debug(`[${versionMode}](${step}) Using to tag: ${to}`);
2496
- return config.to || to;
2497
- }
2498
- async function resolveTags({
2499
- config,
2500
- step,
2501
- pkg,
2502
- newVersion
2503
- }) {
2504
- const versionMode = config.monorepo?.versionMode || "standalone";
2505
- const logLevel = config.logLevel;
2506
- logger.debug(`[${versionMode}](${step}) Resolving tags`);
2507
- const releaseType = config.bump.type;
2508
- const graduating = typeof newVersion === "string" ? isGraduatingToStableBetweenVersion(pkg.version, newVersion) : isGraduating(pkg.version, releaseType);
2509
- const from = await resolveFromTag({
2510
- config,
2511
- versionMode,
2512
- step,
2513
- packageName: pkg.name,
2514
- currentVersion: pkg.version,
2515
- graduating,
2516
- logLevel
2517
- });
2518
- const to = resolveToTag({
2519
- config,
2520
- versionMode,
2521
- newVersion,
2522
- step,
2523
- packageName: pkg.name
2524
- });
2525
- logger.debug(`[${versionMode}](${step}) Using tags: ${from} \u2192 ${to}`);
2526
- return { from, to };
2527
2559
  }
2528
2560
 
2529
2561
  let sessionOtp;
@@ -2712,7 +2744,7 @@ async function handleOtpError() {
2712
2744
  logger.debug("OTP received, retrying publish...");
2713
2745
  return otp;
2714
2746
  } catch (promptError) {
2715
- logger.error("Failed to get OTP");
2747
+ logger.fail("Failed to get OTP");
2716
2748
  throw promptError;
2717
2749
  }
2718
2750
  }
@@ -2756,69 +2788,362 @@ function getAuthCommand({
2756
2788
  });
2757
2789
  return `${packageManager} ${args.join(" ")}`;
2758
2790
  }
2759
- function getPublishCommand({
2760
- packageManager,
2761
- tag,
2762
- config,
2763
- otp,
2764
- dryRun
2791
+ function getPublishCommand({
2792
+ packageManager,
2793
+ tag,
2794
+ config,
2795
+ otp,
2796
+ dryRun
2797
+ }) {
2798
+ const args = getCommandArgs({
2799
+ packageManager,
2800
+ tag,
2801
+ config,
2802
+ otp,
2803
+ dryRun,
2804
+ type: "publish"
2805
+ });
2806
+ const baseCommand = packageManager === "yarn" && isYarnBerry() ? "yarn npm" : packageManager;
2807
+ return `${baseCommand} ${args.join(" ")}`;
2808
+ }
2809
+ async function publishPackage({
2810
+ pkg,
2811
+ config,
2812
+ packageManager,
2813
+ dryRun
2814
+ }) {
2815
+ const tag = determinePublishTag(pkg.newVersion || pkg.version, config.publish.tag);
2816
+ const packageNameAndVersion = getIndependentTag({ name: pkg.name, version: pkg.newVersion || pkg.version });
2817
+ logger.debug(`Building publish command for ${pkg.name}`);
2818
+ let dynamicOtp;
2819
+ const maxAttempts = 2;
2820
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2821
+ try {
2822
+ const command = getPublishCommand({
2823
+ packageManager,
2824
+ tag,
2825
+ config,
2826
+ otp: dynamicOtp,
2827
+ dryRun
2828
+ });
2829
+ process.chdir(pkg.path);
2830
+ await executePublishCommand({
2831
+ command,
2832
+ packageNameAndVersion,
2833
+ packageManager,
2834
+ pkg,
2835
+ config,
2836
+ dryRun,
2837
+ tag
2838
+ });
2839
+ if (dynamicOtp && !sessionOtp) {
2840
+ sessionOtp = dynamicOtp;
2841
+ logger.debug("OTP stored for session");
2842
+ }
2843
+ return;
2844
+ } catch (error) {
2845
+ if (isOtpError(error) && attempt < maxAttempts - 1) {
2846
+ dynamicOtp = await handleOtpError();
2847
+ } else {
2848
+ logger.error(`Failed to publish ${packageNameAndVersion}:`, error);
2849
+ throw error;
2850
+ }
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
2765
3109
  }) {
2766
- const args = getCommandArgs({
2767
- packageManager,
2768
- tag,
2769
- config,
2770
- otp,
2771
- dryRun,
2772
- type: "publish"
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
2773
3119
  });
2774
- const baseCommand = packageManager === "yarn" && isYarnBerry() ? "yarn npm" : packageManager;
2775
- return `${baseCommand} ${args.join(" ")}`;
2776
- }
2777
- async function publishPackage({
2778
- pkg,
2779
- config,
2780
- packageManager,
2781
- dryRun
2782
- }) {
2783
- const tag = determinePublishTag(pkg.newVersion || pkg.version, config.publish.tag);
2784
- const packageNameAndVersion = getIndependentTag({ name: pkg.name, version: pkg.newVersion || pkg.version });
2785
- logger.debug(`Building publish command for ${pkg.name}`);
2786
- let dynamicOtp;
2787
- const maxAttempts = 2;
2788
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
2789
- try {
2790
- const command = getPublishCommand({
2791
- packageManager,
2792
- tag,
2793
- config,
2794
- otp: dynamicOtp,
2795
- dryRun
2796
- });
2797
- process.chdir(pkg.path);
2798
- await executePublishCommand({
2799
- command,
2800
- packageNameAndVersion,
2801
- packageManager,
2802
- pkg,
2803
- config,
2804
- dryRun,
2805
- tag
2806
- });
2807
- if (dynamicOtp && !sessionOtp) {
2808
- sessionOtp = dynamicOtp;
2809
- logger.debug("OTP stored for session");
2810
- }
2811
- return;
2812
- } catch (error) {
2813
- if (isOtpError(error) && attempt < maxAttempts - 1) {
2814
- dynamicOtp = await handleOtpError();
2815
- } else {
2816
- logger.error(`Failed to publish ${packageNameAndVersion}:`, error);
2817
- throw error;
2818
- }
2819
- } finally {
2820
- process.chdir(config.cwd);
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");
2821
3144
  }
3145
+ logger.error("Failed to post tweet:", error.message || error);
3146
+ throw error;
2822
3147
  }
2823
3148
  }
2824
3149
 
@@ -2856,7 +3181,6 @@ async function bumpUnifiedMode({
2856
3181
  logger.debug(`${currentVersion} \u2192 ${newVersion} (${config.monorepo?.versionMode || "standalone"} mode)`);
2857
3182
  const packages = await getPackages({
2858
3183
  config,
2859
- patterns: config.monorepo?.packages,
2860
3184
  suffix,
2861
3185
  force
2862
3186
  });
@@ -2877,7 +3201,8 @@ async function bumpUnifiedMode({
2877
3201
  } else {
2878
3202
  logger.info(`${packages.length === 1 ? packages[0].name : packages.length} package(s) bumped from ${currentVersion} to ${newVersion} (${config.monorepo?.versionMode || "standalone"} mode)`);
2879
3203
  }
2880
- for (const pkg of [rootPackage, ...packages]) {
3204
+ const packagesToWrite = [rootPackage, ...packages];
3205
+ for (const pkg of packagesToWrite) {
2881
3206
  writeVersion(pkg.path, newVersion, dryRun);
2882
3207
  }
2883
3208
  updateLernaVersion({
@@ -2938,7 +3263,6 @@ async function bumpSelectiveMode({
2938
3263
  logger.debug("Determining packages to bump...");
2939
3264
  const packages = await getPackages({
2940
3265
  config,
2941
- patterns: config.monorepo?.packages,
2942
3266
  suffix,
2943
3267
  force
2944
3268
  });
@@ -2969,7 +3293,8 @@ async function bumpSelectiveMode({
2969
3293
  }
2970
3294
  }
2971
3295
  logger.debug(`Writing version to ${packages.length} package(s)`);
2972
- for (const pkg of [rootPackage, ...packages]) {
3296
+ const packagesToWrite = [rootPackage, ...packages];
3297
+ for (const pkg of packagesToWrite) {
2973
3298
  writeVersion(pkg.path, newVersion, dryRun);
2974
3299
  }
2975
3300
  updateLernaVersion({
@@ -3006,7 +3331,6 @@ async function bumpIndependentMode({
3006
3331
  logger.debug("Starting bump in independent mode");
3007
3332
  const packagesToBump = await getPackages({
3008
3333
  config,
3009
- patterns: config.monorepo?.packages,
3010
3334
  suffix,
3011
3335
  force
3012
3336
  });
@@ -3078,7 +3402,7 @@ async function bump(options = {}) {
3078
3402
  checkGitStatusIfDirty();
3079
3403
  }
3080
3404
  await fetchGitTags(config.cwd);
3081
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3405
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3082
3406
  const packages = readPackages({
3083
3407
  cwd: config.cwd,
3084
3408
  patterns: config.monorepo?.packages,
@@ -3104,7 +3428,7 @@ async function bump(options = {}) {
3104
3428
  logger.success(`${dryRun ? "[dry-run] " : ""}Version bump completed (${resultLog} package${resultLog === 1 || typeof resultLog === "string" ? "" : "s"} bumped)`);
3105
3429
  } else {
3106
3430
  logger.fail("No packages to bump, no relevant commits found");
3107
- exit(1);
3431
+ process$1.exit(1);
3108
3432
  }
3109
3433
  await executeHook("success:bump", config, dryRun);
3110
3434
  return result;
@@ -3227,20 +3551,20 @@ async function changelog(options = {}) {
3227
3551
  });
3228
3552
  const dryRun = options.dryRun ?? false;
3229
3553
  logger.debug(`Dry run: ${dryRun}`);
3230
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3554
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3231
3555
  try {
3232
3556
  await executeHook("before:changelog", config, dryRun);
3233
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
+ });
3234
3564
  if (config.changelog?.rootChangelog && config.monorepo) {
3235
3565
  if (config.monorepo.versionMode === "independent") {
3236
- const packages2 = await getPackagesOrBumpedPackages({
3237
- config,
3238
- bumpResult: options.bumpResult,
3239
- suffix: options.suffix,
3240
- force: options.force ?? false
3241
- });
3242
3566
  await generateIndependentRootChangelog({
3243
- packages: packages2,
3567
+ packages,
3244
3568
  config,
3245
3569
  dryRun
3246
3570
  });
@@ -3257,12 +3581,6 @@ async function changelog(options = {}) {
3257
3581
  logger.debug("Skipping root changelog generation");
3258
3582
  }
3259
3583
  logger.debug("Generating package changelogs...");
3260
- const packages = await getPackagesOrBumpedPackages({
3261
- config,
3262
- bumpResult: options.bumpResult,
3263
- suffix: options.suffix,
3264
- force: options.force ?? false
3265
- });
3266
3584
  logger.debug(`Processing ${packages.length} package(s)`);
3267
3585
  let generatedCount = 0;
3268
3586
  for await (const pkg of packages) {
@@ -3309,22 +3627,28 @@ async function changelog(options = {}) {
3309
3627
 
3310
3628
  function providerReleaseSafetyCheck({ config, provider }) {
3311
3629
  if (!config.safetyCheck || !config.release.providerRelease) {
3630
+ logger.debug("Safety check disabled or provider release disabled");
3312
3631
  return;
3313
3632
  }
3633
+ logger.debug("Start checking provider release config");
3314
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
+ }
3315
3640
  let token;
3316
3641
  if (internalProvider === "github") {
3317
3642
  token = config.tokens?.github || config.repo?.token;
3318
3643
  } else if (internalProvider === "gitlab") {
3319
3644
  token = config.tokens?.gitlab || config.repo?.token;
3320
3645
  } else {
3321
- logger.error(`[provider-release-safety-check] Unsupported Git provider: ${internalProvider || "unknown"}`);
3322
- process.exit(1);
3646
+ throw new Error(`Unsupported Git provider: ${internalProvider || "unknown"}`);
3323
3647
  }
3324
3648
  if (!token) {
3325
- 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`);
3326
- 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`);
3327
3650
  }
3651
+ logger.info("provider release config checked successfully");
3328
3652
  }
3329
3653
  async function providerRelease(options = {}) {
3330
3654
  const config = await loadRelizyConfig({
@@ -3343,9 +3667,10 @@ async function providerRelease(options = {}) {
3343
3667
  });
3344
3668
  const dryRun = options.dryRun ?? false;
3345
3669
  logger.debug(`Dry run: ${dryRun}`);
3346
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3670
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3671
+ let detectedProvider = null;
3347
3672
  try {
3348
- const detectedProvider = options.provider || detectGitProvider();
3673
+ detectedProvider = options.provider || detectGitProvider();
3349
3674
  providerReleaseSafetyCheck({ config, provider: detectedProvider });
3350
3675
  await executeHook("before:provider-release", config, dryRun);
3351
3676
  logger.start("Start provider release");
@@ -3358,6 +3683,16 @@ async function providerRelease(options = {}) {
3358
3683
  );
3359
3684
  }
3360
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
+ }
3361
3696
  const payload = {
3362
3697
  from: config.from || options.bumpResult?.fromTag,
3363
3698
  to: config.to,
@@ -3385,20 +3720,24 @@ async function providerRelease(options = {}) {
3385
3720
  logger.error("Error publishing releases!\n\n", error);
3386
3721
  }
3387
3722
  await executeHook("error:provider-release", config, dryRun);
3388
- throw error;
3723
+ const errorMessage = error instanceof Error ? error.message : String(error);
3724
+ return {
3725
+ detectedProvider: detectedProvider || "github",
3726
+ postedReleases: [],
3727
+ error: errorMessage
3728
+ };
3389
3729
  }
3390
3730
  }
3391
3731
 
3392
3732
  async function publishSafetyCheck({ config }) {
3393
- logger.debug("[publish-safety-check] Running publish safety check");
3394
3733
  if (!config.safetyCheck || !config.release.publish || !config.publish.safetyCheck) {
3395
- logger.debug("[publish-safety-check] Safety check disabled or publish disabled");
3734
+ logger.debug("Safety check disabled or publish disabled");
3396
3735
  return;
3397
3736
  }
3737
+ logger.debug("Start checking auth config to package registry");
3398
3738
  const packageManager = config.publish.packageManager || detectPackageManager(config.cwd);
3399
3739
  if (!packageManager) {
3400
- logger.error("[publish-safety-check] Unable to detect package manager");
3401
- process.exit(1);
3740
+ throw new Error("Unable to detect package manager");
3402
3741
  }
3403
3742
  const isPnpmOrNpm = packageManager === "pnpm" || packageManager === "npm";
3404
3743
  if (isPnpmOrNpm) {
@@ -3408,7 +3747,7 @@ async function publishSafetyCheck({ config }) {
3408
3747
  otp: config.publish.otp
3409
3748
  });
3410
3749
  try {
3411
- logger.debug("[publish-safety-check] Authenticating to package registry...");
3750
+ logger.debug("Authenticating to package registry...");
3412
3751
  await execPromise(authCommand, {
3413
3752
  cwd: config.cwd,
3414
3753
  noStderr: true,
@@ -3416,10 +3755,9 @@ async function publishSafetyCheck({ config }) {
3416
3755
  logLevel: config.logLevel,
3417
3756
  noSuccess: true
3418
3757
  });
3419
- logger.info("[publish-safety-check] Successfully authenticated to package registry");
3758
+ logger.info("Successfully authenticated to package registry");
3420
3759
  } catch (error) {
3421
- logger.error("[publish-safety-check] Failed to authenticate to package registry:", error);
3422
- process.exit(1);
3760
+ throw new Error("Failed to authenticate to package registry", { cause: error });
3423
3761
  }
3424
3762
  }
3425
3763
  }
@@ -3444,7 +3782,7 @@ async function publish(options = {}) {
3444
3782
  logger.debug(`Dry run: ${dryRun}`);
3445
3783
  const packageManager = config.publish.packageManager || detectPackageManager(config.cwd);
3446
3784
  logger.debug(`Package manager: ${packageManager}`);
3447
- logger.info(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3785
+ logger.debug(`Version mode: ${config.monorepo?.versionMode || "standalone"}`);
3448
3786
  if (config.publish.registry) {
3449
3787
  logger.debug(`Registry: ${config.publish.registry}`);
3450
3788
  }
@@ -3519,6 +3857,286 @@ async function publish(options = {}) {
3519
3857
  }
3520
3858
  }
3521
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
+
3522
4140
  function getReleaseConfig(options = {}) {
3523
4141
  return loadRelizyConfig({
3524
4142
  configFile: options.configName,
@@ -3556,7 +4174,8 @@ function getReleaseConfig(options = {}) {
3556
4174
  noVerify: options.noVerify,
3557
4175
  providerRelease: options.providerRelease,
3558
4176
  clean: options.clean,
3559
- gitTag: options.gitTag
4177
+ gitTag: options.gitTag,
4178
+ social: options.social
3560
4179
  },
3561
4180
  safetyCheck: options.safetyCheck
3562
4181
  }
@@ -3569,8 +4188,19 @@ async function releaseSafetyCheck({
3569
4188
  if (!config.safetyCheck) {
3570
4189
  return;
3571
4190
  }
3572
- providerReleaseSafetyCheck({ config, provider });
3573
- 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
+ }
3574
4204
  }
3575
4205
  async function release(options = {}) {
3576
4206
  const dryRun = options.dryRun ?? false;
@@ -3583,7 +4213,7 @@ async function release(options = {}) {
3583
4213
  await releaseSafetyCheck({ config, provider: options.provider });
3584
4214
  try {
3585
4215
  await executeHook("before:release", config, dryRun);
3586
- logger.box("Step 1/6: Bump versions");
4216
+ logger.box("Bump versions");
3587
4217
  const bumpResult = await bump({
3588
4218
  type: config.bump.type,
3589
4219
  preid: config.bump.preid,
@@ -3598,7 +4228,7 @@ async function release(options = {}) {
3598
4228
  logger.debug("No packages bumped");
3599
4229
  return;
3600
4230
  }
3601
- logger.box("Step 2/6: Generate changelogs");
4231
+ logger.box("Generate changelogs");
3602
4232
  if (config.release.changelog) {
3603
4233
  await changelog({
3604
4234
  from: config.from,
@@ -3616,7 +4246,7 @@ async function release(options = {}) {
3616
4246
  } else {
3617
4247
  logger.info("Skipping changelog generation (--no-changelog)");
3618
4248
  }
3619
- logger.box("Step 3/6: Publish packages to registry");
4249
+ logger.box("Publish packages to registry");
3620
4250
  let publishResponse;
3621
4251
  if (config.release.publish) {
3622
4252
  try {
@@ -3630,17 +4260,18 @@ async function release(options = {}) {
3630
4260
  config,
3631
4261
  configName: options.configName,
3632
4262
  suffix: options.suffix,
3633
- force
4263
+ force,
4264
+ safetyCheck: false
3634
4265
  });
3635
4266
  } catch (error) {
3636
- logger.error("Publish failed, rolling back modified files...");
4267
+ logger.fail("Publish failed, rolling back modified files...");
3637
4268
  await rollbackModifiedFiles({ config });
3638
4269
  throw error;
3639
4270
  }
3640
4271
  } else {
3641
4272
  logger.info("Skipping publish (--no-publish)");
3642
4273
  }
3643
- logger.box("Step 4/6: Commit changes and create tag");
4274
+ logger.box("Commit changes and create tag");
3644
4275
  let createdTags = [];
3645
4276
  if (config.release.commit) {
3646
4277
  createdTags = await createCommitAndTags({
@@ -3654,7 +4285,7 @@ async function release(options = {}) {
3654
4285
  } else {
3655
4286
  logger.info("Skipping commit and tag (--no-commit)");
3656
4287
  }
3657
- logger.box("Step 5/6: Push changes and tags");
4288
+ logger.box("Push changes and tags");
3658
4289
  if (config.release.push && config.release.commit) {
3659
4290
  await executeHook("before:push", config, dryRun);
3660
4291
  try {
@@ -3674,40 +4305,75 @@ async function release(options = {}) {
3674
4305
  }
3675
4306
  let provider = config.repo?.provider;
3676
4307
  let postedReleases = [];
3677
- logger.box("Step 6/6: Publish Git release");
4308
+ let providerError;
4309
+ logger.box("Publish Git release");
3678
4310
  if (config.release.providerRelease) {
3679
4311
  logger.debug(`Provider from config: ${provider}`);
3680
- try {
3681
- const response = await providerRelease({
3682
- from: config.from,
3683
- to: config.to,
3684
- token: options.token,
3685
- provider,
3686
- dryRun,
3687
- config,
3688
- logLevel: config.logLevel,
3689
- bumpResult,
3690
- configName: options.configName,
3691
- force,
3692
- suffix: options.suffix
3693
- });
3694
- provider = response.detectedProvider;
3695
- postedReleases = response.postedReleases;
3696
- } catch (error) {
3697
- logger.error("Error during release publication:", error);
3698
- }
4312
+ const response = await providerRelease({
4313
+ from: config.from,
4314
+ to: config.to,
4315
+ token: options.token,
4316
+ provider,
4317
+ dryRun,
4318
+ config,
4319
+ logLevel: config.logLevel,
4320
+ bumpResult,
4321
+ configName: options.configName,
4322
+ force,
4323
+ suffix: options.suffix,
4324
+ safetyCheck: false
4325
+ });
4326
+ provider = response.detectedProvider;
4327
+ postedReleases = response.postedReleases;
4328
+ providerError = response.error;
3699
4329
  } else {
3700
4330
  logger.info("Skipping release (--no-provider-release)");
3701
4331
  }
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
+ });
4346
+ } else {
4347
+ logger.info("Skipping social media posts (--no-social or no social media enabled)");
4348
+ }
3702
4349
  const publishedPackageCount = publishResponse?.publishedPackages.length ?? 0;
3703
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
+ }
3704
4369
  logger.box(`Release workflow completed!
3705
4370
 
3706
4371
  Version: ${versionDisplay ?? "Unknown"}
3707
- Tag(s): ${createdTags.length ? createdTags.join(", ") : "No"}
4372
+ Tag(s): ${createdTags?.length ? createdTags.join(", ") : "None"}
3708
4373
  Pushed: ${config.release.push ? "Yes" : "Disabled"}
3709
4374
  Published packages: ${config.release.publish ? publishedPackageCount : "Disabled"}
3710
- Published release: ${config.release.providerRelease ? postedReleases.length : "Disabled"}
4375
+ Provider release: ${providerDisplay}
4376
+ Social media: ${socialDisplay}
3711
4377
  Git provider: ${provider}`);
3712
4378
  await executeHook("success:release", config, dryRun);
3713
4379
  } catch (error) {
@@ -3717,4 +4383,4 @@ Git provider: ${provider}`);
3717
4383
  }
3718
4384
  }
3719
4385
 
3720
- export { executeBuildCmd as $, getCurrentGitBranch as A, getCurrentGitRef as B, github as C, createGitlabRelease as D, gitlab as E, detectPackageManager as F, determinePublishTag as G, getPackagesToPublishInSelectiveMode as H, getPackagesToPublishInIndependentMode as I, getAuthCommand as J, publishPackage as K, readPackageJson as L, getRootPackage as M, readPackages as N, getPackages as O, getPackageCommits as P, hasLernaJson as Q, getIndependentTag as R, getLastStableTag as S, getLastTag as T, getLastRepoTag as U, getLastPackageTag as V, resolveTags as W, executeHook as X, isInCI as Y, getCIName as Z, executeFormatCmd as _, providerRelease as a, isBumpedPackage as a0, getPackagesOrBumpedPackages as a1, isGraduatingToStableBetweenVersion as a2, determineSemverChange as a3, determineReleaseType as a4, writeVersion as a5, getPackageNewVersion as a6, updateLernaVersion as a7, extractVersionFromPackageTag as a8, isPrerelease as a9, isStableReleaseType as aa, isPrereleaseReleaseType as ab, isGraduating as ac, getPreid as ad, isChangedPreid as ae, getBumpedPackageIndependently as af, confirmBump as ag, getBumpedIndependentPackages as ah, shouldFilterPrereleaseTags as ai, extractVersionFromTag as aj, isTagVersionCompatibleWithCurrent as ak, 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, getModifiedReleaseFilePatterns as u, createCommitAndTags as v, writeChangelogToFile as w, pushCommitAndTags as x, rollbackModifiedFiles as y, getFirstCommit 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 };