@reelout/core 0.1.1 → 0.2.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.
Files changed (125) hide show
  1. package/dist/analyzer.js +38 -0
  2. package/dist/analyzer.js.map +1 -0
  3. package/dist/builder.js +52 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/changelog.js +58 -0
  6. package/dist/changelog.js.map +1 -0
  7. package/dist/formatError-BXNdyRfu.js +23 -0
  8. package/dist/formatError-BXNdyRfu.js.map +1 -0
  9. package/dist/index.js +175 -1
  10. package/dist/index.js.map +1 -0
  11. package/dist/types-la_KkjCS.js +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/workflow-BoVmbvH6.js +2342 -0
  14. package/dist/workflow-BoVmbvH6.js.map +1 -0
  15. package/package.json +27 -3
  16. package/src/analyzer.ts +63 -0
  17. package/src/builder.ts +104 -0
  18. package/src/cache/TTLCache.ts +100 -0
  19. package/src/cache/index.ts +4 -0
  20. package/src/changelog.ts +97 -0
  21. package/src/config/const.ts +97 -0
  22. package/src/config/index.ts +11 -0
  23. package/src/config/isSemVerGitTag.ts +25 -0
  24. package/src/config/normalizeCommitFilter.ts +37 -0
  25. package/src/config/normalizeReleaseRule.ts +42 -0
  26. package/src/config/normalizeWorkspace.ts +34 -0
  27. package/src/config/resolvePlugin.ts +56 -0
  28. package/src/config/resolveReleaseTarget.ts +71 -0
  29. package/src/config/sanitizePrereleaseLabel.ts +18 -0
  30. package/src/git/branch/getCurrentGitBranch.ts +18 -0
  31. package/src/git/branch/index.ts +4 -0
  32. package/src/git/commit/createGitCommit.ts +29 -0
  33. package/src/git/commit/extractMentions.ts +34 -0
  34. package/src/git/commit/getCurrentGitCommit.ts +14 -0
  35. package/src/git/commit/getParsedCommit.ts +53 -0
  36. package/src/git/commit/index.ts +9 -0
  37. package/src/git/commit/parseCommit.ts +99 -0
  38. package/src/git/commit/parseGitPrettyFormat.ts +76 -0
  39. package/src/git/history/countGitHistory.ts +28 -0
  40. package/src/git/history/expandGitHistory.ts +108 -0
  41. package/src/git/history/index.ts +9 -0
  42. package/src/git/history/mergeGitHistory.ts +76 -0
  43. package/src/git/history/restoreFullGitHistory.ts +19 -0
  44. package/src/git/history/showGitHistory.ts +80 -0
  45. package/src/git/history/showParsedGitHistory.ts +30 -0
  46. package/src/git/index.ts +9 -0
  47. package/src/git/repository/ensureGitUserConfig.ts +31 -0
  48. package/src/git/repository/getGitConfigValue.ts +27 -0
  49. package/src/git/repository/getGitRemote.ts +20 -0
  50. package/src/git/repository/index.ts +10 -0
  51. package/src/git/repository/isGitShallowRepository.ts +21 -0
  52. package/src/git/repository/pushGitChanges.ts +25 -0
  53. package/src/git/repository/resolveGitRepository.ts +40 -0
  54. package/src/git/repository/withShallowLock.ts +62 -0
  55. package/src/git/revision/detectGitRelationship.ts +43 -0
  56. package/src/git/revision/index.ts +7 -0
  57. package/src/git/revision/isGitAncestor.ts +33 -0
  58. package/src/git/revision/readGitFile.ts +29 -0
  59. package/src/git/revision/resolveGitRevision.ts +23 -0
  60. package/src/git/tag/createGitTag.ts +25 -0
  61. package/src/git/tag/deleteGitTag.ts +17 -0
  62. package/src/git/tag/ensureGitTag.ts +63 -0
  63. package/src/git/tag/generateGitTagName.ts +43 -0
  64. package/src/git/tag/getCurrentGitTag.ts +23 -0
  65. package/src/git/tag/getGitTagMessage.ts +35 -0
  66. package/src/git/tag/getLatestGitTag.ts +22 -0
  67. package/src/git/tag/index.ts +13 -0
  68. package/src/git/tag/isGitTagConnected.ts +33 -0
  69. package/src/git/tag/isGitTagPointed.ts +38 -0
  70. package/src/git/tag/isGitTagPresent.ts +17 -0
  71. package/src/index.ts +8 -2
  72. package/src/types.ts +217 -0
  73. package/src/util/buildTrailerPattern.ts +20 -0
  74. package/src/util/compact.ts +14 -0
  75. package/src/util/execCommand.ts +130 -0
  76. package/src/util/execGit.ts +18 -0
  77. package/src/util/formatError.ts +21 -0
  78. package/src/util/formatFile.ts +41 -0
  79. package/src/util/index.ts +16 -0
  80. package/src/util/interpolate.ts +141 -0
  81. package/src/util/isCI.ts +23 -0
  82. package/src/util/isDefined.ts +12 -0
  83. package/src/util/isMatch.ts +43 -0
  84. package/src/util/readJson.ts +47 -0
  85. package/src/util/unique.ts +26 -0
  86. package/src/util/writeJson.ts +28 -0
  87. package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
  88. package/src/workflow/analysis/index.ts +9 -0
  89. package/src/workflow/analysis/isCommitForPackage.ts +23 -0
  90. package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
  91. package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
  92. package/src/workflow/analysis/sliceUnreleased.ts +36 -0
  93. package/src/workflow/analysis/streamPackageCommits.ts +34 -0
  94. package/src/workflow/changelog/buildChangelog.ts +79 -0
  95. package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
  96. package/src/workflow/changelog/extractLatestVersion.ts +22 -0
  97. package/src/workflow/changelog/extractPlainText.ts +38 -0
  98. package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
  99. package/src/workflow/changelog/extractSemVer.ts +33 -0
  100. package/src/workflow/changelog/index.ts +12 -0
  101. package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
  102. package/src/workflow/changelog/prependChangelog.ts +59 -0
  103. package/src/workflow/changelog/writeChangelog.ts +27 -0
  104. package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
  105. package/src/workflow/committing/index.ts +5 -0
  106. package/src/workflow/committing/isReleaseCommit.ts +52 -0
  107. package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
  108. package/src/workflow/discovery/discoverPackageTag.ts +98 -0
  109. package/src/workflow/discovery/index.ts +9 -0
  110. package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
  111. package/src/workflow/discovery/resolvePackageTag.ts +45 -0
  112. package/src/workflow/discovery/sortPackages.ts +73 -0
  113. package/src/workflow/discovery/verifyPackageTag.ts +43 -0
  114. package/src/workflow/index.ts +10 -0
  115. package/src/workflow/report/generateReleaseReport.ts +76 -0
  116. package/src/workflow/report/index.ts +5 -0
  117. package/src/workflow/report/printReleaseReport.ts +97 -0
  118. package/src/workflow/runner/index.ts +4 -0
  119. package/src/workflow/runner/runWorkflow.ts +120 -0
  120. package/src/workflow/versioning/bumpVersion.ts +67 -0
  121. package/src/workflow/versioning/deriveReleaseType.ts +33 -0
  122. package/src/workflow/versioning/determineReleaseType.ts +35 -0
  123. package/src/workflow/versioning/index.ts +8 -0
  124. package/src/workflow/versioning/isStableVersion.ts +31 -0
  125. package/src/workflow/versioning/pickPriorityReleaseType.ts +27 -0
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import {
5
+ ensureGitTag,
6
+ expandGitHistory,
7
+ getParsedCommit,
8
+ isGitTagConnected,
9
+ } from '#git'
10
+ import type { Package } from '#types'
11
+ import { isCommitForPackage } from '../analysis/isCommitForPackage'
12
+
13
+ /**
14
+ * Verify a Git tag against package specifications and history connection.
15
+ *
16
+ * Ensures the target tag exists locally before parsing its commit data. Rejects
17
+ * tags that do not target a commit matching the given `pkg`. Expands the history
18
+ * graph up to the tag reference before verifying structural connectivity.
19
+ *
20
+ * @param cwd Repository working directory
21
+ * @param pkg Package descriptor containing version and location context
22
+ * @param tagName The name of the Git tag to verify
23
+ * @returns A promise resolving to true if the tag is valid and connected
24
+ */
25
+ export async function verifyPackageTag(
26
+ cwd: string,
27
+ pkg: Package,
28
+ tagName: string
29
+ ): Promise<boolean> {
30
+ await ensureGitTag(cwd, tagName)
31
+
32
+ const tagRef = `refs/tags/${tagName}`
33
+
34
+ const tagCommit = await getParsedCommit(cwd, tagRef)
35
+
36
+ if (!tagCommit || !isCommitForPackage(pkg, tagCommit)) {
37
+ return false
38
+ }
39
+
40
+ await expandGitHistory(cwd, { until: tagRef })
41
+
42
+ return isGitTagConnected(cwd, tagName)
43
+ }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './analysis'
5
+ export * from './changelog'
6
+ export * from './committing'
7
+ export * from './discovery'
8
+ export * from './report'
9
+ export * from './runner'
10
+ export * from './versioning'
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type {
5
+ ReleaseContext,
6
+ ReleaseLifecycle,
7
+ ReleaseReport,
8
+ ReleaseStatus,
9
+ } from '#types'
10
+ import { compact } from '#util'
11
+
12
+ /**
13
+ * Generate a release report from the current workflow state.
14
+ *
15
+ * Extracts relevant information from the context and organizes it into a report
16
+ * containing workflow phase, status, plugin metadata, packages, and release plans.
17
+ *
18
+ * @param context Release context with workflow state and plans
19
+ * @param plugin Name of the current plugin executing
20
+ * @param hook Current workflow phase
21
+ * @param status Workflow completion status (`'success'` or `'failure'`)
22
+ * @param reason Error or failure reason (used only when status is `'failure'`)
23
+ * @returns Release report object with structured workflow results
24
+ */
25
+ export function generateReleaseReport(
26
+ context: ReleaseContext,
27
+ plugin: string | undefined,
28
+ hook: ReleaseLifecycle | undefined,
29
+ status: ReleaseStatus,
30
+ reason: unknown
31
+ ): ReleaseReport {
32
+ return {
33
+ phase: plugin && hook ? { plugin, hook } : undefined,
34
+ status,
35
+ reason: Error.isError(reason)
36
+ ? reason.message
37
+ : typeof reason === 'string'
38
+ ? reason
39
+ : null,
40
+ prereleaseLabel: context.prereleaseLabel,
41
+ releaseChannel: context.releaseChannel,
42
+ plugins: context.plugins.map((plugin) => ({
43
+ name: plugin.name,
44
+ version: plugin.version,
45
+ hooks: Object.keys(plugin).filter(
46
+ (key) => key !== 'name' && key !== 'version'
47
+ ),
48
+ })),
49
+ packages: context.packages.map((pkg) => ({
50
+ dir: pkg.dir,
51
+ name: pkg.name,
52
+ version: pkg.version,
53
+ })),
54
+ plans: compact(
55
+ context.plans.map((plan) => {
56
+ if (!plan.release?.version) return null
57
+
58
+ return {
59
+ package: {
60
+ entry: plan.package.entry,
61
+ dir: plan.package.dir,
62
+ name: plan.package.name,
63
+ version: plan.package.version,
64
+ private: !!plan.package.private,
65
+ },
66
+ release: {
67
+ type: plan.release.type,
68
+ version: plan.release.version,
69
+ tag: plan.release.tag?.name,
70
+ changelog: plan.release.changelog,
71
+ },
72
+ }
73
+ })
74
+ ),
75
+ }
76
+ }
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './generateReleaseReport'
5
+ export * from './printReleaseReport'
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { ReleaseContext, ReleaseReport } from '#types'
5
+ import { colors } from 'consola/utils'
6
+ import path from 'node:path'
7
+
8
+ /**
9
+ * Print a formatted release report to the logger.
10
+ *
11
+ * Logs the workflow status and details about released packages. On success,
12
+ * displays released package names and version changes. On failure, displays
13
+ * the error reason.
14
+ *
15
+ * @param context Release context for logging
16
+ * @param report Release report to display
17
+ * @returns void
18
+ */
19
+ export function printReleaseReport(
20
+ context: ReleaseContext,
21
+ report: ReleaseReport
22
+ ): void {
23
+ const statusLabel = {
24
+ success: colors.green('SUCCESS'),
25
+ failure: colors.red('FAILURE'),
26
+ }[report.status]
27
+
28
+ context.logger.info(`Status: ${statusLabel}`)
29
+
30
+ if (report.status === 'failure') {
31
+ return context.logger.error(
32
+ [
33
+ 'Release complete:',
34
+ colors.red(
35
+ report.reason ||
36
+ 'An unexpected error occurred during the release process.'
37
+ ),
38
+ ].join(' ')
39
+ )
40
+ }
41
+
42
+ if (report.plans.length === 0) {
43
+ return context.logger.info(
44
+ [
45
+ 'Release complete:',
46
+ colors.yellow('No packages are scheduled for release.'),
47
+ ].join(' ')
48
+ )
49
+ }
50
+
51
+ const header = [
52
+ 'Release complete:',
53
+ colors.bold(colors.green(`${report.plans.length} released`)),
54
+ colors.gray(`(${report.packages.length})`),
55
+ ].join(' ')
56
+
57
+ const list = report.plans
58
+ .toSorted((a, b) =>
59
+ (a.package.name || '').localeCompare(b.package.name || '')
60
+ )
61
+ .map((plan) => {
62
+ let dir = path.normalize(path.join(plan.package.dir, '.'))
63
+
64
+ dir = dir === '.' ? dir : `./${dir}`
65
+
66
+ const name = plan.package.name
67
+ ? colors.bgBlue(` ${plan.package.name} `)
68
+ : !plan.package.entry && colors.gray(` ${dir} `)
69
+
70
+ let version = [plan.release.version, plan.package.version]
71
+ .filter(Boolean)
72
+ .join(' ← ')
73
+
74
+ version = ` ${version} `
75
+
76
+ const type = plan.release.type && colors.gray(` ${plan.release.type} `)
77
+
78
+ const summary = [
79
+ name,
80
+ plan.release.type === 'major'
81
+ ? colors.bgRed(version)
82
+ : colors.bgGreen(version),
83
+ type,
84
+ ]
85
+ .filter(Boolean)
86
+ .join('')
87
+
88
+ const changelog = plan.release.changelog
89
+ ?.replace(/^(#+ .+)$/gm, (_, header) => colors.cyan(header))
90
+ .trim()
91
+
92
+ return [summary, changelog].filter(Boolean).join('\n\n')
93
+ })
94
+ .join('\n\n')
95
+
96
+ context.logger.info(`${header}\n\n${list}\n`)
97
+ }
@@ -0,0 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './runWorkflow'
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { RELEASE_LIFECYCLE_HOOK_LIST } from '#config'
5
+ import type {
6
+ ReleaseContext,
7
+ ReleaseLifecycle,
8
+ ReleasePlugin,
9
+ ReleasePluginHookRollback,
10
+ ReleaseStatus,
11
+ } from '#types'
12
+ import { compact } from '#util'
13
+ import { colors } from 'consola/utils'
14
+ import { generateReleaseReport, printReleaseReport } from '../report'
15
+
16
+ /**
17
+ * Execute the plugin-driven release workflow using the provided `ReleaseContext`.
18
+ *
19
+ * The workflow runs plugin stages in order: `configure`, `discover`,
20
+ * `analyze`, `build`, `write`, `commit`, and `publish`. After a successful
21
+ * run the function sets `context.status` to `'success'`. The workflow is
22
+ * finalized in all cases; on success a release report is printed.
23
+ *
24
+ * The returned array contains all computed release plans, including plans
25
+ * without releases. Side effects (file writes, commits, tags, publishing)
26
+ * are performed by plugins and may be skipped when `context.dryRun` is true.
27
+ *
28
+ * @returns Promise that resolves when the workflow is complete.
29
+ */
30
+ export async function runWorkflow(
31
+ createContext: () => ReleaseContext | Promise<ReleaseContext>
32
+ ): Promise<void> {
33
+ const isSilent = import.meta.env.MODE === 'test'
34
+ const context = await createContext()
35
+ const stack: ReleasePluginHookRollback[] = []
36
+ let plugin: ReleasePlugin | undefined
37
+ let hook: ReleaseLifecycle | undefined
38
+ let status: ReleaseStatus = 'success'
39
+ let reason: unknown = null
40
+
41
+ try {
42
+ const ordered = RELEASE_LIFECYCLE_HOOK_LIST.flatMap((hookName) => {
43
+ return compact(
44
+ context.plugins.map((plugin) => {
45
+ let hook = plugin[hookName]
46
+
47
+ if (!hook) return undefined
48
+
49
+ hook = typeof hook === 'function' ? { handler: hook } : hook
50
+
51
+ const { enforce, handler } = hook
52
+
53
+ const weight = enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0
54
+
55
+ return {
56
+ plugin,
57
+ hook: hookName,
58
+ handler,
59
+ weight,
60
+ }
61
+ })
62
+ ).sort((a, b) => a.weight - b.weight)
63
+ })
64
+
65
+ let handler: (typeof ordered)[number]['handler']
66
+
67
+ for ({ plugin, hook, handler } of ordered) {
68
+ const label = `${colors.gray(`[${hook}]`)} ${plugin.name}`
69
+ let startedLogging = false
70
+ let timer: NodeJS.Timeout | undefined
71
+
72
+ /* istanbul ignore next */
73
+ if (!isSilent) {
74
+ timer = setTimeout(() => {
75
+ startedLogging = true
76
+ context.logger.start(label)
77
+ }, 10)
78
+ }
79
+
80
+ const startTime = performance.now()
81
+
82
+ try {
83
+ const rollback = await handler(context)
84
+
85
+ if (typeof rollback === 'function') {
86
+ stack.push(rollback)
87
+ }
88
+ } finally {
89
+ clearTimeout(timer)
90
+ }
91
+
92
+ const endTime = performance.now()
93
+ const duration = (endTime - startTime) / 1000
94
+
95
+ /* istanbul ignore next */
96
+ if (startedLogging) {
97
+ const color = duration >= 5 ? colors.red : colors.cyan
98
+ const time = color(`${duration.toFixed(2)}s`)
99
+
100
+ context.logger.success(`${label} ${time}`)
101
+ }
102
+ }
103
+ } catch (err) {
104
+ status = 'failure'
105
+ reason = err
106
+
107
+ for (const rollback of stack.toReversed()) {
108
+ try {
109
+ await rollback(reason)
110
+ } catch {}
111
+ }
112
+
113
+ throw err
114
+ } finally {
115
+ printReleaseReport(
116
+ context,
117
+ generateReleaseReport(context, plugin?.name, hook, status, reason)
118
+ )
119
+ }
120
+ }
@@ -0,0 +1,67 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Package, ReleaseType } from '#types'
5
+ import { colors } from 'consola/utils'
6
+ import semver from 'semver'
7
+
8
+ /**
9
+ * Bump a semantic version according to release type and prerelease settings.
10
+ *
11
+ * Increments the version using the provided release type. When a prerelease ID
12
+ * is provided, generates a prerelease version with that identifier. Throws an
13
+ * error when the version is invalid or the initial version is missing.
14
+ *
15
+ * @param pkg Package object containing current version
16
+ * @param releaseType Semantic version component to increment (`'major'`, `'minor'`, `'patch'`)
17
+ * @param prereleaseId Optional prerelease identifier (e.g., `'alpha'`, `'beta'`)
18
+ * @returns Next semantic version string, or `null` when `releaseType` is falsy
19
+ * @throws If the version is invalid or initial version is missing
20
+ */
21
+ export function bumpVersion(
22
+ pkg: Package,
23
+ releaseType: ReleaseType | null | undefined,
24
+ prereleaseId: string | null | undefined
25
+ ): string | null {
26
+ if (!releaseType) {
27
+ return null
28
+ }
29
+
30
+ if (!pkg.version) {
31
+ if (pkg.private) {
32
+ return null
33
+ }
34
+
35
+ throw new Error(
36
+ 'An initial version is required to compute the next version.'
37
+ )
38
+ }
39
+
40
+ const parsed = semver.parse(pkg.version)
41
+
42
+ if (!parsed) {
43
+ throw new Error(
44
+ `The version "${colors.bold(pkg.version)}" is not a valid SemVer.`
45
+ )
46
+ }
47
+
48
+ prereleaseId ||= undefined
49
+
50
+ let type: semver.ReleaseType = releaseType
51
+
52
+ if (prereleaseId) {
53
+ type = `pre${releaseType}` as const
54
+
55
+ if (parsed.prerelease.length > 0) {
56
+ if (
57
+ (releaseType === 'major' && parsed.minor === 0 && parsed.patch === 0) ||
58
+ (releaseType === 'minor' && parsed.patch === 0) ||
59
+ releaseType === 'patch'
60
+ ) {
61
+ type = 'prerelease'
62
+ }
63
+ }
64
+ }
65
+
66
+ return semver.inc(parsed, type, { loose: undefined }, prereleaseId, '0')
67
+ }
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Commit, CommitClassifier, ReleaseType } from '#types'
5
+ import { determineReleaseType } from './determineReleaseType'
6
+ import { pickPriorityReleaseType } from './pickPriorityReleaseType'
7
+
8
+ /**
9
+ * Derive the combined release type from a commit history collection.
10
+ *
11
+ * Processes commits sequentially, accumulating the highest-precedence release
12
+ * type found by evaluating each individual commit and comparing it against the
13
+ * current maximum via `pickPriorityReleaseType`.
14
+ *
15
+ * @param classifier Function mapping commits to release types
16
+ * @param history Array of parsed commits to evaluate
17
+ * @returns `ReleaseType` string, or `null` when no matching rule applies
18
+ */
19
+ export function deriveReleaseType(
20
+ classifier: CommitClassifier,
21
+ history: readonly Commit[]
22
+ ): ReleaseType | null {
23
+ let releaseType: ReleaseType | null = null
24
+
25
+ for (const commit of history) {
26
+ releaseType = pickPriorityReleaseType(
27
+ releaseType,
28
+ determineReleaseType(classifier, commit)
29
+ )
30
+ }
31
+
32
+ return releaseType
33
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { Commit, CommitClassifier, ReleaseType } from '#types'
5
+
6
+ /**
7
+ * Determine the release type for a single commit.
8
+ *
9
+ * Automatically returns `'major'` if the commit is marked as a breaking change.
10
+ * Otherwise, routes the commit through the custom `classifier` and ensures the
11
+ * resolved value is a valid semantic versioning release type.
12
+ *
13
+ * @param classifier Function mapping commits to release types
14
+ * @param commit Parsed commit to evaluate
15
+ * @returns `ReleaseType` string, or `null` when no matching rule applies
16
+ */
17
+ export function determineReleaseType(
18
+ classifier: CommitClassifier,
19
+ commit: Commit
20
+ ): ReleaseType | null {
21
+ if (commit.notes.length > 0) {
22
+ return 'major'
23
+ }
24
+
25
+ const type = classifier(commit)
26
+
27
+ switch (type) {
28
+ case 'major':
29
+ case 'minor':
30
+ case 'patch':
31
+ return type
32
+ }
33
+
34
+ return null
35
+ }
@@ -0,0 +1,8 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './bumpVersion'
5
+ export * from './deriveReleaseType'
6
+ export * from './determineReleaseType'
7
+ export * from './isStableVersion'
8
+ export * from './pickPriorityReleaseType'
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { colors } from 'consola/utils'
5
+ import semver from 'semver'
6
+
7
+ /**
8
+ * Determine whether a version string represents a stable SemVer release.
9
+ *
10
+ * A version is considered stable when:
11
+ * - It is a valid SemVer string.
12
+ * - Its major version is greater than 0 (not 0.x.x).
13
+ * - It contains no prerelease identifiers (no `-alpha`, `-beta`, `-rc`, etc.).
14
+ *
15
+ * Build metadata (the `+` suffix) does not affect stability.
16
+ *
17
+ * @param version Semantic version string to check
18
+ * @returns `true` if `version` is a stable SemVer; otherwise `false`
19
+ * @throws If `version` is not a valid SemVer string
20
+ */
21
+ export function isStableVersion(version: string): boolean {
22
+ const parsed = semver.parse(version)
23
+
24
+ if (!parsed) {
25
+ throw new Error(
26
+ `The version "${colors.bold(version)}" is not a valid SemVer.`
27
+ )
28
+ }
29
+
30
+ return parsed.major > 0 && parsed.prerelease.length === 0
31
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { ReleaseType } from '#types'
5
+
6
+ /**
7
+ * Determine the highest-priority release type between two candidates.
8
+ *
9
+ * Compares release types by precedence order: `major` > `minor` > `patch`.
10
+ * Returns the higher-precedence type, or `null` if neither applies.
11
+ *
12
+ * @param curr Current accumulated release type
13
+ * @param next Candidate release type to consider
14
+ * @returns Highest-precedence `ReleaseType`, or `null` if neither applies
15
+ */
16
+ export function pickPriorityReleaseType(
17
+ curr: ReleaseType | null | undefined,
18
+ next: ReleaseType | null | undefined
19
+ ): ReleaseType | null {
20
+ for (const type of ['major', 'minor', 'patch'] satisfies ReleaseType[]) {
21
+ if (curr === type || next === type) {
22
+ return type
23
+ }
24
+ }
25
+
26
+ return null
27
+ }