@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,100 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
interface Entry<V> {
|
|
5
|
+
value: V
|
|
6
|
+
timer: NodeJS.Timeout
|
|
7
|
+
ttl: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A simple time-to-live cache that automatically evicts entries after a TTL.
|
|
12
|
+
*
|
|
13
|
+
* - Entries are removed automatically when their TTL expires.
|
|
14
|
+
* - Reading an entry resets its TTL to the original value.
|
|
15
|
+
* - Setting an existing key clears its previous timer and resets the TTL.
|
|
16
|
+
*
|
|
17
|
+
* @template K Type of cache keys
|
|
18
|
+
* @template V Type of cache values
|
|
19
|
+
*/
|
|
20
|
+
export class TTLCache<K, V> {
|
|
21
|
+
private cache = new Map<K, Entry<V>>()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Store a value under `key` for `ttl` milliseconds.
|
|
25
|
+
*
|
|
26
|
+
* If the key already exists its previous timer is cleared and the TTL is reset.
|
|
27
|
+
*
|
|
28
|
+
* @param key Cache key
|
|
29
|
+
* @param value Value to store
|
|
30
|
+
* @param ttl Time to live in milliseconds (default: 60_000)
|
|
31
|
+
*/
|
|
32
|
+
set(key: K, value: V, ttl: number = 60_000): void {
|
|
33
|
+
const ref = this.cache.get(key)
|
|
34
|
+
|
|
35
|
+
if (ref) {
|
|
36
|
+
clearTimeout(ref.timer)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
this.cache.delete(key)
|
|
41
|
+
}, ttl).unref()
|
|
42
|
+
|
|
43
|
+
this.cache.set(key, { value, timer, ttl })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Retrieve the value for `key`, or `undefined` if not present.
|
|
48
|
+
*
|
|
49
|
+
* Reading an entry resets its TTL to the original `ttl` provided when it was set.
|
|
50
|
+
*
|
|
51
|
+
* @param key Cache key
|
|
52
|
+
* @returns The stored value or `undefined` if not present
|
|
53
|
+
*/
|
|
54
|
+
get(key: K): V | undefined {
|
|
55
|
+
const ref = this.cache.get(key)
|
|
56
|
+
|
|
57
|
+
if (!ref) return undefined
|
|
58
|
+
|
|
59
|
+
this.set(key, ref.value, ref.ttl)
|
|
60
|
+
|
|
61
|
+
return ref.value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check whether `key` exists in the cache.
|
|
66
|
+
*
|
|
67
|
+
* @param key Cache key
|
|
68
|
+
* @returns `true` if present; otherwise `false`
|
|
69
|
+
*/
|
|
70
|
+
has(key: K): boolean {
|
|
71
|
+
return this.cache.has(key)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Remove `key` from the cache and clear its eviction timer.
|
|
76
|
+
*
|
|
77
|
+
* @param key Cache key
|
|
78
|
+
* @returns `true` if the key was present and removed; otherwise `false`
|
|
79
|
+
*/
|
|
80
|
+
delete(key: K): boolean {
|
|
81
|
+
const ref = this.cache.get(key)
|
|
82
|
+
|
|
83
|
+
if (!ref) return false
|
|
84
|
+
|
|
85
|
+
clearTimeout(ref.timer)
|
|
86
|
+
|
|
87
|
+
return this.cache.delete(key)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear all entries and their timers.
|
|
92
|
+
*/
|
|
93
|
+
clear(): void {
|
|
94
|
+
for (const { timer } of this.cache.values()) {
|
|
95
|
+
clearTimeout(timer)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.cache.clear()
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/changelog.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { Package, ReleasePlugin } from '#types'
|
|
5
|
+
import { formatError } from '#util'
|
|
6
|
+
import {
|
|
7
|
+
buildChangelog,
|
|
8
|
+
discoverPackageTag,
|
|
9
|
+
resolvePackageTag,
|
|
10
|
+
writeChangelog,
|
|
11
|
+
} from '#workflow'
|
|
12
|
+
import { colors } from 'consola/utils'
|
|
13
|
+
import { version } from '../package.json'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Built-in plugin that resolves package tags, generates changelogs, and writes changelog files.
|
|
17
|
+
*
|
|
18
|
+
* Stages:
|
|
19
|
+
* - discover (enforce: post): resolve missing package tags from the repository.
|
|
20
|
+
* - build: generate changelogs for planned releases (optionally using the previous tag)
|
|
21
|
+
* and attach tag messages derived from changelogs.
|
|
22
|
+
* - write: persist changelog files for each plan (skipped in dry-run).
|
|
23
|
+
*/
|
|
24
|
+
const changelog = (): ReleasePlugin => ({
|
|
25
|
+
name: '@reelout/core/changelog',
|
|
26
|
+
version,
|
|
27
|
+
discover: {
|
|
28
|
+
enforce: 'post',
|
|
29
|
+
handler: async (context) => {
|
|
30
|
+
context.packages = await Promise.all(
|
|
31
|
+
context.packages.map(async (pkg): Promise<Package> => ({
|
|
32
|
+
...pkg,
|
|
33
|
+
tag: await resolvePackageTag(context.cwd, pkg),
|
|
34
|
+
}))
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
build: async (context) => {
|
|
39
|
+
context.plans = await Promise.all(
|
|
40
|
+
context.plans.map(async (plan) => {
|
|
41
|
+
plan = { ...plan }
|
|
42
|
+
|
|
43
|
+
if (!plan.release) {
|
|
44
|
+
return plan
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!plan.release.version) {
|
|
48
|
+
return plan
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
plan.release = { ...plan.release }
|
|
52
|
+
|
|
53
|
+
let changelog: string | null | undefined
|
|
54
|
+
|
|
55
|
+
let previousTag = plan.package.tag?.name
|
|
56
|
+
|
|
57
|
+
if (!previousTag) {
|
|
58
|
+
previousTag = (await discoverPackageTag(context.cwd, plan.package))
|
|
59
|
+
?.name
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
changelog = await buildChangelog(context, plan.history, {
|
|
64
|
+
packageDir: plan.package.dir,
|
|
65
|
+
changelogFile: plan.package.changelogFile,
|
|
66
|
+
version: plan.release.version,
|
|
67
|
+
previousTag,
|
|
68
|
+
currentTag: plan.release.tag?.name,
|
|
69
|
+
})
|
|
70
|
+
} catch (err) {
|
|
71
|
+
context.logger.warn(
|
|
72
|
+
colors.yellow(
|
|
73
|
+
`Changelog could not be generated for package at "${colors.bold(plan.package.dir)}": ${formatError(err)}`
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (changelog) {
|
|
79
|
+
plan.release.changelog = changelog
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return plan
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
},
|
|
86
|
+
write: async (context) => {
|
|
87
|
+
if (context.dryRun) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const plan of context.plans) {
|
|
92
|
+
await writeChangelog(context.cwd, plan)
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
export default changelog
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { ReleaseLifecycle } from '#types'
|
|
5
|
+
|
|
6
|
+
export const ALL_FILES = '**/*'
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_CHANGELOG_FILE = 'CHANGELOG.md'
|
|
9
|
+
|
|
10
|
+
export const GIT_TRAILER_RELEASE_AGENT = 'Release-Agent'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 1. Primitive Building Blocks
|
|
14
|
+
*
|
|
15
|
+
* Base regular expression patterns for wildcards, alphanumeric strings with hyphens,
|
|
16
|
+
* and standard non-negative integers.
|
|
17
|
+
*/
|
|
18
|
+
const WILDCARD = '\\*'
|
|
19
|
+
const ALPHANUM_DASH = '[0-9a-zA-Z-]+'
|
|
20
|
+
const NUMERIC_IDENTIFIER = '0|[1-9]\\d*'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 2. Package Name & Prefix Constructs
|
|
24
|
+
*
|
|
25
|
+
* Matches package name prefixes (unscoped or scoped) or a simple `v` prefix.
|
|
26
|
+
*
|
|
27
|
+
* Capture Groups:
|
|
28
|
+
* - Group 1: Package name (e.g., `my-package` or `@scope/my-package`)
|
|
29
|
+
*/
|
|
30
|
+
const PKG_NAME_PART = `(?:${WILDCARD}|${ALPHANUM_DASH})`
|
|
31
|
+
const CAPTURED_PACKAGE_NAME = `(${PKG_NAME_PART}|@${PKG_NAME_PART}\\/${PKG_NAME_PART})`
|
|
32
|
+
const OPTIONAL_VERSION_PREFIX = `(?:v|${CAPTURED_PACKAGE_NAME}@)`
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 3. Core SemVer Components
|
|
36
|
+
*
|
|
37
|
+
* Matches standard `MAJOR.MINOR.PATCH` versions or wildcard variants (e.g., `1.0.0`, `*.*.*`).
|
|
38
|
+
*
|
|
39
|
+
* Capture Groups:
|
|
40
|
+
* - Group 3: Major version
|
|
41
|
+
* - Group 4: Minor version
|
|
42
|
+
* - Group 5: Patch version
|
|
43
|
+
*/
|
|
44
|
+
const CAPTURED_VERSION_DIGIT = `(${WILDCARD}|${NUMERIC_IDENTIFIER})`
|
|
45
|
+
const SEMVER_CORE = `${CAPTURED_VERSION_DIGIT}\\.${CAPTURED_VERSION_DIGIT}\\.${CAPTURED_VERSION_DIGIT}`
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 4. Prerelease & Build Metadata
|
|
49
|
+
*
|
|
50
|
+
* Matches optional prerelease identifiers (e.g., `-alpha.1`) and build metadata (e.g., `+build123`).
|
|
51
|
+
*
|
|
52
|
+
* Capture Groups:
|
|
53
|
+
* - Group 6: Complete prerelease string (without `-`)
|
|
54
|
+
* - Group 7: Non-numeric prerelease token
|
|
55
|
+
* - Group 8: Complete build metadata string (without `+`)
|
|
56
|
+
*/
|
|
57
|
+
const CAPTURED_NON_NUMERIC_PRERELEASE_ID = `(\\d*[a-zA-Z-][0-9a-zA-Z-]*)`
|
|
58
|
+
const PRERELEASE_IDENTIFIER = `(?:${WILDCARD}|${NUMERIC_IDENTIFIER}|${CAPTURED_NON_NUMERIC_PRERELEASE_ID})`
|
|
59
|
+
const OPTIONAL_PRERELEASE_SECTION = `(?:-(${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*))`
|
|
60
|
+
const OPTIONAL_BUILD_SECTION = `(?:\\+(${ALPHANUM_DASH}(?:\\.${ALPHANUM_DASH})*))`
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 5. Assembled SemVer Pattern
|
|
64
|
+
*
|
|
65
|
+
* Combines core SemVer components with optional prerelease identifiers and build metadata.
|
|
66
|
+
*
|
|
67
|
+
* Capture Groups:
|
|
68
|
+
* - Group 2: Full SemVer version string (excluding package/prefix)
|
|
69
|
+
*/
|
|
70
|
+
const CAPTURED_SEMVER_VERSION = `(${SEMVER_CORE}${OPTIONAL_PRERELEASE_SECTION}?${OPTIONAL_BUILD_SECTION}?)`
|
|
71
|
+
const FULL_SEMVER_PATTERN = `${OPTIONAL_VERSION_PREFIX}?${CAPTURED_SEMVER_VERSION}`
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Regular expression to match and capture Semantic Versioning (SemVer) strings,
|
|
75
|
+
* including optional package prefixes (e.g., `@scope/pkg@`) and `v` prefixes.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* SEMVER_REGEX.test('1.0.0') // true
|
|
80
|
+
* SEMVER_REGEX.test('v1.2.3-beta.1+sha.1234') // true
|
|
81
|
+
* SEMVER_REGEX.test('@scope/package@1.0.0') // true
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @see {@link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string SemVer Specification}
|
|
85
|
+
*/
|
|
86
|
+
export const SEMVER_REGEX = new RegExp(FULL_SEMVER_PATTERN)
|
|
87
|
+
|
|
88
|
+
export const RELEASE_LIFECYCLE_HOOK_LIST = [
|
|
89
|
+
'configure',
|
|
90
|
+
'discover',
|
|
91
|
+
'analyze',
|
|
92
|
+
'build',
|
|
93
|
+
'write',
|
|
94
|
+
'commit',
|
|
95
|
+
'publish',
|
|
96
|
+
'finalize',
|
|
97
|
+
] as const satisfies ReleaseLifecycle[]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
export * from './const'
|
|
5
|
+
export * from './isSemVerGitTag'
|
|
6
|
+
export * from './normalizeCommitFilter'
|
|
7
|
+
export * from './normalizeReleaseRule'
|
|
8
|
+
export * from './normalizeWorkspace'
|
|
9
|
+
export * from './resolvePlugin'
|
|
10
|
+
export * from './resolveReleaseTarget'
|
|
11
|
+
export * from './sanitizePrereleaseLabel'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { SEMVER_REGEX } from './const'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether a tag string is a valid SemVer-style Git tag.
|
|
8
|
+
*
|
|
9
|
+
* Anchors `SEMVER_REGEX.source` for full-string matching and applies the
|
|
10
|
+
* repository's Git-tag acceptance rules.
|
|
11
|
+
*
|
|
12
|
+
* @param tag Tag string to validate
|
|
13
|
+
* @returns `true` if `tag` is accepted as a SemVer-style Git tag; otherwise `false`
|
|
14
|
+
*/
|
|
15
|
+
export function isSemVerGitTag(tag: string): boolean {
|
|
16
|
+
const match = new RegExp(`^${SEMVER_REGEX.source}$`).exec(tag)
|
|
17
|
+
|
|
18
|
+
if (!match) return false
|
|
19
|
+
|
|
20
|
+
const isScopedPackage = Boolean(match[1])
|
|
21
|
+
|
|
22
|
+
const hasVersionPrefix = tag.startsWith('v')
|
|
23
|
+
|
|
24
|
+
return isScopedPackage || hasVersionPrefix
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { CommitClassifier, ReleaseConfig, ReleaseType } from '#types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a commit classifier into a consistent function.
|
|
8
|
+
*
|
|
9
|
+
* Accepts `null`/`undefined` (returns no-op classifier), a function classifier,
|
|
10
|
+
* or a config object mapping commit types to release types. Returns a classifier
|
|
11
|
+
* function that maps commits to their corresponding release types.
|
|
12
|
+
*
|
|
13
|
+
* @param classifier Commit classifier config, function, or falsy value
|
|
14
|
+
* @returns Normalized classifier function
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeCommitFilter(
|
|
17
|
+
classifier: ReleaseConfig['classifier'] | null | undefined
|
|
18
|
+
): CommitClassifier {
|
|
19
|
+
if (classifier == null) {
|
|
20
|
+
return () => null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof classifier === 'function') {
|
|
24
|
+
return classifier
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const map = new Map(
|
|
28
|
+
Object.entries(classifier).flatMap(([releaseType, commitTypes]) => {
|
|
29
|
+
return commitTypes.map((commitType) => [
|
|
30
|
+
commitType,
|
|
31
|
+
releaseType as ReleaseType,
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (commit) => (commit.type ? map.get(commit.type) : null)
|
|
37
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { ReleaseContext, ReleaseRule } from '#types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a release rule for the given ref.
|
|
8
|
+
*
|
|
9
|
+
* If `rule` is `true`, derive the rule from the ref (branch or tag).
|
|
10
|
+
* Falsy inputs return an empty rule.
|
|
11
|
+
*
|
|
12
|
+
* @param ref Current repository ref (branch or tag)
|
|
13
|
+
* @param rule Rule input to normalize
|
|
14
|
+
* @returns Normalized `ReleaseRule` object
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeReleaseRule(
|
|
17
|
+
ref: ReleaseContext['ref'],
|
|
18
|
+
rule: boolean | string | Partial<ReleaseRule> | null | undefined
|
|
19
|
+
): ReleaseRule {
|
|
20
|
+
if (rule === true) {
|
|
21
|
+
switch (ref.type) {
|
|
22
|
+
case 'branch':
|
|
23
|
+
rule = ref.name.split('/').at(0)
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!rule) {
|
|
29
|
+
rule = {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof rule === 'string') {
|
|
33
|
+
rule = { prereleaseLabel: rule, releaseChannel: rule }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
type: rule.type || ref.type,
|
|
38
|
+
pattern: rule.pattern || ref.name,
|
|
39
|
+
prereleaseLabel: rule.prereleaseLabel,
|
|
40
|
+
releaseChannel: rule.releaseChannel,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { Workspace } from '#types'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalize and validate workspace configuration for a repository.
|
|
9
|
+
*
|
|
10
|
+
* Resolves the workspace entry directory relative to `cwd` and normalizes
|
|
11
|
+
* the path representation. Returns a workspace object with normalized `entry`
|
|
12
|
+
* and optional workspace members.
|
|
13
|
+
*
|
|
14
|
+
* @param cwd Repository working directory
|
|
15
|
+
* @param workspace Partial workspace configuration (optional)
|
|
16
|
+
* @returns Normalized workspace object
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeWorkspace(
|
|
19
|
+
cwd: string,
|
|
20
|
+
workspace?: {
|
|
21
|
+
entry?: string | null | undefined
|
|
22
|
+
members?: readonly string[] | null | undefined
|
|
23
|
+
}
|
|
24
|
+
): Workspace {
|
|
25
|
+
let entry = workspace?.entry || '.'
|
|
26
|
+
const members = workspace?.members ?? null
|
|
27
|
+
|
|
28
|
+
entry = path.normalize(path.relative(cwd, path.resolve(cwd, entry)))
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
entry,
|
|
32
|
+
members,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { ReleasePlugin } from '#types'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a plugin reference to a `ReleasePlugin` instance.
|
|
10
|
+
*
|
|
11
|
+
* Accepts a module specifier, a loaded plugin object, or an initializer
|
|
12
|
+
* function (optionally with options) and normalizes it to a `ReleasePlugin`,
|
|
13
|
+
* or `null` when resolution fails.
|
|
14
|
+
*
|
|
15
|
+
* @param cwd Base directory used to resolve module specifiers
|
|
16
|
+
* @param plugin Plugin reference (module path, function, object, or [path, opts])
|
|
17
|
+
* @returns Resolved `ReleasePlugin` or `null` when not resolvable
|
|
18
|
+
*/
|
|
19
|
+
export async function resolvePlugin(
|
|
20
|
+
cwd: string,
|
|
21
|
+
plugin: unknown
|
|
22
|
+
): Promise<ReleasePlugin | null> {
|
|
23
|
+
let options: unknown
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(plugin)) {
|
|
26
|
+
;[plugin, options] = plugin
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof plugin === 'string') {
|
|
30
|
+
const require = createRequire(pathToFileURL(cwd))
|
|
31
|
+
plugin = await import(require.resolve(plugin))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
plugin != null &&
|
|
36
|
+
typeof plugin === 'object' &&
|
|
37
|
+
Object.hasOwn(plugin, 'default')
|
|
38
|
+
) {
|
|
39
|
+
plugin = (plugin as { default: unknown }).default
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof plugin === 'function') {
|
|
43
|
+
plugin = plugin(options)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
plugin != null &&
|
|
48
|
+
typeof plugin === 'object' &&
|
|
49
|
+
Object.hasOwn(plugin, 'name') &&
|
|
50
|
+
typeof (plugin as ReleasePlugin).name === 'string'
|
|
51
|
+
) {
|
|
52
|
+
return plugin as ReleasePlugin
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { ReleaseContext, ReleaseRule } from '#types'
|
|
5
|
+
import { interpolate, isMatch } from '#util'
|
|
6
|
+
import { sanitizePrereleaseLabel } from './sanitizePrereleaseLabel'
|
|
7
|
+
|
|
8
|
+
export type ResolveReleaseTargetOptions = Record<
|
|
9
|
+
Extract<keyof ReleaseRule, 'prereleaseLabel' | 'releaseChannel'>,
|
|
10
|
+
string | boolean | undefined
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve prerelease label and release channel for a ref.
|
|
15
|
+
*
|
|
16
|
+
* Resolution priority for each key:
|
|
17
|
+
* 1. Explicit `options` (string values are used verbatim; `false` disables the key).
|
|
18
|
+
* 2. First matching `rule` (rules are checked in order).
|
|
19
|
+
* 3. Fallback: derive and sanitize a label from `ref.name`.
|
|
20
|
+
*
|
|
21
|
+
* Resolved string values are interpolated with `data` before being returned.
|
|
22
|
+
*
|
|
23
|
+
* @param ref Reference context containing `type` and `name`
|
|
24
|
+
* @param rules Ordered list of release rules to match against
|
|
25
|
+
* @param options User overrides for `prereleaseLabel` and `releaseChannel`
|
|
26
|
+
* @param data Values used to interpolate resolved template strings
|
|
27
|
+
* @returns Partial release rule with resolved `prereleaseLabel` and/or `releaseChannel`
|
|
28
|
+
*/
|
|
29
|
+
export function resolveReleaseTarget(
|
|
30
|
+
ref: ReleaseContext['ref'],
|
|
31
|
+
rules: readonly ReleaseRule[],
|
|
32
|
+
options: ResolveReleaseTargetOptions,
|
|
33
|
+
data: Record<string, unknown>
|
|
34
|
+
): Partial<ReleaseRule> {
|
|
35
|
+
const matchedRule = rules.find(
|
|
36
|
+
(rule) => rule.type === ref.type && isMatch(ref.name, rule.pattern)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const resolve = (key: keyof ResolveReleaseTargetOptions) => {
|
|
40
|
+
const value1 = options[key]
|
|
41
|
+
|
|
42
|
+
if (value1 === false) {
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (value1 && typeof value1 === 'string') {
|
|
47
|
+
return value1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const value2 = matchedRule?.[key] || value1
|
|
51
|
+
|
|
52
|
+
if (!value2) {
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof value2 === 'string') {
|
|
57
|
+
return value2
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sanitizePrereleaseLabel(ref.name)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prereleaseLabel = resolve('prereleaseLabel')
|
|
64
|
+
|
|
65
|
+
const releaseChannel = resolve('releaseChannel')
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
prereleaseLabel: prereleaseLabel && interpolate(prereleaseLabel, data),
|
|
69
|
+
releaseChannel: releaseChannel && interpolate(releaseChannel, data),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize a string into a safe, semver-compliant prerelease label.
|
|
6
|
+
*
|
|
7
|
+
* Replaces sequential non-alphanumeric characters or consecutive hyphens
|
|
8
|
+
* with a single hyphen, and strips leading or trailing hyphens.
|
|
9
|
+
*
|
|
10
|
+
* @param label Raw string to sanitize
|
|
11
|
+
* @returns Safe string for a prerelease label
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizePrereleaseLabel(label: string): string {
|
|
14
|
+
return label
|
|
15
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
16
|
+
.replace(/-+/g, '-')
|
|
17
|
+
.replace(/^-+|-+$/g, '')
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the current Git branch name.
|
|
8
|
+
*
|
|
9
|
+
* Uses `execGit` to run `git symbolic-ref -q --short HEAD` in the provided
|
|
10
|
+
* working directory and trims the result. Returns an empty string when the
|
|
11
|
+
* repository is in a detached HEAD state.
|
|
12
|
+
*
|
|
13
|
+
* @param cwd The working directory to run the Git command in
|
|
14
|
+
* @returns The current branch name as a string
|
|
15
|
+
*/
|
|
16
|
+
export async function getCurrentGitBranch(cwd: string): Promise<string> {
|
|
17
|
+
return (await execGit(cwd, ['symbolic-ref', '-q', '--short', 'HEAD'])).trim()
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execGit } from '#util'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stage all changes and create a git commit with the specified message.
|
|
8
|
+
*
|
|
9
|
+
* Validates the commit message and working directory status before staging
|
|
10
|
+
* all changes and executing the commit command.
|
|
11
|
+
*
|
|
12
|
+
* @param cwd The target directory path for executing git commands
|
|
13
|
+
* @param message The message to associate with the commit
|
|
14
|
+
* @returns A promise that resolves when the operation completes
|
|
15
|
+
*/
|
|
16
|
+
export async function createGitCommit(
|
|
17
|
+
cwd: string,
|
|
18
|
+
message: string | null | undefined
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (!message) return
|
|
21
|
+
|
|
22
|
+
await execGit(cwd, ['add', '.'])
|
|
23
|
+
|
|
24
|
+
const stdout = (await execGit(cwd, ['status', '--porcelain'])).trim()
|
|
25
|
+
|
|
26
|
+
if (!stdout) return
|
|
27
|
+
|
|
28
|
+
await execGit(cwd, ['commit', '--message', message])
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
const ALPHANUMERIC = '[a-zA-Z0-9]'
|
|
5
|
+
const SEPARATOR = '[-_]'
|
|
6
|
+
const USERNAME = `${ALPHANUMERIC}+(?:${SEPARATOR}+${ALPHANUMERIC}+)*`
|
|
7
|
+
const EMAIL_LOCAL = '[a-zA-Z0-9._%+-]'
|
|
8
|
+
const DOMAIN_SUFFIX = '[a-zA-Z0-9.-]*\\.[a-zA-Z]{2,}'
|
|
9
|
+
const SCOPED_PACKAGE_SPEC_SUFFIX = `/${USERNAME}@[0-9]+\\.[0-9]+\\.[0-9]+`
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract unique mention handles from the input text string.
|
|
13
|
+
*
|
|
14
|
+
* Evaluates `text` against boundary rules to isolate valid handles while
|
|
15
|
+
* excluding email addresses, domain names, and scoped package specs, then
|
|
16
|
+
* de-duplicates the resulting handles.
|
|
17
|
+
*
|
|
18
|
+
* @param text The input string to parse for mentions
|
|
19
|
+
* @returns An array of unique mention handles
|
|
20
|
+
*/
|
|
21
|
+
export function extractMentions(text: string): string[] {
|
|
22
|
+
if (!text) return []
|
|
23
|
+
|
|
24
|
+
const regex = new RegExp(
|
|
25
|
+
`(?<!${EMAIL_LOCAL})@(${USERNAME})\\b(?!${SEPARATOR}|${DOMAIN_SUFFIX}|${SCOPED_PACKAGE_SPEC_SUFFIX})`,
|
|
26
|
+
'g'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const matches = text.matchAll(regex)
|
|
30
|
+
|
|
31
|
+
const mentions = new Set(matches.map((match) => match[1] as string))
|
|
32
|
+
|
|
33
|
+
return Array.from(mentions)
|
|
34
|
+
}
|