@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.
- package/dist/analyzer.js +38 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/builder.js +52 -0
- package/dist/builder.js.map +1 -0
- package/dist/changelog.js +58 -0
- package/dist/changelog.js.map +1 -0
- package/dist/formatError-BXNdyRfu.js +23 -0
- package/dist/formatError-BXNdyRfu.js.map +1 -0
- package/dist/index.js +175 -1
- package/dist/index.js.map +1 -0
- package/dist/types-la_KkjCS.js +1 -0
- package/dist/types.js +2 -0
- package/dist/workflow-BoVmbvH6.js +2342 -0
- package/dist/workflow-BoVmbvH6.js.map +1 -0
- package/package.json +27 -3
- package/src/analyzer.ts +63 -0
- package/src/builder.ts +104 -0
- package/src/cache/TTLCache.ts +100 -0
- package/src/cache/index.ts +4 -0
- package/src/changelog.ts +97 -0
- package/src/config/const.ts +97 -0
- package/src/config/index.ts +11 -0
- package/src/config/isSemVerGitTag.ts +25 -0
- package/src/config/normalizeCommitFilter.ts +37 -0
- package/src/config/normalizeReleaseRule.ts +42 -0
- package/src/config/normalizeWorkspace.ts +34 -0
- package/src/config/resolvePlugin.ts +56 -0
- package/src/config/resolveReleaseTarget.ts +71 -0
- package/src/config/sanitizePrereleaseLabel.ts +18 -0
- package/src/git/branch/getCurrentGitBranch.ts +18 -0
- package/src/git/branch/index.ts +4 -0
- package/src/git/commit/createGitCommit.ts +29 -0
- package/src/git/commit/extractMentions.ts +34 -0
- package/src/git/commit/getCurrentGitCommit.ts +14 -0
- package/src/git/commit/getParsedCommit.ts +53 -0
- package/src/git/commit/index.ts +9 -0
- package/src/git/commit/parseCommit.ts +99 -0
- package/src/git/commit/parseGitPrettyFormat.ts +76 -0
- package/src/git/history/countGitHistory.ts +28 -0
- package/src/git/history/expandGitHistory.ts +108 -0
- package/src/git/history/index.ts +9 -0
- package/src/git/history/mergeGitHistory.ts +76 -0
- package/src/git/history/restoreFullGitHistory.ts +19 -0
- package/src/git/history/showGitHistory.ts +80 -0
- package/src/git/history/showParsedGitHistory.ts +30 -0
- package/src/git/index.ts +9 -0
- package/src/git/repository/ensureGitUserConfig.ts +31 -0
- package/src/git/repository/getGitConfigValue.ts +27 -0
- package/src/git/repository/getGitRemote.ts +20 -0
- package/src/git/repository/index.ts +10 -0
- package/src/git/repository/isGitShallowRepository.ts +21 -0
- package/src/git/repository/pushGitChanges.ts +25 -0
- package/src/git/repository/resolveGitRepository.ts +40 -0
- package/src/git/repository/withShallowLock.ts +62 -0
- package/src/git/revision/detectGitRelationship.ts +43 -0
- package/src/git/revision/index.ts +7 -0
- package/src/git/revision/isGitAncestor.ts +33 -0
- package/src/git/revision/readGitFile.ts +29 -0
- package/src/git/revision/resolveGitRevision.ts +23 -0
- package/src/git/tag/createGitTag.ts +25 -0
- package/src/git/tag/deleteGitTag.ts +17 -0
- package/src/git/tag/ensureGitTag.ts +63 -0
- package/src/git/tag/generateGitTagName.ts +43 -0
- package/src/git/tag/getCurrentGitTag.ts +23 -0
- package/src/git/tag/getGitTagMessage.ts +35 -0
- package/src/git/tag/getLatestGitTag.ts +22 -0
- package/src/git/tag/index.ts +13 -0
- package/src/git/tag/isGitTagConnected.ts +33 -0
- package/src/git/tag/isGitTagPointed.ts +38 -0
- package/src/git/tag/isGitTagPresent.ts +17 -0
- package/src/index.ts +8 -2
- package/src/types.ts +217 -0
- package/src/util/buildTrailerPattern.ts +20 -0
- package/src/util/compact.ts +14 -0
- package/src/util/execCommand.ts +130 -0
- package/src/util/execGit.ts +18 -0
- package/src/util/formatError.ts +21 -0
- package/src/util/formatFile.ts +41 -0
- package/src/util/index.ts +16 -0
- package/src/util/interpolate.ts +141 -0
- package/src/util/isCI.ts +23 -0
- package/src/util/isDefined.ts +12 -0
- package/src/util/isMatch.ts +43 -0
- package/src/util/readJson.ts +47 -0
- package/src/util/unique.ts +26 -0
- package/src/util/writeJson.ts +28 -0
- package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
- package/src/workflow/analysis/index.ts +9 -0
- package/src/workflow/analysis/isCommitForPackage.ts +23 -0
- package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
- package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
- package/src/workflow/analysis/sliceUnreleased.ts +36 -0
- package/src/workflow/analysis/streamPackageCommits.ts +34 -0
- package/src/workflow/changelog/buildChangelog.ts +79 -0
- package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
- package/src/workflow/changelog/extractLatestVersion.ts +22 -0
- package/src/workflow/changelog/extractPlainText.ts +38 -0
- package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
- package/src/workflow/changelog/extractSemVer.ts +33 -0
- package/src/workflow/changelog/index.ts +12 -0
- package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
- package/src/workflow/changelog/prependChangelog.ts +59 -0
- package/src/workflow/changelog/writeChangelog.ts +27 -0
- package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
- package/src/workflow/committing/index.ts +5 -0
- package/src/workflow/committing/isReleaseCommit.ts +52 -0
- package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
- package/src/workflow/discovery/discoverPackageTag.ts +98 -0
- package/src/workflow/discovery/index.ts +9 -0
- package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
- package/src/workflow/discovery/resolvePackageTag.ts +45 -0
- package/src/workflow/discovery/sortPackages.ts +73 -0
- package/src/workflow/discovery/verifyPackageTag.ts +43 -0
- package/src/workflow/index.ts +10 -0
- package/src/workflow/report/generateReleaseReport.ts +76 -0
- package/src/workflow/report/index.ts +5 -0
- package/src/workflow/report/printReleaseReport.ts +97 -0
- package/src/workflow/runner/index.ts +4 -0
- package/src/workflow/runner/runWorkflow.ts +120 -0
- package/src/workflow/versioning/bumpVersion.ts +67 -0
- package/src/workflow/versioning/deriveReleaseType.ts +33 -0
- package/src/workflow/versioning/determineReleaseType.ts +35 -0
- package/src/workflow/versioning/index.ts +8 -0
- package/src/workflow/versioning/isStableVersion.ts +31 -0
- 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,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,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,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
|
+
}
|