@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,20 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Get the URL for the named remote (defaults to `origin`).
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @param remoteName Remote name (defaults to `'origin'`)
11
+ * @returns Remote URL string or `null` when unavailable
12
+ */
13
+ export function getGitRemote(
14
+ cwd: string,
15
+ remoteName = 'origin'
16
+ ): Promise<string | null> {
17
+ return execGit(cwd, ['remote', 'get-url', remoteName])
18
+ .then((stdout) => stdout.trim())
19
+ .catch(() => null)
20
+ }
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './ensureGitUserConfig'
5
+ export * from './getGitConfigValue'
6
+ export * from './getGitRemote'
7
+ export * from './isGitShallowRepository'
8
+ export * from './pushGitChanges'
9
+ export * from './resolveGitRepository'
10
+ export * from './withShallowLock'
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Determine whether the repository at `cwd` is a shallow clone.
8
+ *
9
+ * Runs `git rev-parse --is-shallow-repository` to detect shallow clones.
10
+ * Useful before operations that require full history.
11
+ *
12
+ * @param cwd Repository working directory
13
+ * @returns `true` if shallow, `false` if full clone, or `null` on error
14
+ */
15
+ export async function isGitShallowRepository(cwd: string): Promise<boolean> {
16
+ const stdout = (
17
+ await execGit(cwd, ['rev-parse', '--is-shallow-repository'])
18
+ ).trim()
19
+
20
+ return stdout === 'true'
21
+ }
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { compact, execGit } from '#util'
5
+
6
+ /**
7
+ * Push a branch and optional tags to the `origin` remote using an atomic push.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @param branch Branch name to push
11
+ * @param additionalTags Optional list of tag names to push alongside the branch
12
+ * @returns Promise that resolves when the push completes
13
+ */
14
+ export async function pushGitChanges(
15
+ cwd: string,
16
+ branch: string | null,
17
+ ...additionalTags: readonly string[]
18
+ ): Promise<void> {
19
+ await execGit(cwd, [
20
+ 'push',
21
+ 'origin',
22
+ ...compact([branch, ...additionalTags]),
23
+ '--atomic',
24
+ ])
25
+ }
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import type { GitRepository } from '#types'
5
+ import GitHost from 'hosted-git-info'
6
+ import { getGitRemote } from './getGitRemote'
7
+
8
+ /**
9
+ * Resolve Git repository host, owner, and project metadata from the `origin` remote URL.
10
+ *
11
+ * @param cwd Repository working directory
12
+ * @returns Promise that resolves to a `GitRepository` object, or `null` when unavailable
13
+ */
14
+ export async function resolveGitRepository(
15
+ cwd: string
16
+ ): Promise<GitRepository | null> {
17
+ const origin = await getGitRemote(cwd)
18
+
19
+ if (!origin) {
20
+ return null
21
+ }
22
+
23
+ const info = GitHost.fromUrl(origin)
24
+
25
+ if (
26
+ info &&
27
+ typeof info.type === 'string' &&
28
+ typeof info.user === 'string' &&
29
+ typeof info.project === 'string'
30
+ ) {
31
+ return {
32
+ host: info.type,
33
+ owner: info.user,
34
+ repo: info.project,
35
+ url: info.browse(),
36
+ }
37
+ }
38
+
39
+ return null
40
+ }
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { AsyncLocalStorage } from 'node:async_hooks'
5
+ import { isGitShallowRepository } from './isGitShallowRepository'
6
+
7
+ const lockQueue = new Map<string, Promise<void>>()
8
+
9
+ const context = new AsyncLocalStorage<Set<string>>()
10
+
11
+ /**
12
+ * Serialize access to a shallow Git repository while running a callback.
13
+ *
14
+ * If `cwd` is a shallow clone, callers from different async contexts are
15
+ * executed one at a time; nested calls within the same async context run
16
+ * immediately without waiting. If the repository becomes non-shallow before
17
+ * a queued turn runs, downstream waiters are released early.
18
+ *
19
+ * @param cwd Repository working directory path string
20
+ * @param callback Function to execute while the lock is held
21
+ * @returns Promise resolving to the result of `callback`
22
+ */
23
+ export async function withShallowLock<T>(
24
+ cwd: string,
25
+ callback: () => T | Promise<T>
26
+ ): Promise<T> {
27
+ if (!(await isGitShallowRepository(cwd))) {
28
+ return callback()
29
+ }
30
+
31
+ const active = context.getStore()
32
+
33
+ if (active?.has(cwd)) {
34
+ return callback()
35
+ }
36
+
37
+ const { promise, resolve } = Promise.withResolvers<void>()
38
+ const previous = lockQueue.get(cwd) ?? Promise.resolve()
39
+ const queued = previous.then(() => promise)
40
+
41
+ lockQueue.set(cwd, queued)
42
+
43
+ // Wait until previous tasks complete.
44
+ await previous
45
+
46
+ // If an earlier task made the repo non-shallow, release downstream waiters now.
47
+ if (!(await isGitShallowRepository(cwd))) {
48
+ resolve()
49
+ }
50
+
51
+ try {
52
+ return await context.run(new Set(active).add(cwd), callback)
53
+ } finally {
54
+ // Ensure downstream waiters are unblocked even if callback throws.
55
+ resolve()
56
+
57
+ // Remove the queue entry only if no newer task has replaced it.
58
+ if (lockQueue.get(cwd) === queued) {
59
+ lockQueue.delete(cwd)
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { isGitAncestor } from './isGitAncestor'
5
+
6
+ export const GIT_REL_FLAGS = {
7
+ NONE: 0,
8
+ IS_ANCESTOR: 1,
9
+ IS_DESCENDANT: 2,
10
+ IS_EQUAL: 3,
11
+ } as const
12
+
13
+ export type GitRelFlags = (typeof GIT_REL_FLAGS)[keyof typeof GIT_REL_FLAGS]
14
+
15
+ /**
16
+ * Determine the relationship between two git refs.
17
+ *
18
+ * Runs `git merge-base --is-ancestor` in both directions and returns a
19
+ * numeric bitmask using `GIT_REL_FLAGS`. A successful check (exit code 0)
20
+ * indicates an ancestor relationship; non-zero exit is treated as "not ancestor".
21
+ *
22
+ * @param cwd Repository working directory
23
+ * @param refA First ref (candidate ancestor)
24
+ * @param refB Second ref (candidate descendant)
25
+ * @returns Bitmask of type `GitRelFlags`
26
+ */
27
+ export async function detectGitRelationship(
28
+ cwd: string,
29
+ refA: string,
30
+ refB: string
31
+ ): Promise<GitRelFlags> {
32
+ let flags = GIT_REL_FLAGS.NONE
33
+
34
+ const [aIsAncestor, bIsAncestor] = await Promise.all([
35
+ isGitAncestor(cwd, refA, refB),
36
+ isGitAncestor(cwd, refB, refA),
37
+ ])
38
+
39
+ if (aIsAncestor) flags |= GIT_REL_FLAGS.IS_ANCESTOR
40
+ if (bIsAncestor) flags |= GIT_REL_FLAGS.IS_DESCENDANT
41
+
42
+ return flags as GitRelFlags
43
+ }
@@ -0,0 +1,7 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './detectGitRelationship'
5
+ export * from './isGitAncestor'
6
+ export * from './readGitFile'
7
+ export * from './resolveGitRevision'
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Determine whether `ancestor` is an ancestor of `descendant`.
8
+ *
9
+ * Runs `git merge-base --is-ancestor <ancestor> <descendant>` in the repository at `cwd`.
10
+ * Resolves to `true` if `ancestor` is an ancestor, `false` if not.
11
+ * Rethrows errors for invalid refs or other failures.
12
+ *
13
+ * @param cwd Repository working directory
14
+ * @param ancestor Candidate ancestor ref
15
+ * @param descendant Candidate descendant ref
16
+ * @returns Resolves to `true` if `ancestor` is an ancestor of `descendant`, `false` otherwise
17
+ * @throws If a ref is invalid or the Git command fails
18
+ */
19
+ export function isGitAncestor(
20
+ cwd: string,
21
+ ancestor: string,
22
+ descendant: string
23
+ ): Promise<boolean> {
24
+ return execGit(cwd, ['merge-base', '--is-ancestor', ancestor, descendant])
25
+ .then(() => true)
26
+ .catch((err) => {
27
+ if ((err as { code?: number })?.code === 1) {
28
+ return false
29
+ }
30
+
31
+ throw err
32
+ })
33
+ }
@@ -0,0 +1,29 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Read a file's contents from a specific Git revision.
8
+ *
9
+ * Runs `git show <ref>:<filePath>` and returns the command output as a
10
+ * string. If the file does not exist at the given revision or the Git
11
+ * command fails for any reason, the function returns `null`.
12
+ *
13
+ * The returned string is the raw file contents as emitted by Git; callers
14
+ * that need binary-safe handling should decode or process the bytes
15
+ * accordingly. This function intentionally swallows Git errors and signals
16
+ * absence via a `null` result rather than throwing.
17
+ *
18
+ * @param cwd Repository working directory
19
+ * @param ref Commit-ish reference (commit SHA, tag, or branch) to read from
20
+ * @param filePath Path to the file relative to the repository root
21
+ * @returns File contents as a string, or `null` if the file is unavailable at the ref
22
+ */
23
+ export function readGitFile(
24
+ cwd: string,
25
+ ref: string,
26
+ filePath: string
27
+ ): Promise<string | null> {
28
+ return execGit(cwd, ['show', `${ref}:${filePath}`]).catch(() => null)
29
+ }
@@ -0,0 +1,23 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Resolve a Git revision or commit-ish expression to its full commit hash.
8
+ *
9
+ * Runs `git rev-parse --verify <revision>^{commit}` under the hood. Peels annotated
10
+ * tags, branches, short SHAs, and relative expressions down to a verified commit object.
11
+ *
12
+ * @param cwd The repository working directory
13
+ * @param revision The Git revision or expression to resolve
14
+ * @returns A promise that resolves to the full, trimmed commit SHA-1 string
15
+ */
16
+ export async function resolveGitRevision(
17
+ cwd: string,
18
+ revision: string
19
+ ): Promise<string> {
20
+ return (
21
+ await execGit(cwd, ['rev-parse', '--verify', `${revision}^{commit}`])
22
+ ).trim()
23
+ }
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Create a git tag in the repository at `cwd`.
8
+ *
9
+ * Creates an annotated tag when `message` is provided.
10
+ *
11
+ * @param cwd Repository working directory
12
+ * @param tag Tag name to create
13
+ * @param message Optional tag message (creates an annotated tag when present)
14
+ * @returns Promise that resolves when the tag is created
15
+ */
16
+ export async function createGitTag(
17
+ cwd: string,
18
+ tag: string,
19
+ message?: string
20
+ ): Promise<void> {
21
+ await execGit(cwd, [
22
+ 'tag',
23
+ ...(message ? ['-a', tag, '-m', message, '--cleanup=whitespace'] : [tag]),
24
+ ])
25
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Delete a local git tag in the repository at `cwd`.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @param tag Tag name to delete
11
+ * @returns `true` when deletion succeeded, otherwise `false`
12
+ */
13
+ export function deleteGitTag(cwd: string, tag: string): Promise<boolean> {
14
+ return execGit(cwd, ['tag', '-d', tag])
15
+ .then(() => true)
16
+ .catch(() => false)
17
+ }
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+ import { countGitHistory } from '../history/countGitHistory'
6
+ import { withShallowLock } from '../repository/withShallowLock'
7
+ import { deleteGitTag } from './deleteGitTag'
8
+ import { isGitTagPresent } from './isGitTagPresent'
9
+
10
+ const MIN_HISTORY_DEPTH = 2
11
+ const ORIGIN_NOT_REPO = `fatal: 'origin' does not appear to be a git repository`
12
+ const REMOTE_REF_NOT_FOUND = `fatal: couldn't find remote ref`
13
+
14
+ /**
15
+ * Ensure a tag exists locally, fetching it shallowly from `origin` if needed.
16
+ *
17
+ * Acquires a shallow-fetch lock, verifies local presence and history depth,
18
+ * attempts a shallow fetch of `refs/tags/<tag>` when required, and removes a
19
+ * local tag if the remote reports the tag is missing.
20
+ *
21
+ * @param cwd Working directory
22
+ * @param tag Tag name (for example `v1.2.0`)
23
+ * @returns `true` if the tag is present or was fetched; otherwise `false`
24
+ */
25
+ export function ensureGitTag(cwd: string, tag: string): Promise<boolean> {
26
+ return withShallowLock(cwd, async () => {
27
+ const tagRef = `refs/tags/${tag}`
28
+
29
+ if (
30
+ (await isGitTagPresent(cwd, tag)) &&
31
+ (await countGitHistory(cwd, tagRef)) >= MIN_HISTORY_DEPTH
32
+ ) {
33
+ return true
34
+ }
35
+
36
+ try {
37
+ await execGit(cwd, [
38
+ 'fetch',
39
+ 'origin',
40
+ `${tagRef}:${tagRef}`,
41
+ `--depth=${MIN_HISTORY_DEPTH}`,
42
+ '--update-shallow',
43
+ '--force',
44
+ ])
45
+
46
+ return true
47
+ } catch (err) {
48
+ const errorMessage = (err as Error | undefined)?.message
49
+
50
+ if (errorMessage?.includes(ORIGIN_NOT_REPO)) {
51
+ return await isGitTagPresent(cwd, tag)
52
+ }
53
+
54
+ if (errorMessage?.includes(REMOTE_REF_NOT_FOUND)) {
55
+ if (await isGitTagPresent(cwd, tag)) {
56
+ await deleteGitTag(cwd, tag)
57
+ }
58
+ }
59
+ }
60
+
61
+ return false
62
+ })
63
+ }
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ /**
5
+ * Generate a git tag name for a package.
6
+ *
7
+ * For entry packages the tag is `v<version>`. For non-entry packages the
8
+ * tag is `<name>@<version>`. Returns `null` when `version` is not provided.
9
+ *
10
+ * @param entry Whether the package is the repository entry package
11
+ * @param name Package name (required for non-entry packages)
12
+ * @param version Package version
13
+ * @returns Tag string, or `null` when `version` is absent
14
+ */
15
+ export function generateGitTagName(
16
+ entry: boolean,
17
+ name: string | null | undefined,
18
+ version: string
19
+ ): string
20
+ export function generateGitTagName(
21
+ entry: boolean,
22
+ name: string | null | undefined,
23
+ version: string | null | undefined
24
+ ): string | null
25
+ export function generateGitTagName(
26
+ entry: boolean,
27
+ name: string | null | undefined,
28
+ version: string | null | undefined
29
+ ): string | null {
30
+ if (!version) {
31
+ return null
32
+ }
33
+
34
+ if (entry) {
35
+ return `v${version}`
36
+ }
37
+
38
+ if (!name) {
39
+ throw new Error('Package name is required to build a tag name.')
40
+ }
41
+
42
+ return `${name}@${version}`
43
+ }
@@ -0,0 +1,23 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Get the tag name that exactly matches the current `HEAD`, if any.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @returns Tag name or `null` when no exact tag matches HEAD
11
+ */
12
+ export async function getCurrentGitTag(cwd: string): Promise<string | null> {
13
+ try {
14
+ const stdout = await execGit(cwd, ['describe', '--tags', '--exact-match'])
15
+
16
+ const tag = stdout.trim()
17
+
18
+ /* istanbul ignore next */
19
+ return tag ? tag : null
20
+ } catch {
21
+ return null
22
+ }
23
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Get the message of an annotated git tag in the repository at `cwd`.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @param tag Tag name to query
11
+ * @returns The tag message string when found, otherwise `undefined`
12
+ */
13
+ export async function getGitTagMessage(
14
+ cwd: string,
15
+ tag: string
16
+ ): Promise<string | undefined> {
17
+ try {
18
+ const stdout = await execGit(cwd, [
19
+ 'tag',
20
+ '-l',
21
+ '--format=%(objecttype)%00%(contents)',
22
+ tag,
23
+ ])
24
+
25
+ const [objectType, message] = stdout.split('\0')
26
+
27
+ if (objectType?.trim() !== 'tag') {
28
+ return undefined
29
+ }
30
+
31
+ return message?.trim() || undefined
32
+ } catch {
33
+ return undefined
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Get the most recent tag reachable from `HEAD`.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @returns Latest tag string or `null` when none is found
11
+ */
12
+ export async function getLatestGitTag(cwd: string): Promise<string | null> {
13
+ try {
14
+ const stdout = await execGit(cwd, ['describe', '--tags', '--abbrev=0'])
15
+ const tag = stdout.trim()
16
+
17
+ /* istanbul ignore next */
18
+ return tag ? tag : null
19
+ } catch {
20
+ return null
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ export * from './createGitTag'
5
+ export * from './deleteGitTag'
6
+ export * from './ensureGitTag'
7
+ export * from './generateGitTagName'
8
+ export * from './getCurrentGitTag'
9
+ export * from './getGitTagMessage'
10
+ export * from './getLatestGitTag'
11
+ export * from './isGitTagConnected'
12
+ export * from './isGitTagPointed'
13
+ export * from './isGitTagPresent'
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Determine whether a specific git tag commit is reachable from the current HEAD.
8
+ *
9
+ * Uses `git merge-base --is-ancestor refs/tags/<tag>^{commit} HEAD` to verify
10
+ * that the tag's commit is an ancestor of the current reference. Any non-zero
11
+ * exit status or execution error is treated as "not reachable".
12
+ *
13
+ * @param cwd Working directory where the git command will run
14
+ * @param tag Tag name to evaluate (for example, "v1.2.0")
15
+ * @returns True if the tag commit is reachable from HEAD, false otherwise
16
+ */
17
+ export async function isGitTagConnected(
18
+ cwd: string,
19
+ tag: string
20
+ ): Promise<boolean> {
21
+ try {
22
+ await execGit(cwd, [
23
+ 'merge-base',
24
+ '--is-ancestor',
25
+ `refs/tags/${tag}^{commit}`,
26
+ 'HEAD',
27
+ ])
28
+
29
+ return true
30
+ } catch {
31
+ return false
32
+ }
33
+ }
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { execGit } from '#util'
5
+
6
+ /**
7
+ * Check whether a given tag points to the provided commit.
8
+ *
9
+ * @param cwd Repository working directory
10
+ * @param tag Tag name to resolve
11
+ * @param commit Commit hash to compare against
12
+ * @returns `true` when the tag points at `commit`, otherwise `false`
13
+ */
14
+ export async function isGitTagPointed(
15
+ cwd: string,
16
+ tag: string,
17
+ commit: string
18
+ ): Promise<boolean> {
19
+ try {
20
+ await execGit(cwd, [
21
+ 'merge-base',
22
+ '--is-ancestor',
23
+ `refs/tags/${tag}^{commit}`,
24
+ commit,
25
+ ])
26
+
27
+ await execGit(cwd, [
28
+ 'merge-base',
29
+ '--is-ancestor',
30
+ commit,
31
+ `refs/tags/${tag}^{commit}`,
32
+ ])
33
+
34
+ return true
35
+ } catch {
36
+ return false
37
+ }
38
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
3
+
4
+ import { resolveGitRevision } from '../revision/resolveGitRevision'
5
+
6
+ /**
7
+ * Determine whether a specific git tag exists locally in the repository.
8
+ *
9
+ * @param cwd Working directory for the git command
10
+ * @param tag Name of the tag to evaluate
11
+ * @returns True if the tag reference exists locally, false otherwise
12
+ */
13
+ export function isGitTagPresent(cwd: string, tag: string): Promise<boolean> {
14
+ return resolveGitRevision(cwd, `refs/tags/${tag}`)
15
+ .then(() => true)
16
+ .catch(() => false)
17
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,9 @@
1
- // Copyright (c) 2026 Fai Chen. Licensed under the MIT License.
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Fai
2
3
 
3
- export {}
4
+ export * from './cache'
5
+ export * from './config'
6
+ export * from './git'
7
+ export * from './types'
8
+ export * from './util'
9
+ export * from './workflow'