relizy 1.5.0-beta.2 → 1.5.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import process from 'node:process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { printBanner, logger } from '@maz-ui/node';
7
7
  import { Command } from 'commander';
8
- import { ar as isInCI, R as getCIName, e as bump, g as changelog, aG as publish, aE as providerRelease, aY as social, aD as prComment, aP as release } from './shared/relizy.B0MWtDvR.mjs';
8
+ import { av as isInCI, S as getCIName, f as bump, h as changelog, aM as publish, aK as providerRelease, b3 as social, aJ as prComment, aV as release } from './shared/relizy.CobLJLr_.mjs';
9
9
  import 'node:child_process';
10
10
  import '@maz-ui/utils/helpers/formatJson';
11
11
  import 'c12';
@@ -109,7 +109,7 @@ program.command("changelog").description("Generate changelogs for all packages")
109
109
  process.exit(1);
110
110
  }
111
111
  });
112
- program.command("publish").description("Publish packages to registry").option("--registry <url>", "Custom registry URL").option("--tag <tag>", "Publish with specific tag (default: latest for stable, next for prerelease)").option("--access <type>", "Package access level (public or restricted)").option("--otp <code>", "One-time password for 2FA").option("--build-cmd <cmd>", 'Command to build packages before publish (e.g. "pnpm build")').option("--publish-token <token>", 'NPM token (e.g. "123456") - only supported for pnpm and npm').action(async (options) => {
112
+ program.command("publish").description("Publish packages to registry").option("--registry <url>", "Custom registry URL").option("--tag <tag>", "Publish with specific tag (default: latest for stable, next for prerelease)").option("--access <type>", "Package access level (public or restricted)").option("--otp <code>", "One-time password for 2FA").option("--build-cmd <cmd>", 'Command to build packages before publish (e.g. "pnpm build")').option("--publish-token <token>", 'NPM token (e.g. "123456") - only supported for pnpm and npm').option("--skip-existing-versions", "Skip packages whose version already exists on the registry instead of failing").action(async (options) => {
113
113
  try {
114
114
  await publish({
115
115
  registry: options.registry,
@@ -118,6 +118,7 @@ program.command("publish").description("Publish packages to registry").option("-
118
118
  otp: options.otp,
119
119
  buildCmd: options.buildCmd,
120
120
  token: options.publishToken,
121
+ skipExistingVersions: options.skipExistingVersions,
121
122
  dryRun: program.opts().dryRun,
122
123
  logLevel: program.opts().logLevel,
123
124
  configName: program.opts().config
@@ -173,7 +174,7 @@ program.command("pr-comment").description("Post or re-post a PR comment with rel
173
174
  process.exit(1);
174
175
  }
175
176
  });
176
- program.command("release").description("Complete release workflow (bump + changelog + commit + tag + push to remote + publish release)").option("--major", "Bump major version").option("--minor", "Bump minor version").option("--patch", "Bump patch version").option("--prerelease", "Bump prerelease version").option("--premajor", "Bump premajor version").option("--preminor", "Bump preminor version").option("--prepatch", "Bump prepatch version").option("--preid <id>", "Prerelease identifier (alpha, beta, rc, etc.)").option("--suffix <suffix>", "Custom suffix for prerelease versions - replace the last .X with .suffix (e.g. 1.0.0-beta.0 -> 1.0.0-beta.suffix)").option("--from <ref>", "Start commit reference").option("--to <ref>", "End commit reference").option("--no-push", "Skip push changes and tags to remote").option("--no-provider-release", "Skip release creation (GitHub/GitLab)").option("--no-publish", "Skip npm publish").option("--registry <url>", "Custom npm registry URL").option("--tag <tag>", "Publish with specific tag (default: latest for stable, next for prerelease)").option("--access <type>", "Package access level (public or restricted)").option("--otp <code>", "One-time password for 2FA").option("--no-verify", "Skip git hooks during commit").option("--format-cmd <cmd>", 'Command to format CHANGELOG files after generation (e.g. "pnpm lint")').option("--build-cmd <cmd>", 'Command to build packages before publish (e.g. "pnpm build")').option("--no-root-changelog", "Skip generation of root changelog file").option("--token <token>", "Git token (github or gitlab)").option("--force", "Bump even if there are no commits").option("--no-clean", "Skip check if the working directory is clean").option("--no-commit", "Skip commit and tag").option("--no-git-tag", "Skip tag creation").option("--no-changelog", "Skip changelog generation files").option("--provider <provider>", "Git provider (github or gitlab)").option("--no-social", "Skip social media posting").option("--no-pr-comment", "Skip PR comment posting").option("--yes", "Skip confirmation prompt about bumping packages").option("--publish-token <token>", 'NPM token (e.g. "123456") - only supported for pnpm and npm').option("--canary", "Publish a canary release from the current commit").option("--include-private", "Include private packages in bump and changelog phases").option("--ai", "Force-enable AI for release notes and social posts").option("--no-ai", "Force-disable AI for release notes and social posts").action(async (options) => {
177
+ program.command("release").description("Complete release workflow (bump + changelog + commit + tag + push to remote + publish release)").option("--major", "Bump major version").option("--minor", "Bump minor version").option("--patch", "Bump patch version").option("--prerelease", "Bump prerelease version").option("--premajor", "Bump premajor version").option("--preminor", "Bump preminor version").option("--prepatch", "Bump prepatch version").option("--preid <id>", "Prerelease identifier (alpha, beta, rc, etc.)").option("--suffix <suffix>", "Custom suffix for prerelease versions - replace the last .X with .suffix (e.g. 1.0.0-beta.0 -> 1.0.0-beta.suffix)").option("--from <ref>", "Start commit reference").option("--to <ref>", "End commit reference").option("--no-push", "Skip push changes and tags to remote").option("--no-provider-release", "Skip release creation (GitHub/GitLab)").option("--no-publish", "Skip npm publish").option("--registry <url>", "Custom npm registry URL").option("--tag <tag>", "Publish with specific tag (default: latest for stable, next for prerelease)").option("--access <type>", "Package access level (public or restricted)").option("--otp <code>", "One-time password for 2FA").option("--no-verify", "Skip git hooks during commit").option("--format-cmd <cmd>", 'Command to format CHANGELOG files after generation (e.g. "pnpm lint")').option("--build-cmd <cmd>", 'Command to build packages before publish (e.g. "pnpm build")').option("--no-root-changelog", "Skip generation of root changelog file").option("--token <token>", "Git token (github or gitlab)").option("--force", "Bump even if there are no commits").option("--no-clean", "Skip check if the working directory is clean").option("--no-commit", "Skip commit and tag").option("--no-git-tag", "Skip tag creation").option("--no-changelog", "Skip changelog generation files").option("--provider <provider>", "Git provider (github or gitlab)").option("--no-social", "Skip social media posting").option("--no-pr-comment", "Skip PR comment posting").option("--yes", "Skip confirmation prompt about bumping packages").option("--publish-token <token>", 'NPM token (e.g. "123456") - only supported for pnpm and npm').option("--skip-existing-versions", "Skip packages whose version already exists on the registry instead of failing").option("--canary", "Publish a canary release from the current commit").option("--include-private", "Include private packages in bump and changelog phases").option("--ai", "Force-enable AI for release notes and social posts").option("--no-ai", "Force-disable AI for release notes and social posts").action(async (options) => {
177
178
  try {
178
179
  await release({
179
180
  type: getReleaseType(options),
@@ -201,6 +202,7 @@ program.command("release").description("Complete release workflow (bump + change
201
202
  rootChangelog: hasCliFlag("--no-root-changelog") ? false : void 0,
202
203
  token: options.token,
203
204
  publishToken: options.publishToken,
205
+ skipExistingVersions: options.skipExistingVersions,
204
206
  logLevel: program.opts().logLevel,
205
207
  force: options.force,
206
208
  yes: options.yes,
package/dist/index.d.mts CHANGED
@@ -29,6 +29,7 @@ declare function getDefaultConfig(): {
29
29
  token: string | undefined;
30
30
  safetyCheck: true;
31
31
  safetyCheckTimeout: number;
32
+ skipExistingVersions: boolean;
32
33
  packageManager: PackageManager;
33
34
  };
34
35
  tokens: {
@@ -401,6 +402,45 @@ declare function withRegistryNpmrc<T>({ config, registryTarget, packageName, pac
401
402
  packageManager: PackageManager;
402
403
  fn: () => Promise<T>;
403
404
  }): Promise<T>;
405
+ /**
406
+ * Detect whether a failed publish is caused by the version already existing on
407
+ * the registry (the registry refusing to overwrite an immutable asset), as
408
+ * opposed to any other publish failure. This is what makes a retry safe: the
409
+ * artifact is already there, so the failure is really a "nothing to do".
410
+ *
411
+ * Registries phrase this differently, so we match the known variants:
412
+ * - npm public registry / verdaccio: `EPUBLISHCONFLICT`, `cannot publish over
413
+ * the previously published versions`, `you cannot publish over ...`;
414
+ * - Nexus: `Repository does not allow updating assets` (HTTP 400);
415
+ * - Artifactory/JFrog and others: `already exists` / `version already exists`;
416
+ * - a bare HTTP 409 Conflict from a registry that returns no message body.
417
+ */
418
+ declare function isVersionAlreadyPublishedError(error: unknown): boolean;
419
+ /**
420
+ * Ask the registry whether `pkg@version` already exists. This is the primary,
421
+ * robust idempotency check: it relies on the standard registry metadata
422
+ * endpoint that every npm-compatible registry (npm, Nexus, JFrog, Verdaccio,
423
+ * GitHub/GitLab Packages) implements to serve `install`, rather than on the
424
+ * wording of a publish error - which varies per registry.
425
+ *
426
+ * Strategy: for npm/pnpm, use the `view` command first (it inherits the full
427
+ * `.npmrc` environment - proxy, CA, scoped auth). If that is inconclusive, or
428
+ * for yarn/bun (no usable `view`), fall back to a direct HTTP request to the
429
+ * packument endpoint so every package manager is covered.
430
+ *
431
+ * Returns:
432
+ * - `true` - the version is already published;
433
+ * - `false` - the package, or that specific version, is not on the registry;
434
+ * - `null` - the check was inconclusive (auth/network/registry error), so the
435
+ * caller should fall back to attempting the publish rather than skipping.
436
+ */
437
+ declare function isVersionPublished({ pkg, version, config, packageManager, registryTarget, }: {
438
+ pkg: PackageBase;
439
+ version: string;
440
+ config: ResolvedRelizyConfig;
441
+ packageManager: PackageManager;
442
+ registryTarget: RegistryTarget;
443
+ }): Promise<boolean | null>;
404
444
  declare function getAuthCommand({ packageManager, config, otp, registryTarget, }: {
405
445
  packageManager: PackageManager;
406
446
  config: ResolvedRelizyConfig;
@@ -490,6 +530,33 @@ declare function postPrComment({ config, pr, body, }: {
490
530
  declare function redactSecrets<T>(value: T): T;
491
531
 
492
532
  declare function readPackageJson(packagePath: string): ReadPackage | undefined;
533
+ /**
534
+ * Resolve a set of path globs (relative to `cwd`) to the absolute directories
535
+ * they match. Shared by package discovery and ignore resolution.
536
+ */
537
+ declare function globDirsAbsolute(cwd: string, globs?: string[]): Set<string>;
538
+ /**
539
+ * Relative POSIX paths of every package ignored by config, resolved from BOTH
540
+ * `monorepo.ignored` (path globs) and `monorepo.ignorePackageNames` (names).
541
+ *
542
+ * The name-based source is resolved against the configured `packages` globs so
543
+ * the two options behave identically downstream (path filtering + root commit
544
+ * exclusion).
545
+ */
546
+ declare function resolveIgnoredPackagePaths(config: ResolvedRelizyConfig): string[];
547
+ /**
548
+ * Extract the changed file paths (POSIX) from a commit body. `getGitDiff` runs
549
+ * `git log --name-status`, so the body carries lines like `M\tpath/to/file`
550
+ * (renames/copies use `R100\told\tnew` - the new path is the last field).
551
+ */
552
+ declare function getCommitChangedFiles(commit: GitCommit): string[];
553
+ /**
554
+ * True when EVERY changed file of the commit lives inside an ignored package.
555
+ * Such commits are excluded from the root (unified/selective) version bump and
556
+ * changelog so a breaking change scoped to an ignored package never bumps the
557
+ * whole repository. Commits with no detectable files are kept (fail-open).
558
+ */
559
+ declare function isCommitOnlyInIgnoredPackages(commit: GitCommit, ignoredPaths: string[]): boolean;
493
560
  interface RootPackage extends ReadPackage {
494
561
  fromTag: string;
495
562
  commits: GitCommit[];
@@ -504,10 +571,11 @@ declare function getRootPackage({ config, force, from, to, suffix, changelog, dr
504
571
  changelog: boolean;
505
572
  dryRun?: boolean;
506
573
  }): Promise<RootPackage>;
507
- declare function readPackages({ cwd, patterns, ignorePackageNames, includePrivates, }: {
574
+ declare function readPackages({ cwd, patterns, ignorePackageNames, ignored, includePrivates, }: {
508
575
  cwd: string;
509
576
  patterns?: string[];
510
577
  ignorePackageNames: NonNullable<ResolvedRelizyConfig['monorepo']>['ignorePackageNames'];
578
+ ignored?: NonNullable<ResolvedRelizyConfig['monorepo']>['ignored'];
511
579
  includePrivates?: boolean;
512
580
  }): ReadPackage[];
513
581
  declare function getPackages({ config, suffix, force, includeAll, dryRun, }: {
@@ -721,14 +789,15 @@ type SemverChangeType = 'major' | 'minor' | 'patch' | undefined;
721
789
  * through this function — only commit-based detection is capped.
722
790
  */
723
791
  declare function capReleaseTypeForZeroMajor(currentVersion: string, detected: SemverChangeType): SemverChangeType;
724
- declare function determineSemverChange(commits: GitCommit[], types: NonNullable<RelizyConfig['types']>, currentVersion?: string): SemverChangeType;
725
- declare function determineReleaseType({ currentVersion, commits, releaseType, preid, types, force, }: {
792
+ declare function determineSemverChange(commits: GitCommit[], types: NonNullable<RelizyConfig['types']>, currentVersion?: string, capZeroMajor?: boolean): SemverChangeType;
793
+ declare function determineReleaseType({ currentVersion, commits, releaseType, preid, types, force, capZeroMajor, }: {
726
794
  currentVersion: string;
727
795
  commits?: GitCommit[];
728
796
  releaseType: ReleaseType;
729
797
  preid: string | undefined;
730
798
  types: ResolvedRelizyConfig['types'];
731
799
  force: boolean;
800
+ capZeroMajor?: boolean;
732
801
  }): ReleaseType | undefined;
733
802
  declare function writeVersion(pkgPath: string, newVersion: string, dryRun?: boolean): void;
734
803
  declare function getPackageNewVersion({ name, currentVersion, releaseType, preid, suffix, }: {
@@ -1011,9 +1080,26 @@ interface MonorepoConfig {
1011
1080
  packages: string[];
1012
1081
  /**
1013
1082
  * Package names to ignore.
1083
+ * @deprecated Use `ignored` (path globs) instead, for consistency with
1084
+ * `packages`. Both options are still honored and merged together.
1014
1085
  * @default []
1015
1086
  */
1016
1087
  ignorePackageNames?: string[];
1088
+ /**
1089
+ * Glob patterns (relative to `cwd`, POSIX separators) of package directories
1090
+ * to ignore - matched the same way as `packages`. Ignored packages are
1091
+ * excluded from bump, changelog, publish, provider-release and pr-comment.
1092
+ *
1093
+ * In `unified`/`selective` mode, commits whose changed files all live inside
1094
+ * ignored packages are also excluded from the root version bump and the root
1095
+ * changelog (a breaking change in an ignored package no longer bumps the
1096
+ * whole repository).
1097
+ *
1098
+ * Prefer this over `ignorePackageNames` for path-based configuration
1099
+ * consistent with `packages`. Both options are honored and merged together.
1100
+ * @default []
1101
+ */
1102
+ ignored?: string[];
1017
1103
  /**
1018
1104
  * Include private packages (with `"private": true` in package.json) in
1019
1105
  * bump and changelog operations. Private packages remain excluded from
@@ -1057,6 +1143,18 @@ interface BumpConfig {
1057
1143
  * @default true
1058
1144
  */
1059
1145
  yes?: boolean;
1146
+ /**
1147
+ * Cap commit-detected `major` bumps to `minor` while the current version is
1148
+ * in the `0.x.y` range (semver §4, initial development), so a breaking change
1149
+ * does not graduate a `0.x` package to `1.0.0` automatically.
1150
+ *
1151
+ * Set to `false` to opt out: a breaking commit then bumps `0.x.y` straight to
1152
+ * `1.0.0`. This only affects **commit-based** detection; explicit CLI release
1153
+ * types (`--major`, `--premajor`, …) are never capped. Once the package
1154
+ * reaches `1.x`, this option is a no-op.
1155
+ * @default true
1156
+ */
1157
+ capZeroMajor?: boolean;
1060
1158
  }
1061
1159
  interface BumpOptions extends BumpConfig {
1062
1160
  /**
@@ -1367,6 +1465,17 @@ type PublishConfig = ChangelogConfig$1['publish'] & {
1367
1465
  * single-registry behavior.
1368
1466
  */
1369
1467
  registries?: RegistryTarget[];
1468
+ /**
1469
+ * Skip a package (per registry) instead of failing when its version already
1470
+ * exists on the registry. Makes re-running a release idempotent: a retried
1471
+ * job whose version is already published (e.g. a canary rerun on the same
1472
+ * commit, or a partially completed publish) succeeds and only publishes what
1473
+ * is missing, rather than crashing on the registry's "cannot overwrite"
1474
+ * error. When disabled (the default), that situation still fails, but with a
1475
+ * clear error message instead of the raw registry error.
1476
+ * @default false
1477
+ */
1478
+ skipExistingVersions?: boolean;
1370
1479
  };
1371
1480
  interface PublishOptions extends PublishConfig {
1372
1481
  /**
@@ -2104,6 +2213,19 @@ interface CommentBodyParams {
2104
2213
  }
2105
2214
  declare function buildCommentBody(params: CommentBodyParams): string;
2106
2215
  declare function prComment(options?: PrCommentOptions): Promise<boolean>;
2216
+ /**
2217
+ * Best-effort wrapper around {@link prComment} for the release flow: honours the
2218
+ * `--no-pr-comment` gate and swallows errors so a failed PR comment never aborts
2219
+ * an otherwise successful release.
2220
+ */
2221
+ declare function tryPostPrComment({ config, releaseContext, prNumber, dryRun, logLevel, configName, }: {
2222
+ config: ResolvedRelizyConfig;
2223
+ releaseContext: ReleaseContext;
2224
+ prNumber?: number;
2225
+ dryRun: boolean;
2226
+ logLevel?: string;
2227
+ configName?: string;
2228
+ }): Promise<boolean>;
2107
2229
 
2108
2230
  declare function providerReleaseSafetyCheck({ config, provider }: {
2109
2231
  config: ResolvedRelizyConfig;
@@ -2119,6 +2241,22 @@ declare function publish(options?: Partial<PublishOptions>): Promise<{
2119
2241
  publishedPackages: PackageBase[];
2120
2242
  } | undefined>;
2121
2243
 
2244
+ /**
2245
+ * Build the final "Release workflow completed!" summary box from the outcome of
2246
+ * each release step. Pure formatting - no side effects - so it stays easy to read
2247
+ * and to test in isolation, and keeps the `release()` orchestration lean.
2248
+ */
2249
+ declare function buildReleaseSummary({ config, bumpResult, publishResponse, createdTags, provider, postedReleases, providerError, socialResults, prCommentPosted, }: {
2250
+ config: ResolvedRelizyConfig;
2251
+ bumpResult: BumpResultTruthy;
2252
+ publishResponse?: PublishResponse;
2253
+ createdTags: string[];
2254
+ provider?: GitProvider;
2255
+ postedReleases: PostedRelease[];
2256
+ providerError?: string;
2257
+ socialResults?: SocialResult;
2258
+ prCommentPosted: boolean;
2259
+ }): string;
2122
2260
  declare function release(options?: Partial<ReleaseOptions>): Promise<void>;
2123
2261
 
2124
2262
  declare function socialSafetyCheck({ config }: {
@@ -2126,5 +2264,5 @@ declare function socialSafetyCheck({ config }: {
2126
2264
  }): Promise<void>;
2127
2265
  declare function social(options?: Partial<SocialOptions>): Promise<SocialResult>;
2128
2266
 
2129
- export { NEW_PACKAGE_MARKER, PR_COMMENT_MARKER, buildChangelogBody, buildCommentBody, buildCompareLink, buildContributors, bump, capReleaseTypeForZeroMajor, changelog, checkGitStatusIfDirty, collectContributorNames, collectPackageBumps, confirmBump, createCommitAndTags, createGitlabRelease, defineConfig, detectGitProvider, detectPackageManager, detectPullRequest, determinePublishTag, determineReleaseType, determineSemverChange, executeBuildCmd, executeFormatCmd, executeHook, expandPackagesToBumpWithDependents, extractChangelogSummary, extractVersionFromPackageTag, extractVersionFromTag, fetchGitTags, filterOutPrivatePackages, findGitHubPR, findGitLabMR, findReachableCommitBySubject, formatChangelogForSlack, formatPackagesForSlack, formatSlackMessage, formatTweetMessage, generateChangelog, generateMarkDown, getAuthCommand, getBumpedIndependentPackages, getBumpedPackageIndependently, getCIName, getCanaryVersion, getCommitSubject, getCurrentGitBranch, getCurrentGitRef, getDefaultConfig, getDependentsOf, getFirstCommit, getGitStatus, getIndependentTag, getLastPackageTag, getLastRepoTag, getLastStableTag, getLastTag, getModifiedReleaseFilePatterns, getNpmRegistry, getPackageCommits, getPackageDependencies, getPackageNewVersion, getPackages, getPackagesOrBumpedPackages, getPackagesToPublishInIndependentMode, getPackagesToPublishInSelectiveMode, getPreid, getReleaseUrl, getRootPackage, getShortCommitSha, getSlackToken, getSlackWebhookUrl, getTwitterCredentials, github, gitlab, hasLernaJson, isAncestor, isBumpedPackage, isChangedPreid, isGraduating, isGraduatingToStableBetweenVersion, isInCI, isPrerelease, isPrereleaseReleaseType, isStableReleaseType, isTagVersionCompatibleWithCurrent, loadRelizyConfig, mergeTypes, parseChangelogMarkdown, parseGitRemoteUrl, postPrComment, postReleaseToSlack, postReleaseToTwitter, prComment, providerRelease, providerReleaseSafetyCheck, publish, publishPackage, publishSafetyCheck, pushCommitAndTags, pushTagForce, readPackageJson, readPackages, reconcileFromTag, redactSecrets, release, resetRewrittenTagCache, resolveAllConfiguredRegistryTargets, resolveRegistryTargetsForPackage, resolveRegistryTargetsForPackages, resolveTags, retagAnnotatedLocal, rollbackModifiedFiles, shouldFilterPrereleaseTags, social, socialSafetyCheck, tagExists, topologicalSort, updateLernaVersion, withRegistryNpmrc, writeChangelogToFile, writeVersion };
2267
+ export { NEW_PACKAGE_MARKER, PR_COMMENT_MARKER, buildChangelogBody, buildCommentBody, buildCompareLink, buildContributors, buildReleaseSummary, bump, capReleaseTypeForZeroMajor, changelog, checkGitStatusIfDirty, collectContributorNames, collectPackageBumps, confirmBump, createCommitAndTags, createGitlabRelease, defineConfig, detectGitProvider, detectPackageManager, detectPullRequest, determinePublishTag, determineReleaseType, determineSemverChange, executeBuildCmd, executeFormatCmd, executeHook, expandPackagesToBumpWithDependents, extractChangelogSummary, extractVersionFromPackageTag, extractVersionFromTag, fetchGitTags, filterOutPrivatePackages, findGitHubPR, findGitLabMR, findReachableCommitBySubject, formatChangelogForSlack, formatPackagesForSlack, formatSlackMessage, formatTweetMessage, generateChangelog, generateMarkDown, getAuthCommand, getBumpedIndependentPackages, getBumpedPackageIndependently, getCIName, getCanaryVersion, getCommitChangedFiles, getCommitSubject, getCurrentGitBranch, getCurrentGitRef, getDefaultConfig, getDependentsOf, getFirstCommit, getGitStatus, getIndependentTag, getLastPackageTag, getLastRepoTag, getLastStableTag, getLastTag, getModifiedReleaseFilePatterns, getNpmRegistry, getPackageCommits, getPackageDependencies, getPackageNewVersion, getPackages, getPackagesOrBumpedPackages, getPackagesToPublishInIndependentMode, getPackagesToPublishInSelectiveMode, getPreid, getReleaseUrl, getRootPackage, getShortCommitSha, getSlackToken, getSlackWebhookUrl, getTwitterCredentials, github, gitlab, globDirsAbsolute, hasLernaJson, isAncestor, isBumpedPackage, isChangedPreid, isCommitOnlyInIgnoredPackages, isGraduating, isGraduatingToStableBetweenVersion, isInCI, isPrerelease, isPrereleaseReleaseType, isStableReleaseType, isTagVersionCompatibleWithCurrent, isVersionAlreadyPublishedError, isVersionPublished, loadRelizyConfig, mergeTypes, parseChangelogMarkdown, parseGitRemoteUrl, postPrComment, postReleaseToSlack, postReleaseToTwitter, prComment, providerRelease, providerReleaseSafetyCheck, publish, publishPackage, publishSafetyCheck, pushCommitAndTags, pushTagForce, readPackageJson, readPackages, reconcileFromTag, redactSecrets, release, resetRewrittenTagCache, resolveAllConfiguredRegistryTargets, resolveIgnoredPackagePaths, resolveRegistryTargetsForPackage, resolveRegistryTargetsForPackages, resolveTags, retagAnnotatedLocal, rollbackModifiedFiles, shouldFilterPrereleaseTags, social, socialSafetyCheck, tagExists, topologicalSort, tryPostPrComment, updateLernaVersion, withRegistryNpmrc, writeChangelogToFile, writeVersion };
2130
2268
  export type { AIConfig, AIPromptTarget, AIProviderName, AISocialConfig, AISystemPromptOverrides, AITargetConfig, BumpConfig, BumpOptions, BumpResult, BumpResultFalsy, BumpResultTruthy, ChangelogConfig, ChangelogInclude, ChangelogOptions, ClaudeCodeProviderOptions, ConfigType, GitProvider, GitlabRelease, GitlabReleaseResponse, HookConfig, HookStep, HookType, MonorepoConfig, OnRewrittenTag, PackageBase, PackageBumpEntry, PackageManager, PostedRelease, PrCommentConfig, PrCommentMode, PrCommentOptions, PrCommentStatus, ProviderReleaseOptions, ProviderReleaseResult, PublishConfig, PublishOptions, PublishResponse, PullRequestInfo, ReadPackage, Reference, RegistryTarget, ReleaseConfig, ReleaseContext, ReleaseOptions, RelizyConfig, RepoConfig, ResolvedConfig, ResolvedRelizyConfig, ResolvedTags, ResolvedTwitterCredentials, RootPackage, SlackCredentials, SlackOptions, SlackPackageEntry, SlackSocialConfig, SocialConfig, SocialNetworkResult, SocialOptions, SocialResult, Step, TemplatesConfig, TokensConfig, TwitterCredentials, TwitterOptions, TwitterSocialConfig, VersionMode };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ declare function getDefaultConfig(): {
29
29
  token: string | undefined;
30
30
  safetyCheck: true;
31
31
  safetyCheckTimeout: number;
32
+ skipExistingVersions: boolean;
32
33
  packageManager: PackageManager;
33
34
  };
34
35
  tokens: {
@@ -401,6 +402,45 @@ declare function withRegistryNpmrc<T>({ config, registryTarget, packageName, pac
401
402
  packageManager: PackageManager;
402
403
  fn: () => Promise<T>;
403
404
  }): Promise<T>;
405
+ /**
406
+ * Detect whether a failed publish is caused by the version already existing on
407
+ * the registry (the registry refusing to overwrite an immutable asset), as
408
+ * opposed to any other publish failure. This is what makes a retry safe: the
409
+ * artifact is already there, so the failure is really a "nothing to do".
410
+ *
411
+ * Registries phrase this differently, so we match the known variants:
412
+ * - npm public registry / verdaccio: `EPUBLISHCONFLICT`, `cannot publish over
413
+ * the previously published versions`, `you cannot publish over ...`;
414
+ * - Nexus: `Repository does not allow updating assets` (HTTP 400);
415
+ * - Artifactory/JFrog and others: `already exists` / `version already exists`;
416
+ * - a bare HTTP 409 Conflict from a registry that returns no message body.
417
+ */
418
+ declare function isVersionAlreadyPublishedError(error: unknown): boolean;
419
+ /**
420
+ * Ask the registry whether `pkg@version` already exists. This is the primary,
421
+ * robust idempotency check: it relies on the standard registry metadata
422
+ * endpoint that every npm-compatible registry (npm, Nexus, JFrog, Verdaccio,
423
+ * GitHub/GitLab Packages) implements to serve `install`, rather than on the
424
+ * wording of a publish error - which varies per registry.
425
+ *
426
+ * Strategy: for npm/pnpm, use the `view` command first (it inherits the full
427
+ * `.npmrc` environment - proxy, CA, scoped auth). If that is inconclusive, or
428
+ * for yarn/bun (no usable `view`), fall back to a direct HTTP request to the
429
+ * packument endpoint so every package manager is covered.
430
+ *
431
+ * Returns:
432
+ * - `true` - the version is already published;
433
+ * - `false` - the package, or that specific version, is not on the registry;
434
+ * - `null` - the check was inconclusive (auth/network/registry error), so the
435
+ * caller should fall back to attempting the publish rather than skipping.
436
+ */
437
+ declare function isVersionPublished({ pkg, version, config, packageManager, registryTarget, }: {
438
+ pkg: PackageBase;
439
+ version: string;
440
+ config: ResolvedRelizyConfig;
441
+ packageManager: PackageManager;
442
+ registryTarget: RegistryTarget;
443
+ }): Promise<boolean | null>;
404
444
  declare function getAuthCommand({ packageManager, config, otp, registryTarget, }: {
405
445
  packageManager: PackageManager;
406
446
  config: ResolvedRelizyConfig;
@@ -490,6 +530,33 @@ declare function postPrComment({ config, pr, body, }: {
490
530
  declare function redactSecrets<T>(value: T): T;
491
531
 
492
532
  declare function readPackageJson(packagePath: string): ReadPackage | undefined;
533
+ /**
534
+ * Resolve a set of path globs (relative to `cwd`) to the absolute directories
535
+ * they match. Shared by package discovery and ignore resolution.
536
+ */
537
+ declare function globDirsAbsolute(cwd: string, globs?: string[]): Set<string>;
538
+ /**
539
+ * Relative POSIX paths of every package ignored by config, resolved from BOTH
540
+ * `monorepo.ignored` (path globs) and `monorepo.ignorePackageNames` (names).
541
+ *
542
+ * The name-based source is resolved against the configured `packages` globs so
543
+ * the two options behave identically downstream (path filtering + root commit
544
+ * exclusion).
545
+ */
546
+ declare function resolveIgnoredPackagePaths(config: ResolvedRelizyConfig): string[];
547
+ /**
548
+ * Extract the changed file paths (POSIX) from a commit body. `getGitDiff` runs
549
+ * `git log --name-status`, so the body carries lines like `M\tpath/to/file`
550
+ * (renames/copies use `R100\told\tnew` - the new path is the last field).
551
+ */
552
+ declare function getCommitChangedFiles(commit: GitCommit): string[];
553
+ /**
554
+ * True when EVERY changed file of the commit lives inside an ignored package.
555
+ * Such commits are excluded from the root (unified/selective) version bump and
556
+ * changelog so a breaking change scoped to an ignored package never bumps the
557
+ * whole repository. Commits with no detectable files are kept (fail-open).
558
+ */
559
+ declare function isCommitOnlyInIgnoredPackages(commit: GitCommit, ignoredPaths: string[]): boolean;
493
560
  interface RootPackage extends ReadPackage {
494
561
  fromTag: string;
495
562
  commits: GitCommit[];
@@ -504,10 +571,11 @@ declare function getRootPackage({ config, force, from, to, suffix, changelog, dr
504
571
  changelog: boolean;
505
572
  dryRun?: boolean;
506
573
  }): Promise<RootPackage>;
507
- declare function readPackages({ cwd, patterns, ignorePackageNames, includePrivates, }: {
574
+ declare function readPackages({ cwd, patterns, ignorePackageNames, ignored, includePrivates, }: {
508
575
  cwd: string;
509
576
  patterns?: string[];
510
577
  ignorePackageNames: NonNullable<ResolvedRelizyConfig['monorepo']>['ignorePackageNames'];
578
+ ignored?: NonNullable<ResolvedRelizyConfig['monorepo']>['ignored'];
511
579
  includePrivates?: boolean;
512
580
  }): ReadPackage[];
513
581
  declare function getPackages({ config, suffix, force, includeAll, dryRun, }: {
@@ -721,14 +789,15 @@ type SemverChangeType = 'major' | 'minor' | 'patch' | undefined;
721
789
  * through this function — only commit-based detection is capped.
722
790
  */
723
791
  declare function capReleaseTypeForZeroMajor(currentVersion: string, detected: SemverChangeType): SemverChangeType;
724
- declare function determineSemverChange(commits: GitCommit[], types: NonNullable<RelizyConfig['types']>, currentVersion?: string): SemverChangeType;
725
- declare function determineReleaseType({ currentVersion, commits, releaseType, preid, types, force, }: {
792
+ declare function determineSemverChange(commits: GitCommit[], types: NonNullable<RelizyConfig['types']>, currentVersion?: string, capZeroMajor?: boolean): SemverChangeType;
793
+ declare function determineReleaseType({ currentVersion, commits, releaseType, preid, types, force, capZeroMajor, }: {
726
794
  currentVersion: string;
727
795
  commits?: GitCommit[];
728
796
  releaseType: ReleaseType;
729
797
  preid: string | undefined;
730
798
  types: ResolvedRelizyConfig['types'];
731
799
  force: boolean;
800
+ capZeroMajor?: boolean;
732
801
  }): ReleaseType | undefined;
733
802
  declare function writeVersion(pkgPath: string, newVersion: string, dryRun?: boolean): void;
734
803
  declare function getPackageNewVersion({ name, currentVersion, releaseType, preid, suffix, }: {
@@ -1011,9 +1080,26 @@ interface MonorepoConfig {
1011
1080
  packages: string[];
1012
1081
  /**
1013
1082
  * Package names to ignore.
1083
+ * @deprecated Use `ignored` (path globs) instead, for consistency with
1084
+ * `packages`. Both options are still honored and merged together.
1014
1085
  * @default []
1015
1086
  */
1016
1087
  ignorePackageNames?: string[];
1088
+ /**
1089
+ * Glob patterns (relative to `cwd`, POSIX separators) of package directories
1090
+ * to ignore - matched the same way as `packages`. Ignored packages are
1091
+ * excluded from bump, changelog, publish, provider-release and pr-comment.
1092
+ *
1093
+ * In `unified`/`selective` mode, commits whose changed files all live inside
1094
+ * ignored packages are also excluded from the root version bump and the root
1095
+ * changelog (a breaking change in an ignored package no longer bumps the
1096
+ * whole repository).
1097
+ *
1098
+ * Prefer this over `ignorePackageNames` for path-based configuration
1099
+ * consistent with `packages`. Both options are honored and merged together.
1100
+ * @default []
1101
+ */
1102
+ ignored?: string[];
1017
1103
  /**
1018
1104
  * Include private packages (with `"private": true` in package.json) in
1019
1105
  * bump and changelog operations. Private packages remain excluded from
@@ -1057,6 +1143,18 @@ interface BumpConfig {
1057
1143
  * @default true
1058
1144
  */
1059
1145
  yes?: boolean;
1146
+ /**
1147
+ * Cap commit-detected `major` bumps to `minor` while the current version is
1148
+ * in the `0.x.y` range (semver §4, initial development), so a breaking change
1149
+ * does not graduate a `0.x` package to `1.0.0` automatically.
1150
+ *
1151
+ * Set to `false` to opt out: a breaking commit then bumps `0.x.y` straight to
1152
+ * `1.0.0`. This only affects **commit-based** detection; explicit CLI release
1153
+ * types (`--major`, `--premajor`, …) are never capped. Once the package
1154
+ * reaches `1.x`, this option is a no-op.
1155
+ * @default true
1156
+ */
1157
+ capZeroMajor?: boolean;
1060
1158
  }
1061
1159
  interface BumpOptions extends BumpConfig {
1062
1160
  /**
@@ -1367,6 +1465,17 @@ type PublishConfig = ChangelogConfig$1['publish'] & {
1367
1465
  * single-registry behavior.
1368
1466
  */
1369
1467
  registries?: RegistryTarget[];
1468
+ /**
1469
+ * Skip a package (per registry) instead of failing when its version already
1470
+ * exists on the registry. Makes re-running a release idempotent: a retried
1471
+ * job whose version is already published (e.g. a canary rerun on the same
1472
+ * commit, or a partially completed publish) succeeds and only publishes what
1473
+ * is missing, rather than crashing on the registry's "cannot overwrite"
1474
+ * error. When disabled (the default), that situation still fails, but with a
1475
+ * clear error message instead of the raw registry error.
1476
+ * @default false
1477
+ */
1478
+ skipExistingVersions?: boolean;
1370
1479
  };
1371
1480
  interface PublishOptions extends PublishConfig {
1372
1481
  /**
@@ -2104,6 +2213,19 @@ interface CommentBodyParams {
2104
2213
  }
2105
2214
  declare function buildCommentBody(params: CommentBodyParams): string;
2106
2215
  declare function prComment(options?: PrCommentOptions): Promise<boolean>;
2216
+ /**
2217
+ * Best-effort wrapper around {@link prComment} for the release flow: honours the
2218
+ * `--no-pr-comment` gate and swallows errors so a failed PR comment never aborts
2219
+ * an otherwise successful release.
2220
+ */
2221
+ declare function tryPostPrComment({ config, releaseContext, prNumber, dryRun, logLevel, configName, }: {
2222
+ config: ResolvedRelizyConfig;
2223
+ releaseContext: ReleaseContext;
2224
+ prNumber?: number;
2225
+ dryRun: boolean;
2226
+ logLevel?: string;
2227
+ configName?: string;
2228
+ }): Promise<boolean>;
2107
2229
 
2108
2230
  declare function providerReleaseSafetyCheck({ config, provider }: {
2109
2231
  config: ResolvedRelizyConfig;
@@ -2119,6 +2241,22 @@ declare function publish(options?: Partial<PublishOptions>): Promise<{
2119
2241
  publishedPackages: PackageBase[];
2120
2242
  } | undefined>;
2121
2243
 
2244
+ /**
2245
+ * Build the final "Release workflow completed!" summary box from the outcome of
2246
+ * each release step. Pure formatting - no side effects - so it stays easy to read
2247
+ * and to test in isolation, and keeps the `release()` orchestration lean.
2248
+ */
2249
+ declare function buildReleaseSummary({ config, bumpResult, publishResponse, createdTags, provider, postedReleases, providerError, socialResults, prCommentPosted, }: {
2250
+ config: ResolvedRelizyConfig;
2251
+ bumpResult: BumpResultTruthy;
2252
+ publishResponse?: PublishResponse;
2253
+ createdTags: string[];
2254
+ provider?: GitProvider;
2255
+ postedReleases: PostedRelease[];
2256
+ providerError?: string;
2257
+ socialResults?: SocialResult;
2258
+ prCommentPosted: boolean;
2259
+ }): string;
2122
2260
  declare function release(options?: Partial<ReleaseOptions>): Promise<void>;
2123
2261
 
2124
2262
  declare function socialSafetyCheck({ config }: {
@@ -2126,5 +2264,5 @@ declare function socialSafetyCheck({ config }: {
2126
2264
  }): Promise<void>;
2127
2265
  declare function social(options?: Partial<SocialOptions>): Promise<SocialResult>;
2128
2266
 
2129
- export { NEW_PACKAGE_MARKER, PR_COMMENT_MARKER, buildChangelogBody, buildCommentBody, buildCompareLink, buildContributors, bump, capReleaseTypeForZeroMajor, changelog, checkGitStatusIfDirty, collectContributorNames, collectPackageBumps, confirmBump, createCommitAndTags, createGitlabRelease, defineConfig, detectGitProvider, detectPackageManager, detectPullRequest, determinePublishTag, determineReleaseType, determineSemverChange, executeBuildCmd, executeFormatCmd, executeHook, expandPackagesToBumpWithDependents, extractChangelogSummary, extractVersionFromPackageTag, extractVersionFromTag, fetchGitTags, filterOutPrivatePackages, findGitHubPR, findGitLabMR, findReachableCommitBySubject, formatChangelogForSlack, formatPackagesForSlack, formatSlackMessage, formatTweetMessage, generateChangelog, generateMarkDown, getAuthCommand, getBumpedIndependentPackages, getBumpedPackageIndependently, getCIName, getCanaryVersion, getCommitSubject, getCurrentGitBranch, getCurrentGitRef, getDefaultConfig, getDependentsOf, getFirstCommit, getGitStatus, getIndependentTag, getLastPackageTag, getLastRepoTag, getLastStableTag, getLastTag, getModifiedReleaseFilePatterns, getNpmRegistry, getPackageCommits, getPackageDependencies, getPackageNewVersion, getPackages, getPackagesOrBumpedPackages, getPackagesToPublishInIndependentMode, getPackagesToPublishInSelectiveMode, getPreid, getReleaseUrl, getRootPackage, getShortCommitSha, getSlackToken, getSlackWebhookUrl, getTwitterCredentials, github, gitlab, hasLernaJson, isAncestor, isBumpedPackage, isChangedPreid, isGraduating, isGraduatingToStableBetweenVersion, isInCI, isPrerelease, isPrereleaseReleaseType, isStableReleaseType, isTagVersionCompatibleWithCurrent, loadRelizyConfig, mergeTypes, parseChangelogMarkdown, parseGitRemoteUrl, postPrComment, postReleaseToSlack, postReleaseToTwitter, prComment, providerRelease, providerReleaseSafetyCheck, publish, publishPackage, publishSafetyCheck, pushCommitAndTags, pushTagForce, readPackageJson, readPackages, reconcileFromTag, redactSecrets, release, resetRewrittenTagCache, resolveAllConfiguredRegistryTargets, resolveRegistryTargetsForPackage, resolveRegistryTargetsForPackages, resolveTags, retagAnnotatedLocal, rollbackModifiedFiles, shouldFilterPrereleaseTags, social, socialSafetyCheck, tagExists, topologicalSort, updateLernaVersion, withRegistryNpmrc, writeChangelogToFile, writeVersion };
2267
+ export { NEW_PACKAGE_MARKER, PR_COMMENT_MARKER, buildChangelogBody, buildCommentBody, buildCompareLink, buildContributors, buildReleaseSummary, bump, capReleaseTypeForZeroMajor, changelog, checkGitStatusIfDirty, collectContributorNames, collectPackageBumps, confirmBump, createCommitAndTags, createGitlabRelease, defineConfig, detectGitProvider, detectPackageManager, detectPullRequest, determinePublishTag, determineReleaseType, determineSemverChange, executeBuildCmd, executeFormatCmd, executeHook, expandPackagesToBumpWithDependents, extractChangelogSummary, extractVersionFromPackageTag, extractVersionFromTag, fetchGitTags, filterOutPrivatePackages, findGitHubPR, findGitLabMR, findReachableCommitBySubject, formatChangelogForSlack, formatPackagesForSlack, formatSlackMessage, formatTweetMessage, generateChangelog, generateMarkDown, getAuthCommand, getBumpedIndependentPackages, getBumpedPackageIndependently, getCIName, getCanaryVersion, getCommitChangedFiles, getCommitSubject, getCurrentGitBranch, getCurrentGitRef, getDefaultConfig, getDependentsOf, getFirstCommit, getGitStatus, getIndependentTag, getLastPackageTag, getLastRepoTag, getLastStableTag, getLastTag, getModifiedReleaseFilePatterns, getNpmRegistry, getPackageCommits, getPackageDependencies, getPackageNewVersion, getPackages, getPackagesOrBumpedPackages, getPackagesToPublishInIndependentMode, getPackagesToPublishInSelectiveMode, getPreid, getReleaseUrl, getRootPackage, getShortCommitSha, getSlackToken, getSlackWebhookUrl, getTwitterCredentials, github, gitlab, globDirsAbsolute, hasLernaJson, isAncestor, isBumpedPackage, isChangedPreid, isCommitOnlyInIgnoredPackages, isGraduating, isGraduatingToStableBetweenVersion, isInCI, isPrerelease, isPrereleaseReleaseType, isStableReleaseType, isTagVersionCompatibleWithCurrent, isVersionAlreadyPublishedError, isVersionPublished, loadRelizyConfig, mergeTypes, parseChangelogMarkdown, parseGitRemoteUrl, postPrComment, postReleaseToSlack, postReleaseToTwitter, prComment, providerRelease, providerReleaseSafetyCheck, publish, publishPackage, publishSafetyCheck, pushCommitAndTags, pushTagForce, readPackageJson, readPackages, reconcileFromTag, redactSecrets, release, resetRewrittenTagCache, resolveAllConfiguredRegistryTargets, resolveIgnoredPackagePaths, resolveRegistryTargetsForPackage, resolveRegistryTargetsForPackages, resolveTags, retagAnnotatedLocal, rollbackModifiedFiles, shouldFilterPrereleaseTags, social, socialSafetyCheck, tagExists, topologicalSort, tryPostPrComment, updateLernaVersion, withRegistryNpmrc, writeChangelogToFile, writeVersion };
2130
2268
  export type { AIConfig, AIPromptTarget, AIProviderName, AISocialConfig, AISystemPromptOverrides, AITargetConfig, BumpConfig, BumpOptions, BumpResult, BumpResultFalsy, BumpResultTruthy, ChangelogConfig, ChangelogInclude, ChangelogOptions, ClaudeCodeProviderOptions, ConfigType, GitProvider, GitlabRelease, GitlabReleaseResponse, HookConfig, HookStep, HookType, MonorepoConfig, OnRewrittenTag, PackageBase, PackageBumpEntry, PackageManager, PostedRelease, PrCommentConfig, PrCommentMode, PrCommentOptions, PrCommentStatus, ProviderReleaseOptions, ProviderReleaseResult, PublishConfig, PublishOptions, PublishResponse, PullRequestInfo, ReadPackage, Reference, RegistryTarget, ReleaseConfig, ReleaseContext, ReleaseOptions, RelizyConfig, RepoConfig, ResolvedConfig, ResolvedRelizyConfig, ResolvedTags, ResolvedTwitterCredentials, RootPackage, SlackCredentials, SlackOptions, SlackPackageEntry, SlackSocialConfig, SocialConfig, SocialNetworkResult, SocialOptions, SocialResult, Step, TemplatesConfig, TokensConfig, TwitterCredentials, TwitterOptions, TwitterSocialConfig, VersionMode };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { N as NEW_PACKAGE_MARKER, P as PR_COMMENT_MARKER, b as buildChangelogBody, a as buildCommentBody, c as buildCompareLink, d as buildContributors, e as bump, f as capReleaseTypeForZeroMajor, g as changelog, h as checkGitStatusIfDirty, i as collectContributorNames, j as collectPackageBumps, k as confirmBump, l as createCommitAndTags, m as createGitlabRelease, n as defineConfig, o as detectGitProvider, p as detectPackageManager, q as detectPullRequest, r as determinePublishTag, s as determineReleaseType, t as determineSemverChange, u as executeBuildCmd, v as executeFormatCmd, w as executeHook, x as expandPackagesToBumpWithDependents, y as extractChangelogSummary, z as extractVersionFromPackageTag, A as extractVersionFromTag, B as fetchGitTags, C as filterOutPrivatePackages, D as findGitHubPR, E as findGitLabMR, F as findReachableCommitBySubject, G as formatChangelogForSlack, H as formatPackagesForSlack, I as formatSlackMessage, J as formatTweetMessage, K as generateChangelog, L as generateMarkDown, M as getAuthCommand, O as getBumpedIndependentPackages, Q as getBumpedPackageIndependently, R as getCIName, S as getCanaryVersion, T as getCommitSubject, U as getCurrentGitBranch, V as getCurrentGitRef, W as getDefaultConfig, X as getDependentsOf, Y as getFirstCommit, Z as getGitStatus, _ as getIndependentTag, $ as getLastPackageTag, a0 as getLastRepoTag, a1 as getLastStableTag, a2 as getLastTag, a3 as getModifiedReleaseFilePatterns, a4 as getNpmRegistry, a5 as getPackageCommits, a6 as getPackageDependencies, a7 as getPackageNewVersion, a8 as getPackages, a9 as getPackagesOrBumpedPackages, aa as getPackagesToPublishInIndependentMode, ab as getPackagesToPublishInSelectiveMode, ac as getPreid, ad as getReleaseUrl, ae as getRootPackage, af as getShortCommitSha, ag as getSlackToken, ah as getSlackWebhookUrl, ai as getTwitterCredentials, aj as github, ak as gitlab, al as hasLernaJson, am as isAncestor, an as isBumpedPackage, ao as isChangedPreid, ap as isGraduating, aq as isGraduatingToStableBetweenVersion, ar as isInCI, as as isPrerelease, at as isPrereleaseReleaseType, au as isStableReleaseType, av as isTagVersionCompatibleWithCurrent, aw as loadRelizyConfig, ax as mergeTypes, ay as parseChangelogMarkdown, az as parseGitRemoteUrl, aA as postPrComment, aB as postReleaseToSlack, aC as postReleaseToTwitter, aD as prComment, aE as providerRelease, aF as providerReleaseSafetyCheck, aG as publish, aH as publishPackage, aI as publishSafetyCheck, aJ as pushCommitAndTags, aK as pushTagForce, aL as readPackageJson, aM as readPackages, aN as reconcileFromTag, aO as redactSecrets, aP as release, aQ as resetRewrittenTagCache, aR as resolveAllConfiguredRegistryTargets, aS as resolveRegistryTargetsForPackage, aT as resolveRegistryTargetsForPackages, aU as resolveTags, aV as retagAnnotatedLocal, aW as rollbackModifiedFiles, aX as shouldFilterPrereleaseTags, aY as social, aZ as socialSafetyCheck, a_ as tagExists, a$ as topologicalSort, b0 as updateLernaVersion, b1 as withRegistryNpmrc, b2 as writeChangelogToFile, b3 as writeVersion } from './shared/relizy.B0MWtDvR.mjs';
1
+ export { N as NEW_PACKAGE_MARKER, P as PR_COMMENT_MARKER, b as buildChangelogBody, a as buildCommentBody, c as buildCompareLink, d as buildContributors, e as buildReleaseSummary, f as bump, g as capReleaseTypeForZeroMajor, h as changelog, i as checkGitStatusIfDirty, j as collectContributorNames, k as collectPackageBumps, l as confirmBump, m as createCommitAndTags, n as createGitlabRelease, o as defineConfig, p as detectGitProvider, q as detectPackageManager, r as detectPullRequest, s as determinePublishTag, t as determineReleaseType, u as determineSemverChange, v as executeBuildCmd, w as executeFormatCmd, x as executeHook, y as expandPackagesToBumpWithDependents, z as extractChangelogSummary, A as extractVersionFromPackageTag, B as extractVersionFromTag, C as fetchGitTags, D as filterOutPrivatePackages, E as findGitHubPR, F as findGitLabMR, G as findReachableCommitBySubject, H as formatChangelogForSlack, I as formatPackagesForSlack, J as formatSlackMessage, K as formatTweetMessage, L as generateChangelog, M as generateMarkDown, O as getAuthCommand, Q as getBumpedIndependentPackages, R as getBumpedPackageIndependently, S as getCIName, T as getCanaryVersion, U as getCommitChangedFiles, V as getCommitSubject, W as getCurrentGitBranch, X as getCurrentGitRef, Y as getDefaultConfig, Z as getDependentsOf, _ as getFirstCommit, $ as getGitStatus, a0 as getIndependentTag, a1 as getLastPackageTag, a2 as getLastRepoTag, a3 as getLastStableTag, a4 as getLastTag, a5 as getModifiedReleaseFilePatterns, a6 as getNpmRegistry, a7 as getPackageCommits, a8 as getPackageDependencies, a9 as getPackageNewVersion, aa as getPackages, ab as getPackagesOrBumpedPackages, ac as getPackagesToPublishInIndependentMode, ad as getPackagesToPublishInSelectiveMode, ae as getPreid, af as getReleaseUrl, ag as getRootPackage, ah as getShortCommitSha, ai as getSlackToken, aj as getSlackWebhookUrl, ak as getTwitterCredentials, al as github, am as gitlab, an as globDirsAbsolute, ao as hasLernaJson, ap as isAncestor, aq as isBumpedPackage, ar as isChangedPreid, as as isCommitOnlyInIgnoredPackages, at as isGraduating, au as isGraduatingToStableBetweenVersion, av as isInCI, aw as isPrerelease, ax as isPrereleaseReleaseType, ay as isStableReleaseType, az as isTagVersionCompatibleWithCurrent, aA as isVersionAlreadyPublishedError, aB as isVersionPublished, aC as loadRelizyConfig, aD as mergeTypes, aE as parseChangelogMarkdown, aF as parseGitRemoteUrl, aG as postPrComment, aH as postReleaseToSlack, aI as postReleaseToTwitter, aJ as prComment, aK as providerRelease, aL as providerReleaseSafetyCheck, aM as publish, aN as publishPackage, aO as publishSafetyCheck, aP as pushCommitAndTags, aQ as pushTagForce, aR as readPackageJson, aS as readPackages, aT as reconcileFromTag, aU as redactSecrets, aV as release, aW as resetRewrittenTagCache, aX as resolveAllConfiguredRegistryTargets, aY as resolveIgnoredPackagePaths, aZ as resolveRegistryTargetsForPackage, a_ as resolveRegistryTargetsForPackages, a$ as resolveTags, b0 as retagAnnotatedLocal, b1 as rollbackModifiedFiles, b2 as shouldFilterPrereleaseTags, b3 as social, b4 as socialSafetyCheck, b5 as tagExists, b6 as topologicalSort, b7 as tryPostPrComment, b8 as updateLernaVersion, b9 as withRegistryNpmrc, ba as writeChangelogToFile, bb as writeVersion } from './shared/relizy.CobLJLr_.mjs';
2
2
  import '@maz-ui/node';
3
3
  import 'node:child_process';
4
4
  import 'node:process';
@@ -416,6 +416,58 @@ function readPackageJson(packagePath) {
416
416
  path: packagePath
417
417
  };
418
418
  }
419
+ const GLOB_IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
420
+ function globDirsAbsolute(cwd, globs) {
421
+ const dirs = /* @__PURE__ */ new Set();
422
+ for (const pattern of globs ?? []) {
423
+ const matches = fastGlob.sync(pattern, {
424
+ cwd,
425
+ onlyDirectories: true,
426
+ absolute: true,
427
+ ignore: GLOB_IGNORE
428
+ });
429
+ for (const match of matches) {
430
+ dirs.add(match);
431
+ }
432
+ }
433
+ return dirs;
434
+ }
435
+ function resolveIgnoredPackagePaths(config) {
436
+ const cwd = config.cwd;
437
+ const paths = /* @__PURE__ */ new Set();
438
+ for (const absolutePath of globDirsAbsolute(cwd, config.monorepo?.ignored)) {
439
+ paths.add(relative(cwd, absolutePath).split(sep).join("/"));
440
+ }
441
+ const ignorePackageNames = config.monorepo?.ignorePackageNames ?? [];
442
+ if (ignorePackageNames.length > 0) {
443
+ for (const absolutePath of globDirsAbsolute(cwd, config.monorepo?.packages)) {
444
+ const packageBase = readPackageJson(absolutePath);
445
+ if (packageBase && ignorePackageNames.includes(packageBase.name)) {
446
+ paths.add(relative(cwd, absolutePath).split(sep).join("/"));
447
+ }
448
+ }
449
+ }
450
+ return [...paths].filter(Boolean);
451
+ }
452
+ function getCommitChangedFiles(commit) {
453
+ return commit.body.split("\n").filter((line) => /^[A-Z]\d*\t/.test(line)).map((line) => {
454
+ const parts = line.split(" ");
455
+ return parts[parts.length - 1]?.trim();
456
+ }).filter((path) => Boolean(path));
457
+ }
458
+ function isUnderIgnoredPath(file, ignoredPaths) {
459
+ return ignoredPaths.some((ignored) => file === ignored || file.startsWith(`${ignored}/`));
460
+ }
461
+ function isCommitOnlyInIgnoredPackages(commit, ignoredPaths) {
462
+ if (ignoredPaths.length === 0) {
463
+ return false;
464
+ }
465
+ const files = getCommitChangedFiles(commit);
466
+ if (files.length === 0) {
467
+ return false;
468
+ }
469
+ return files.every((file) => isUnderIgnoredPath(file, ignoredPaths));
470
+ }
419
471
  async function getRootPackage({
420
472
  config,
421
473
  force,
@@ -446,7 +498,8 @@ async function getRootPackage({
446
498
  releaseType: config.bump.type,
447
499
  preid: config.bump.preid,
448
500
  types: config.types,
449
- force
501
+ force,
502
+ capZeroMajor: config.bump.capZeroMajor
450
503
  });
451
504
  if (!releaseType) {
452
505
  logger.debug("No commits require a version bump");
@@ -475,11 +528,13 @@ function readPackages({
475
528
  cwd,
476
529
  patterns,
477
530
  ignorePackageNames,
531
+ ignored,
478
532
  includePrivates
479
533
  }) {
480
534
  const packages = [];
481
535
  const foundPaths = /* @__PURE__ */ new Set();
482
536
  const patternsSet = new Set(patterns);
537
+ const ignoredDirs = globDirsAbsolute(cwd, ignored);
483
538
  if (!patterns)
484
539
  patternsSet.add(".");
485
540
  logger.debug(`Read package.json files from patterns: ${[...patternsSet].join(", ")}`);
@@ -494,6 +549,8 @@ function readPackages({
494
549
  for (const matchPath of matches) {
495
550
  if (foundPaths.has(matchPath))
496
551
  continue;
552
+ if (ignoredDirs.has(matchPath))
553
+ continue;
497
554
  const packageBase = readPackageJson(matchPath);
498
555
  if (!packageBase || packageBase.private && !includePrivates || ignorePackageNames?.includes(packageBase.name))
499
556
  continue;
@@ -522,7 +579,8 @@ function getPackageReleaseType({
522
579
  releaseType,
523
580
  preid: config.bump.preid,
524
581
  types: config.types,
525
- force
582
+ force,
583
+ capZeroMajor: config.bump.capZeroMajor
526
584
  });
527
585
  }
528
586
  if (pkg.reason === "dependency") {
@@ -538,7 +596,8 @@ function getPackageReleaseType({
538
596
  releaseType,
539
597
  preid: config.bump.preid,
540
598
  types: config.types,
541
- force
599
+ force,
600
+ capZeroMajor: config.bump.capZeroMajor
542
601
  });
543
602
  }
544
603
  async function getPackages({
@@ -553,11 +612,13 @@ async function getPackages({
553
612
  cwd: config.cwd,
554
613
  patterns,
555
614
  ignorePackageNames: config.monorepo?.ignorePackageNames,
615
+ ignored: config.monorepo?.ignored,
556
616
  includePrivates: config.monorepo?.includePrivates
557
617
  });
558
618
  const packages = /* @__PURE__ */ new Map();
559
619
  const foundPaths = /* @__PURE__ */ new Set();
560
620
  const patternsSet = new Set(patterns);
621
+ const ignoredDirs = globDirsAbsolute(config.cwd, config.monorepo?.ignored);
561
622
  if (!patterns)
562
623
  patternsSet.add(".");
563
624
  logger.debug(`Getting packages from patterns: ${patternsSet.values()}`);
@@ -571,6 +632,10 @@ async function getPackages({
571
632
  for (const matchPath of matches) {
572
633
  if (foundPaths.has(matchPath))
573
634
  continue;
635
+ if (ignoredDirs.has(matchPath)) {
636
+ logger.debug(`${matchPath} ignored by config (monorepo.ignored)`);
637
+ continue;
638
+ }
574
639
  const packageBase = readPackageJson(matchPath);
575
640
  if (!packageBase) {
576
641
  logger.debug(`Failed to read package.json at ${matchPath} - ignored`);
@@ -708,12 +773,17 @@ async function getPackageCommits({
708
773
  throw new Error("Failed to read root package.json");
709
774
  }
710
775
  const isRootPackage = pkg.path === changelogConfig.cwd || pkg.name === rootPackage.name;
776
+ const ignoredPackagePaths = isRootPackage ? resolveIgnoredPackagePaths(changelogConfig) : [];
711
777
  const commits = allCommits.filter((commit) => {
712
778
  const type = changelogConfig?.types[commit.type];
713
779
  if (!isAllowedCommit({ commit, type, changelog })) {
714
780
  return false;
715
781
  }
716
782
  if (isRootPackage) {
783
+ if (isCommitOnlyInIgnoredPackages(commit, ignoredPackagePaths)) {
784
+ logger.debug(`Commit "${commit.message}" only touches ignored packages, excluded from root`);
785
+ return false;
786
+ }
717
787
  return true;
718
788
  }
719
789
  const packageRelativePath = relative(changelogConfig.cwd, pkg.path).split(sep).join("/");
@@ -749,7 +819,7 @@ function capReleaseTypeForZeroMajor(currentVersion, detected) {
749
819
  }
750
820
  return detected;
751
821
  }
752
- function determineSemverChange(commits, types, currentVersion) {
822
+ function determineSemverChange(commits, types, currentVersion, capZeroMajor = true) {
753
823
  let [hasMajor, hasMinor, hasPatch] = [false, false, false];
754
824
  for (const commit of commits) {
755
825
  const commitType = types[commit.type];
@@ -766,12 +836,12 @@ function determineSemverChange(commits, types, currentVersion) {
766
836
  }
767
837
  }
768
838
  const detected = hasMajor ? "major" : hasMinor ? "minor" : hasPatch ? "patch" : void 0;
769
- if (currentVersion)
839
+ if (currentVersion && capZeroMajor)
770
840
  return capReleaseTypeForZeroMajor(currentVersion, detected);
771
841
  return detected;
772
842
  }
773
- function detectReleaseTypeFromCommits(commits, types, currentVersion) {
774
- return determineSemverChange(commits, types, currentVersion);
843
+ function detectReleaseTypeFromCommits(commits, types, currentVersion, capZeroMajor = true) {
844
+ return determineSemverChange(commits, types, currentVersion, capZeroMajor);
775
845
  }
776
846
  function validatePrereleaseDowngrade(currentVersion, targetPreid, configuredType) {
777
847
  if (configuredType !== "prerelease" || !targetPreid || !isPrerelease(currentVersion)) {
@@ -783,12 +853,12 @@ function validatePrereleaseDowngrade(currentVersion, targetPreid, configuredType
783
853
  throw new Error(`Unable to graduate from ${currentVersion} to ${testVersion}, it's not a valid prerelease`);
784
854
  }
785
855
  }
786
- function handleStableVersionWithReleaseType(currentVersion, commits, types, force) {
856
+ function handleStableVersionWithReleaseType(currentVersion, commits, types, force, capZeroMajor) {
787
857
  if (!commits?.length && !force) {
788
858
  logger.debug('No commits found for stable version with "release" type, skipping bump');
789
859
  return void 0;
790
860
  }
791
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types, currentVersion) : void 0;
861
+ const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types, currentVersion, capZeroMajor) : void 0;
792
862
  if (!detectedType && !force) {
793
863
  logger.debug("No significant commits found, skipping bump");
794
864
  return void 0;
@@ -796,12 +866,12 @@ function handleStableVersionWithReleaseType(currentVersion, commits, types, forc
796
866
  logger.debug(`Auto-detected release type from commits: ${detectedType}`);
797
867
  return detectedType;
798
868
  }
799
- function handleStableVersionWithPrereleaseType(currentVersion, commits, types, force) {
869
+ function handleStableVersionWithPrereleaseType(currentVersion, commits, types, force, capZeroMajor) {
800
870
  if (!commits?.length && !force) {
801
871
  logger.debug('No commits found for stable version with "prerelease" type, skipping bump');
802
872
  return void 0;
803
873
  }
804
- const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types, currentVersion) : void 0;
874
+ const detectedType = commits?.length ? detectReleaseTypeFromCommits(commits, types, currentVersion, capZeroMajor) : void 0;
805
875
  if (!detectedType) {
806
876
  logger.debug("No significant commits found, using prepatch as default");
807
877
  return "prepatch";
@@ -835,7 +905,7 @@ function getImpliedBumpFromPrerelease(version) {
835
905
  return "minor";
836
906
  return "major";
837
907
  }
838
- function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force, types }) {
908
+ function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force, types, capZeroMajor }) {
839
909
  const currentPreid = getPreid(currentVersion);
840
910
  const hasChangedPreid = preid && currentPreid && currentPreid !== preid;
841
911
  if (hasChangedPreid) {
@@ -854,7 +924,7 @@ function handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, comm
854
924
  return void 0;
855
925
  }
856
926
  if (commits?.length) {
857
- const detectedType = detectReleaseTypeFromCommits(commits, types, currentVersion);
927
+ const detectedType = detectReleaseTypeFromCommits(commits, types, currentVersion, capZeroMajor);
858
928
  const impliedBump = getImpliedBumpFromPrerelease(currentVersion);
859
929
  const detectedRank = getSemverRank(detectedType);
860
930
  const impliedRank = getSemverRank(impliedBump);
@@ -886,7 +956,8 @@ function determineReleaseType({
886
956
  releaseType,
887
957
  preid,
888
958
  types,
889
- force
959
+ force,
960
+ capZeroMajor = true
890
961
  }) {
891
962
  if (releaseType === "release" && preid) {
892
963
  throw new Error('You cannot use a "release" type with a "preid", to use a preid you must use a "prerelease" type');
@@ -899,10 +970,10 @@ function determineReleaseType({
899
970
  const isCurrentPrerelease = isPrerelease(currentVersion);
900
971
  if (!isCurrentPrerelease) {
901
972
  if (releaseType === "release") {
902
- return handleStableVersionWithReleaseType(currentVersion, commits, types, force);
973
+ return handleStableVersionWithReleaseType(currentVersion, commits, types, force, capZeroMajor);
903
974
  }
904
975
  if (releaseType === "prerelease") {
905
- return handleStableVersionWithPrereleaseType(currentVersion, commits, types, force);
976
+ return handleStableVersionWithPrereleaseType(currentVersion, commits, types, force, capZeroMajor);
906
977
  }
907
978
  return handleExplicitReleaseType({ releaseType, currentVersion });
908
979
  }
@@ -910,7 +981,7 @@ function determineReleaseType({
910
981
  return handlePrereleaseVersionToStable(currentVersion);
911
982
  }
912
983
  if (releaseType === "prerelease") {
913
- return handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force, types });
984
+ return handlePrereleaseVersionWithPrereleaseType({ currentVersion, preid, commits, force, types, capZeroMajor });
914
985
  }
915
986
  return handleExplicitReleaseType({ releaseType, currentVersion });
916
987
  }
@@ -2079,6 +2150,127 @@ function isOtpError(error) {
2079
2150
  ];
2080
2151
  return otpPatterns.some((pattern) => searchText.includes(pattern));
2081
2152
  }
2153
+ function isVersionAlreadyPublishedError(error) {
2154
+ if (typeof error !== "object" || error === null) {
2155
+ return false;
2156
+ }
2157
+ const errorMessage = "message" in error && typeof error.message === "string" ? error.message.toLowerCase() : "";
2158
+ const searchText = `${errorMessage} ${String(error).toLowerCase()}`;
2159
+ const patterns = [
2160
+ "epublishconflict",
2161
+ "cannot publish over",
2162
+ "does not allow updating assets",
2163
+ "version already exists",
2164
+ "already exists in the repository",
2165
+ "status 409",
2166
+ "409 conflict"
2167
+ ];
2168
+ return patterns.some((pattern) => searchText.includes(pattern));
2169
+ }
2170
+ function canQueryVersionViaView(packageManager) {
2171
+ return packageManager === "npm" || packageManager === "pnpm";
2172
+ }
2173
+ function isPackageNotFoundError(error) {
2174
+ if (typeof error !== "object" || error === null) {
2175
+ return false;
2176
+ }
2177
+ const errorMessage = "message" in error && typeof error.message === "string" ? error.message.toLowerCase() : "";
2178
+ const searchText = `${errorMessage} ${String(error).toLowerCase()}`;
2179
+ return searchText.includes("e404") || searchText.includes("404 not found") || searchText.includes("is not in this registry") || searchText.includes("no match found");
2180
+ }
2181
+ async function queryVersionViaView({
2182
+ pkg,
2183
+ version,
2184
+ config,
2185
+ packageManager,
2186
+ registryTarget
2187
+ }) {
2188
+ const args = ["view", `${pkg.name}@${version}`, "version", "--json"];
2189
+ if (registryTarget.registry) {
2190
+ args.push("--registry", registryTarget.registry);
2191
+ }
2192
+ const command = `${packageManager} ${args.join(" ")}`;
2193
+ try {
2194
+ const { stdout } = await withRegistryNpmrc({
2195
+ config,
2196
+ registryTarget,
2197
+ packageName: pkg.name,
2198
+ packageManager,
2199
+ fn: () => execPromise(command, {
2200
+ cwd: pkg.path,
2201
+ noStdout: true,
2202
+ noStderr: true,
2203
+ noSuccess: true,
2204
+ noError: true,
2205
+ logLevel: config.logLevel
2206
+ })
2207
+ });
2208
+ const output = (stdout || "").trim();
2209
+ return output.length > 0 && output !== "undefined" && output !== "[]";
2210
+ } catch (error) {
2211
+ if (isPackageNotFoundError(error)) {
2212
+ return false;
2213
+ }
2214
+ logger.debug(`\`view\` check was inconclusive for ${pkg.name}@${version}: ${error instanceof Error ? error.message : String(error)}`);
2215
+ return null;
2216
+ }
2217
+ }
2218
+ function buildPackumentUrl(registry, packageName) {
2219
+ const base = registry.endsWith("/") ? registry : `${registry}/`;
2220
+ const encodedName = packageName.replace("/", "%2F");
2221
+ return `${base}${encodedName}`;
2222
+ }
2223
+ async function queryVersionViaHttp({
2224
+ pkg,
2225
+ version,
2226
+ config,
2227
+ registryTarget
2228
+ }) {
2229
+ const registry = registryTarget.registry || getNpmRegistry(config.cwd);
2230
+ if (!registry) {
2231
+ return null;
2232
+ }
2233
+ const token = registryTarget.token || config.publish.token || config.tokens.registry;
2234
+ const headers = { accept: "application/json" };
2235
+ if (token) {
2236
+ headers.authorization = `Bearer ${token}`;
2237
+ }
2238
+ const timeoutMs = config.publish.safetyCheckTimeout ?? 15e3;
2239
+ try {
2240
+ const response = await fetch(buildPackumentUrl(registry, pkg.name), {
2241
+ headers,
2242
+ signal: AbortSignal.timeout(timeoutMs)
2243
+ });
2244
+ if (response.status === 404) {
2245
+ return false;
2246
+ }
2247
+ if (!response.ok) {
2248
+ logger.debug(`Packument request for ${pkg.name} returned HTTP ${response.status} - treating as inconclusive`);
2249
+ return null;
2250
+ }
2251
+ const packument = await response.json();
2252
+ return Boolean(packument.versions && Object.hasOwn(packument.versions, version));
2253
+ } catch (error) {
2254
+ logger.debug(`Packument request for ${pkg.name} failed: ${error instanceof Error ? error.message : String(error)}`);
2255
+ return null;
2256
+ }
2257
+ }
2258
+ async function isVersionPublished({
2259
+ pkg,
2260
+ version,
2261
+ config,
2262
+ packageManager,
2263
+ registryTarget
2264
+ }) {
2265
+ if (canQueryVersionViaView(packageManager)) {
2266
+ const viaView = await queryVersionViaView({ pkg, version, config, packageManager, registryTarget });
2267
+ if (viaView !== null) {
2268
+ return viaView;
2269
+ }
2270
+ logger.debug(`Falling back to a direct registry request for ${pkg.name}@${version}`);
2271
+ }
2272
+ return queryVersionViaHttp({ pkg, version, config, registryTarget });
2273
+ }
2082
2274
  function promptOtpWithTimeout(timeout = 9e4) {
2083
2275
  return new Promise((resolve, reject) => {
2084
2276
  const timer = setTimeout(() => {
@@ -2178,6 +2370,14 @@ async function publishToRegistryTarget({
2178
2370
  registryLabel
2179
2371
  }) {
2180
2372
  const tag = determinePublishTag(pkg.newVersion || pkg.version, registryTarget.tag ?? config.publish.tag);
2373
+ if (config.publish.skipExistingVersions && !dryRun) {
2374
+ const version = pkg.newVersion || pkg.version;
2375
+ const alreadyPublished = await isVersionPublished({ pkg, version, config, packageManager, registryTarget });
2376
+ if (alreadyPublished) {
2377
+ logger.warn(`Skipping ${packageNameAndVersion}${registryLabel}: version already exists on the registry (publish.skipExistingVersions enabled)`);
2378
+ return;
2379
+ }
2380
+ }
2181
2381
  let dynamicOtp;
2182
2382
  const maxAttempts = 2;
2183
2383
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
@@ -2215,6 +2415,15 @@ async function publishToRegistryTarget({
2215
2415
  } catch (error) {
2216
2416
  if (isOtpError(error) && attempt < maxAttempts - 1) {
2217
2417
  dynamicOtp = await handleOtpError();
2418
+ } else if (isVersionAlreadyPublishedError(error)) {
2419
+ if (config.publish.skipExistingVersions) {
2420
+ logger.warn(`Skipping ${packageNameAndVersion}${registryLabel}: version already exists on the registry (publish.skipExistingVersions enabled)`);
2421
+ return;
2422
+ }
2423
+ throw new Error(
2424
+ `Version ${packageNameAndVersion} already exists on the registry${registryLabel} and cannot be overwritten. This usually happens when re-running a release on the same commit (e.g. a canary rerun). Enable publish.skipExistingVersions (or pass --skip-existing-versions) to skip already-published versions instead of failing.`,
2425
+ { cause: error }
2426
+ );
2218
2427
  } else {
2219
2428
  logger.error(`Failed to publish ${packageNameAndVersion}:`, error);
2220
2429
  throw error;
@@ -2354,7 +2563,8 @@ function getDefaultConfig() {
2354
2563
  type: "release",
2355
2564
  clean: true,
2356
2565
  dependencyTypes: ["dependencies"],
2357
- yes: false
2566
+ yes: false,
2567
+ capZeroMajor: true
2358
2568
  },
2359
2569
  changelog: {
2360
2570
  rootChangelog: true,
@@ -2372,6 +2582,9 @@ function getDefaultConfig() {
2372
2582
  // (e.g. a corporate proxy) is honored instead of forcing the public one.
2373
2583
  safetyCheck: true,
2374
2584
  safetyCheckTimeout: 15e3,
2585
+ // Annotated as `boolean` (not the inferred `false` literal) so the resolved
2586
+ // config type stays assignable to true when overridden by config/CLI.
2587
+ skipExistingVersions: false,
2375
2588
  packageManager: detectPackageManager(process$1.cwd())
2376
2589
  },
2377
2590
  tokens: {
@@ -3910,10 +4123,11 @@ function collectPackageBumps({
3910
4123
  }) {
3911
4124
  if (bumpedPackages && bumpedPackages.length > 0) {
3912
4125
  return bumpedPackages.map((pkg) => {
3913
- const hasTransition = Boolean(pkg.newVersion && pkg.oldVersion !== pkg.newVersion);
4126
+ const displayOldVersion = extractVersionFromTag(pkg.fromTag, pkg.name) || pkg.oldVersion;
4127
+ const hasTransition = Boolean(pkg.newVersion && displayOldVersion !== pkg.newVersion);
3914
4128
  return {
3915
4129
  name: pkg.name,
3916
- oldVersion: pkg.oldVersion,
4130
+ oldVersion: displayOldVersion,
3917
4131
  newVersion: pkg.newVersion,
3918
4132
  version: pkg.newVersion || pkg.version,
3919
4133
  hasTransition
@@ -5128,7 +5342,8 @@ async function bump(options = {}) {
5128
5342
  const packages = readPackages({
5129
5343
  cwd: config.cwd,
5130
5344
  patterns: config.monorepo?.packages,
5131
- ignorePackageNames: config.monorepo?.ignorePackageNames
5345
+ ignorePackageNames: config.monorepo?.ignorePackageNames,
5346
+ ignored: config.monorepo?.ignored
5132
5347
  });
5133
5348
  logger.debug(`Found ${packages.length} package(s)`);
5134
5349
  let result;
@@ -5500,9 +5715,10 @@ function buildSuccessComment({
5500
5715
  const distTag = config.publish?.tag;
5501
5716
  const installCmd = getInstallCommand(config.publish?.packageManager);
5502
5717
  const lines = [PR_COMMENT_MARKER, "", "## \u{1F680} Release published", ""];
5718
+ const previousVersion = extractVersionFromTag(bumpResult?.fromTag ?? "") || bumpResult?.oldVersion;
5503
5719
  lines.push(...buildMetadataLines({
5504
5720
  version,
5505
- oldVersion: bumpResult?.oldVersion,
5721
+ oldVersion: previousVersion,
5506
5722
  tags,
5507
5723
  distTag,
5508
5724
  date,
@@ -5580,6 +5796,7 @@ async function prComment(options = {}) {
5580
5796
  cwd: config.cwd,
5581
5797
  patterns: config.monorepo?.packages,
5582
5798
  ignorePackageNames: config.monorepo?.ignorePackageNames,
5799
+ ignored: config.monorepo?.ignored,
5583
5800
  includePrivates: config.monorepo?.includePrivates
5584
5801
  }));
5585
5802
  packages = readPkgs.map((pkg) => ({ name: pkg.name, version: pkg.version }));
@@ -5617,6 +5834,32 @@ ${body}`
5617
5834
  }
5618
5835
  return await postPrComment({ config, pr, body });
5619
5836
  }
5837
+ async function tryPostPrComment({
5838
+ config,
5839
+ releaseContext,
5840
+ prNumber,
5841
+ dryRun,
5842
+ logLevel,
5843
+ configName
5844
+ }) {
5845
+ if (!config.release.prComment) {
5846
+ logger.info("Skipping PR comment (--no-pr-comment)");
5847
+ return false;
5848
+ }
5849
+ try {
5850
+ return await prComment({
5851
+ prNumber,
5852
+ dryRun,
5853
+ logLevel,
5854
+ configName,
5855
+ config,
5856
+ releaseContext
5857
+ });
5858
+ } catch (error) {
5859
+ logger.warn("PR comment posting failed:", error);
5860
+ return false;
5861
+ }
5862
+ }
5620
5863
 
5621
5864
  async function providerReleaseSafetyCheck({ config, provider }) {
5622
5865
  if (!config.safetyCheck || !config.release.providerRelease) {
@@ -5801,7 +6044,8 @@ async function publish(options = {}) {
5801
6044
  registries: options.registries,
5802
6045
  tag: options.tag,
5803
6046
  buildCmd: options.buildCmd,
5804
- token: options.token
6047
+ token: options.token,
6048
+ skipExistingVersions: options.skipExistingVersions
5805
6049
  },
5806
6050
  logLevel: options.logLevel,
5807
6051
  safetyCheck: options.safetyCheck
@@ -6388,7 +6632,8 @@ function getReleaseConfig(options = {}) {
6388
6632
  registries: options.registries,
6389
6633
  tag: options.tag,
6390
6634
  buildCmd: options.buildCmd,
6391
- token: options.publishToken
6635
+ token: options.publishToken,
6636
+ skipExistingVersions: options.skipExistingVersions
6392
6637
  },
6393
6638
  release: {
6394
6639
  commit: options.commit,
@@ -6428,31 +6673,51 @@ async function releaseSafetyCheck({
6428
6673
  throw error;
6429
6674
  }
6430
6675
  }
6431
- async function tryPostPrComment({
6676
+ function buildReleaseSummary({
6432
6677
  config,
6433
- releaseContext,
6434
- prNumber,
6435
- dryRun,
6436
- logLevel,
6437
- configName
6678
+ bumpResult,
6679
+ publishResponse,
6680
+ createdTags,
6681
+ provider,
6682
+ postedReleases,
6683
+ providerError,
6684
+ socialResults,
6685
+ prCommentPosted
6438
6686
  }) {
6439
- if (!config.release.prComment) {
6440
- logger.info("Skipping PR comment (--no-pr-comment)");
6441
- return false;
6687
+ const publishedPackageCount = publishResponse?.publishedPackages.length ?? 0;
6688
+ const versionDisplay = config.monorepo?.versionMode === "independent" ? `${bumpResult.bumpedPackages.length} packages bumped independently` : bumpResult.newVersion || readPackageJson(config.cwd)?.version;
6689
+ let providerDisplay = "Disabled";
6690
+ if (config.release.providerRelease) {
6691
+ if (providerError) {
6692
+ providerDisplay = `Failed: ${providerError}`;
6693
+ } else {
6694
+ providerDisplay = `${postedReleases.length} release${postedReleases.length !== 1 ? "s" : ""}`;
6695
+ }
6442
6696
  }
6443
- try {
6444
- return await prComment({
6445
- prNumber,
6446
- dryRun,
6447
- logLevel,
6448
- configName,
6449
- config,
6450
- releaseContext
6451
- });
6452
- } catch (error) {
6453
- logger.warn("PR comment posting failed:", error);
6454
- return false;
6697
+ let socialDisplay = "Disabled";
6698
+ if (config.release.social && socialResults) {
6699
+ if (socialResults.hasErrors) {
6700
+ const failed = socialResults.results.filter((r) => !r.success).map((r) => r.platform);
6701
+ const succeeded = socialResults.results.filter((r) => r.success).map((r) => r.platform);
6702
+ socialDisplay = `${succeeded.length} succeeded, ${failed.length} failed (${failed.join(", ")})`;
6703
+ } else {
6704
+ socialDisplay = `${socialResults.results.length} succeeded`;
6705
+ }
6455
6706
  }
6707
+ let prCommentDisplay = "Disabled";
6708
+ if (config.release.prComment) {
6709
+ prCommentDisplay = prCommentPosted ? "Posted" : "Failed";
6710
+ }
6711
+ return `Release workflow completed!
6712
+
6713
+ Version: ${versionDisplay ?? "Unknown"}
6714
+ Tag(s): ${createdTags?.length ? createdTags.join(", ") : "None"}
6715
+ Pushed: ${config.release.push ? "Yes" : "Disabled"}
6716
+ Published packages: ${config.release.publish ? publishedPackageCount : "Disabled"}
6717
+ Provider release: ${providerDisplay}
6718
+ Social media: ${socialDisplay}
6719
+ PR comment: ${prCommentDisplay}
6720
+ Git provider: ${provider}`;
6456
6721
  }
6457
6722
  async function release(options = {}) {
6458
6723
  const isCanary = options.canary ?? false;
@@ -6641,40 +6906,17 @@ async function release(options = {}) {
6641
6906
  logLevel: config.logLevel,
6642
6907
  configName: options.configName
6643
6908
  });
6644
- const publishedPackageCount = publishResponse?.publishedPackages.length ?? 0;
6645
- const versionDisplay = config.monorepo?.versionMode === "independent" ? `${bumpResult.bumpedPackages.length} packages bumped independently` : bumpResult.newVersion || readPackageJson(config.cwd)?.version;
6646
- let providerDisplay = "Disabled";
6647
- if (config.release.providerRelease) {
6648
- if (providerError) {
6649
- providerDisplay = `Failed: ${providerError}`;
6650
- } else {
6651
- providerDisplay = `${postedReleases.length} release${postedReleases.length !== 1 ? "s" : ""}`;
6652
- }
6653
- }
6654
- let socialDisplay = "Disabled";
6655
- if (config.release.social && socialResults) {
6656
- if (socialResults.hasErrors) {
6657
- const failed = socialResults.results.filter((r) => !r.success).map((r) => r.platform);
6658
- const succeeded = socialResults.results.filter((r) => r.success).map((r) => r.platform);
6659
- socialDisplay = `${succeeded.length} succeeded, ${failed.length} failed (${failed.join(", ")})`;
6660
- } else {
6661
- socialDisplay = `${socialResults.results.length} succeeded`;
6662
- }
6663
- }
6664
- let prCommentDisplay = "Disabled";
6665
- if (config.release.prComment) {
6666
- prCommentDisplay = prCommentPosted ? "Posted" : "Failed";
6667
- }
6668
- logger.box(`Release workflow completed!
6669
-
6670
- Version: ${versionDisplay ?? "Unknown"}
6671
- Tag(s): ${createdTags?.length ? createdTags.join(", ") : "None"}
6672
- Pushed: ${config.release.push ? "Yes" : "Disabled"}
6673
- Published packages: ${config.release.publish ? publishedPackageCount : "Disabled"}
6674
- Provider release: ${providerDisplay}
6675
- Social media: ${socialDisplay}
6676
- PR comment: ${prCommentDisplay}
6677
- Git provider: ${provider}`);
6909
+ logger.box(buildReleaseSummary({
6910
+ config,
6911
+ bumpResult,
6912
+ publishResponse,
6913
+ createdTags,
6914
+ provider,
6915
+ postedReleases,
6916
+ providerError,
6917
+ socialResults,
6918
+ prCommentPosted
6919
+ }));
6678
6920
  await executeHook("success:release", config, dryRun);
6679
6921
  } catch (error) {
6680
6922
  logger.error("Error during release workflow!\n\n", error);
@@ -6695,4 +6937,4 @@ Git provider: ${provider}`);
6695
6937
  }
6696
6938
  }
6697
6939
 
6698
- export { getLastPackageTag as $, extractVersionFromTag as A, fetchGitTags as B, filterOutPrivatePackages as C, findGitHubPR as D, findGitLabMR as E, findReachableCommitBySubject as F, formatChangelogForSlack as G, formatPackagesForSlack as H, formatSlackMessage as I, formatTweetMessage as J, generateChangelog as K, generateMarkDown as L, getAuthCommand as M, NEW_PACKAGE_MARKER as N, getBumpedIndependentPackages as O, PR_COMMENT_MARKER as P, getBumpedPackageIndependently as Q, getCIName as R, getCanaryVersion as S, getCommitSubject as T, getCurrentGitBranch as U, getCurrentGitRef as V, getDefaultConfig as W, getDependentsOf as X, getFirstCommit as Y, getGitStatus as Z, getIndependentTag as _, buildCommentBody as a, topologicalSort as a$, getLastRepoTag as a0, getLastStableTag as a1, getLastTag as a2, getModifiedReleaseFilePatterns as a3, getNpmRegistry as a4, getPackageCommits as a5, getPackageDependencies as a6, getPackageNewVersion as a7, getPackages as a8, getPackagesOrBumpedPackages as a9, postPrComment as aA, postReleaseToSlack as aB, postReleaseToTwitter as aC, prComment as aD, providerRelease as aE, providerReleaseSafetyCheck as aF, publish as aG, publishPackage as aH, publishSafetyCheck as aI, pushCommitAndTags as aJ, pushTagForce as aK, readPackageJson as aL, readPackages as aM, reconcileFromTag as aN, redactSecrets as aO, release as aP, resetRewrittenTagCache as aQ, resolveAllConfiguredRegistryTargets as aR, resolveRegistryTargetsForPackage as aS, resolveRegistryTargetsForPackages as aT, resolveTags as aU, retagAnnotatedLocal as aV, rollbackModifiedFiles as aW, shouldFilterPrereleaseTags as aX, social as aY, socialSafetyCheck as aZ, tagExists as a_, getPackagesToPublishInIndependentMode as aa, getPackagesToPublishInSelectiveMode as ab, getPreid as ac, getReleaseUrl as ad, getRootPackage as ae, getShortCommitSha as af, getSlackToken as ag, getSlackWebhookUrl as ah, getTwitterCredentials as ai, github as aj, gitlab as ak, hasLernaJson as al, isAncestor as am, isBumpedPackage as an, isChangedPreid as ao, isGraduating as ap, isGraduatingToStableBetweenVersion as aq, isInCI as ar, isPrerelease as as, isPrereleaseReleaseType as at, isStableReleaseType as au, isTagVersionCompatibleWithCurrent as av, loadRelizyConfig as aw, mergeTypes as ax, parseChangelogMarkdown as ay, parseGitRemoteUrl as az, buildChangelogBody as b, updateLernaVersion as b0, withRegistryNpmrc as b1, writeChangelogToFile as b2, writeVersion as b3, buildCompareLink as c, buildContributors as d, bump as e, capReleaseTypeForZeroMajor as f, changelog as g, checkGitStatusIfDirty as h, collectContributorNames as i, collectPackageBumps as j, confirmBump as k, createCommitAndTags as l, createGitlabRelease as m, defineConfig as n, detectGitProvider as o, detectPackageManager as p, detectPullRequest as q, determinePublishTag as r, determineReleaseType as s, determineSemverChange as t, executeBuildCmd as u, executeFormatCmd as v, executeHook as w, expandPackagesToBumpWithDependents as x, extractChangelogSummary as y, extractVersionFromPackageTag as z };
6940
+ export { getGitStatus as $, extractVersionFromPackageTag as A, extractVersionFromTag as B, fetchGitTags as C, filterOutPrivatePackages as D, findGitHubPR as E, findGitLabMR as F, findReachableCommitBySubject as G, formatChangelogForSlack as H, formatPackagesForSlack as I, formatSlackMessage as J, formatTweetMessage as K, generateChangelog as L, generateMarkDown as M, NEW_PACKAGE_MARKER as N, getAuthCommand as O, PR_COMMENT_MARKER as P, getBumpedIndependentPackages as Q, getBumpedPackageIndependently as R, getCIName as S, getCanaryVersion as T, getCommitChangedFiles as U, getCommitSubject as V, getCurrentGitBranch as W, getCurrentGitRef as X, getDefaultConfig as Y, getDependentsOf as Z, getFirstCommit as _, buildCommentBody as a, resolveTags as a$, getIndependentTag as a0, getLastPackageTag as a1, getLastRepoTag as a2, getLastStableTag as a3, getLastTag as a4, getModifiedReleaseFilePatterns as a5, getNpmRegistry as a6, getPackageCommits as a7, getPackageDependencies as a8, getPackageNewVersion as a9, isVersionAlreadyPublishedError as aA, isVersionPublished as aB, loadRelizyConfig as aC, mergeTypes as aD, parseChangelogMarkdown as aE, parseGitRemoteUrl as aF, postPrComment as aG, postReleaseToSlack as aH, postReleaseToTwitter as aI, prComment as aJ, providerRelease as aK, providerReleaseSafetyCheck as aL, publish as aM, publishPackage as aN, publishSafetyCheck as aO, pushCommitAndTags as aP, pushTagForce as aQ, readPackageJson as aR, readPackages as aS, reconcileFromTag as aT, redactSecrets as aU, release as aV, resetRewrittenTagCache as aW, resolveAllConfiguredRegistryTargets as aX, resolveIgnoredPackagePaths as aY, resolveRegistryTargetsForPackage as aZ, resolveRegistryTargetsForPackages as a_, getPackages as aa, getPackagesOrBumpedPackages as ab, getPackagesToPublishInIndependentMode as ac, getPackagesToPublishInSelectiveMode as ad, getPreid as ae, getReleaseUrl as af, getRootPackage as ag, getShortCommitSha as ah, getSlackToken as ai, getSlackWebhookUrl as aj, getTwitterCredentials as ak, github as al, gitlab as am, globDirsAbsolute as an, hasLernaJson as ao, isAncestor as ap, isBumpedPackage as aq, isChangedPreid as ar, isCommitOnlyInIgnoredPackages as as, isGraduating as at, isGraduatingToStableBetweenVersion as au, isInCI as av, isPrerelease as aw, isPrereleaseReleaseType as ax, isStableReleaseType as ay, isTagVersionCompatibleWithCurrent as az, buildChangelogBody as b, retagAnnotatedLocal as b0, rollbackModifiedFiles as b1, shouldFilterPrereleaseTags as b2, social as b3, socialSafetyCheck as b4, tagExists as b5, topologicalSort as b6, tryPostPrComment as b7, updateLernaVersion as b8, withRegistryNpmrc as b9, writeChangelogToFile as ba, writeVersion as bb, buildCompareLink as c, buildContributors as d, buildReleaseSummary as e, bump as f, capReleaseTypeForZeroMajor as g, changelog as h, checkGitStatusIfDirty as i, collectContributorNames as j, collectPackageBumps as k, confirmBump as l, createCommitAndTags as m, createGitlabRelease as n, defineConfig as o, detectGitProvider as p, detectPackageManager as q, detectPullRequest as r, determinePublishTag as s, determineReleaseType as t, determineSemverChange as u, executeBuildCmd as v, executeFormatCmd as w, executeHook as x, expandPackagesToBumpWithDependents as y, extractChangelogSummary as z };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "relizy",
3
3
  "type": "module",
4
- "version": "1.5.0-beta.2",
4
+ "version": "1.5.0-beta.4",
5
5
  "description": "Changelogen adapter for monorepo management with unified and independent versioning",
6
6
  "author": "Louis Mazel <me@loicmazuel.com>",
7
7
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  "node": ">=20.0.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "@slack/web-api": "^7.0.0",
58
+ "@slack/web-api": "^7.0.0 || ^8.0.0",
59
59
  "@yoloship/claude-sdk": "^0.2.0-beta.6",
60
60
  "twitter-api-v2": "^1.20.0"
61
61
  },
@@ -73,7 +73,7 @@
73
73
  "dependencies": {
74
74
  "@inquirer/prompts": "^8.7.2",
75
75
  "@maz-ui/node": "5.0.0-beta.26",
76
- "@maz-ui/utils": "5.0.0-beta.25",
76
+ "@maz-ui/utils": "5.0.0-beta.39",
77
77
  "c12": "^3.3.3",
78
78
  "changelogen": "^0.6.2",
79
79
  "commander": "^15.0.0",
@@ -85,28 +85,28 @@
85
85
  "semver": "^7.8.5"
86
86
  },
87
87
  "devDependencies": {
88
- "@commitlint/cli": "^21.2.2",
89
- "@commitlint/config-conventional": "21.2.2",
90
- "@commitlint/cz-commitlint": "^21.2.2",
91
- "@commitlint/types": "^21.2.0",
88
+ "@commitlint/cli": "^21.2.3",
89
+ "@commitlint/config-conventional": "21.2.3",
90
+ "@commitlint/cz-commitlint": "^21.2.3",
91
+ "@commitlint/types": "^21.2.3",
92
92
  "@maz-ui/eslint-config": "5.0.0-beta.43",
93
- "@slack/web-api": "7.16.0",
93
+ "@slack/web-api": "8.1.1",
94
94
  "@types/micromatch": "^4.0.10",
95
- "@types/node": "^25.9.2",
95
+ "@types/node": "^26.6.2",
96
96
  "@types/semver": "^7.8.0",
97
- "@vitest/coverage-v8": "^5.0.0",
97
+ "@vitest/coverage-v8": "^5.0.1",
98
98
  "@yoloship/claude-sdk": "0.2.0-beta.6",
99
99
  "cross-env": "10.1.0",
100
- "eslint": "^10.10.0",
100
+ "eslint": "^10.11.0",
101
101
  "husky": "9.1.7",
102
102
  "jiti": "2.7.0",
103
103
  "lint-staged": "^17.5.1",
104
- "memfs": "^4.78.0",
105
- "tsx": "^4.23.13",
104
+ "memfs": "^4.78.1",
105
+ "tsx": "^4.23.15",
106
106
  "twitter-api-v2": "^1.29.1",
107
- "typescript": "^6.0.3",
107
+ "typescript": "^6.0.0",
108
108
  "unbuild": "^3.6.1",
109
- "vitest": "^5.0.0"
109
+ "vitest": "^5.0.1"
110
110
  },
111
111
  "lint-staged": {
112
112
  "*.{js,jsx,ts,tsx,mjs,mts,cjs,md,yml,json}": [