infra-kit 0.1.102 → 0.1.107

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 (147) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
  3. package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
  4. package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
  5. package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
  6. package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
  7. package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
  8. package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
  9. package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
  10. package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
  11. package/.omc/state/idle-notif-cooldown.json +3 -0
  12. package/.omc/state/last-tool-error.json +4 -4
  13. package/.omc/state/mission-state.json +53 -0
  14. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  15. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
  16. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
  17. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
  18. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
  19. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
  20. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
  21. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
  22. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
  23. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
  24. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
  25. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
  26. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
  27. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
  28. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
  29. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
  30. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
  31. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
  32. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
  33. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
  34. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
  35. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
  36. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
  37. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
  39. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
  40. package/.omc/state/subagent-tracking.json +14 -4
  41. package/.turbo/turbo-build.log +7 -0
  42. package/.turbo/turbo-check.log +14 -0
  43. package/.turbo/turbo-prettier-fix.log +2 -1
  44. package/.turbo/turbo-test.log +28 -5
  45. package/.turbo/turbo-validate.log +14 -0
  46. package/dist/cli.js +88 -74
  47. package/dist/cli.js.map +4 -4
  48. package/dist/entry/index.d.ts +2 -0
  49. package/dist/index.js +2 -0
  50. package/dist/index.js.map +7 -0
  51. package/dist/lib/package-config/package-config.d.ts +71 -0
  52. package/dist/mcp.js +43 -41
  53. package/dist/mcp.js.map +4 -4
  54. package/eslint.config.js +1 -1
  55. package/infra-kit.config.ts +5 -0
  56. package/package.json +20 -13
  57. package/scripts/build.js +32 -3
  58. package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  59. package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
  60. package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
  61. package/src/commands/audit/__tests__/audit.test.ts +59 -0
  62. package/src/commands/audit/audit.ts +177 -0
  63. package/src/commands/audit/index.ts +1 -0
  64. package/src/commands/config/config.ts +49 -7
  65. package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
  66. package/src/commands/doctor/doctor.ts +69 -4
  67. package/src/commands/env-clear/env-clear.ts +1 -1
  68. package/src/commands/env-list/env-list.ts +3 -3
  69. package/src/commands/env-load/env-load.ts +1 -1
  70. package/src/commands/env-status/env-status.ts +1 -1
  71. package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
  72. package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
  73. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
  74. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
  75. package/src/commands/gh-release-list/gh-release-list.ts +19 -8
  76. package/src/commands/init/__tests__/agent-files.test.ts +147 -0
  77. package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
  78. package/src/commands/init/agent-files.ts +199 -0
  79. package/src/commands/init/index.ts +7 -0
  80. package/src/commands/init/init.ts +82 -60
  81. package/src/commands/init/migrate-config.ts +146 -0
  82. package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  83. package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
  84. package/src/commands/release-create/release-create.ts +142 -38
  85. package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
  86. package/src/commands/version/version.ts +1 -1
  87. package/src/commands/worktrees-add/worktrees-add.ts +7 -12
  88. package/src/commands/worktrees-list/worktrees-list.ts +13 -5
  89. package/src/commands/worktrees-open/worktrees-open.ts +1 -1
  90. package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
  91. package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
  92. package/src/entry/cli.ts +50 -7
  93. package/src/entry/index.ts +5 -0
  94. package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
  95. package/src/integrations/cmux/workspace-title.ts +10 -4
  96. package/src/integrations/doppler/doppler-project.ts +1 -1
  97. package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
  98. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
  99. package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
  100. package/src/lib/constants/index.ts +15 -0
  101. package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
  102. package/src/lib/git-utils/git-utils.ts +3 -1
  103. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
  104. package/src/lib/infra-kit-config/index.ts +7 -1
  105. package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
  106. package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
  107. package/src/lib/managed-block/index.ts +8 -0
  108. package/src/lib/managed-block/managed-block.ts +145 -0
  109. package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
  110. package/src/lib/package-config/index.ts +3 -0
  111. package/src/lib/package-config/package-config-schema.ts +19 -0
  112. package/src/lib/package-config/package-config.ts +99 -0
  113. package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
  114. package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
  115. package/src/lib/package-validator/checks/config-check.ts +30 -0
  116. package/src/lib/package-validator/checks/files-check.ts +29 -0
  117. package/src/lib/package-validator/checks/index.ts +4 -0
  118. package/src/lib/package-validator/checks/scripts-check.ts +23 -0
  119. package/src/lib/package-validator/checks/turbo-check.ts +47 -0
  120. package/src/lib/package-validator/fs-utils.ts +18 -0
  121. package/src/lib/package-validator/index.ts +3 -0
  122. package/src/lib/package-validator/loader/config-loader.ts +77 -0
  123. package/src/lib/package-validator/loader/index.ts +2 -0
  124. package/src/lib/package-validator/loader/package-discovery.ts +98 -0
  125. package/src/lib/package-validator/package-validator.ts +48 -0
  126. package/src/lib/package-validator/types.ts +15 -0
  127. package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
  128. package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
  129. package/src/lib/release-id/index.ts +15 -0
  130. package/src/lib/release-id/release-id.ts +257 -0
  131. package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
  132. package/src/lib/release-utils/index.ts +4 -0
  133. package/src/lib/release-utils/release-utils.ts +85 -17
  134. package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
  135. package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
  136. package/src/lib/version-utils/index.ts +3 -0
  137. package/src/lib/version-utils/load-existing-versions.ts +29 -10
  138. package/src/lib/version-utils/next-version.ts +67 -12
  139. package/src/lib/version-utils/version-utils.ts +13 -4
  140. package/src/mcp/tools/index.ts +2 -0
  141. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  142. package/src/types.ts +1 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
  145. /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
  146. /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
  147. /package/src/lib/{constants.ts → constants/constants.ts} +0 -0
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { OperationError } from 'src/lib/errors/operation-error'
4
+
5
+ import {
6
+ DEV_REF,
7
+ formatBranchChoices,
8
+ parseBranchChoices,
9
+ releaseLabelFromBranch,
10
+ resolveReleaseBranch,
11
+ } from '../release-utils'
12
+
13
+ describe('parseBranchChoices', () => {
14
+ it('parses version and name branches and drops junk', () => {
15
+ const result = parseBranchChoices([
16
+ 'release/v1.2.3',
17
+ 'release/n/checkout-redesign',
18
+ 'feature/not-a-release',
19
+ 'release/v9.9', // malformed semver
20
+ ])
21
+
22
+ const labels = result.map((r) => {
23
+ return r.label
24
+ })
25
+ const kinds = result.map((r) => {
26
+ return r.id.kind
27
+ })
28
+ const branches = result.map((r) => {
29
+ return r.branch
30
+ })
31
+
32
+ expect(labels).toEqual(['1.2.3', 'checkout-redesign'])
33
+ expect(kinds).toEqual(['version', 'name'])
34
+ expect(branches).toEqual(['release/v1.2.3', 'release/n/checkout-redesign'])
35
+ })
36
+
37
+ it('returns an empty array when nothing parses', () => {
38
+ expect(parseBranchChoices(['main', 'dev', 'feature/x'])).toEqual([])
39
+ })
40
+ })
41
+
42
+ describe('formatBranchChoices', () => {
43
+ it('labels versions and names, keying Jira descriptions by the Jira version name', () => {
44
+ const choices = formatBranchChoices({
45
+ branches: ['release/v1.2.3', 'release/n/checkout-redesign'],
46
+ // Jira descriptions are keyed by the Jira version NAME: `v1.2.3` | `<name>`.
47
+ descriptions: new Map([
48
+ ['v1.2.3', 'version desc'],
49
+ ['checkout-redesign', 'name desc'],
50
+ ]),
51
+ })
52
+
53
+ expect(choices).toHaveLength(2)
54
+ expect(choices[0]?.value).toBe('release/v1.2.3')
55
+ expect(choices[0]?.name).toContain('1.2.3')
56
+ expect(choices[0]?.name).toContain('version desc')
57
+ expect(choices[1]?.value).toBe('release/n/checkout-redesign')
58
+ expect(choices[1]?.name).toContain('checkout-redesign')
59
+ expect(choices[1]?.name).toContain('name desc')
60
+ })
61
+
62
+ it('includes the type tag when types are provided', () => {
63
+ const choices = formatBranchChoices({
64
+ branches: ['release/n/checkout-redesign'],
65
+ descriptions: new Map(),
66
+ types: new Map([['release/n/checkout-redesign', 'hotfix']]),
67
+ })
68
+
69
+ expect(choices[0]?.name).toContain('[hotfix]')
70
+ })
71
+
72
+ it('drops branches that do not parse as release ids', () => {
73
+ const choices = formatBranchChoices({
74
+ branches: ['release/v1.2.3', 'feature/not-a-release'],
75
+ descriptions: new Map(),
76
+ })
77
+
78
+ expect(
79
+ choices.map((c) => {
80
+ return c.value
81
+ }),
82
+ ).toEqual(['release/v1.2.3'])
83
+ })
84
+ })
85
+
86
+ describe('resolveReleaseBranch', () => {
87
+ it('builds a version branch from a bare or v-prefixed version', () => {
88
+ expect(resolveReleaseBranch('1.2.3')).toBe('release/v1.2.3')
89
+ expect(resolveReleaseBranch('v1.2.3')).toBe('release/v1.2.3')
90
+ })
91
+
92
+ it('builds a name branch from a release name', () => {
93
+ expect(resolveReleaseBranch('checkout-redesign')).toBe('release/n/checkout-redesign')
94
+ })
95
+
96
+ it('throws an OperationError for junk input', () => {
97
+ expect(() => {
98
+ return resolveReleaseBranch('Not A Valid Name')
99
+ }).toThrow(OperationError)
100
+ })
101
+
102
+ it('throws an OperationError for the unresolved "next" token', () => {
103
+ expect(() => {
104
+ return resolveReleaseBranch('next')
105
+ }).toThrow(OperationError)
106
+ })
107
+ })
108
+
109
+ describe('releaseLabelFromBranch', () => {
110
+ it('passes through the dev sentinel unchanged', () => {
111
+ expect(releaseLabelFromBranch(DEV_REF)).toBe('dev')
112
+ })
113
+
114
+ it('labels version and name branches', () => {
115
+ expect(releaseLabelFromBranch('release/v1.2.3')).toBe('1.2.3')
116
+ expect(releaseLabelFromBranch('release/n/checkout-redesign')).toBe('checkout-redesign')
117
+ })
118
+
119
+ it('falls back to the raw branch when it does not parse', () => {
120
+ expect(releaseLabelFromBranch('feature/not-a-release')).toBe('feature/not-a-release')
121
+ })
122
+ })
@@ -5,7 +5,11 @@ export {
5
5
  formatVersionLabel,
6
6
  getBaseBranch,
7
7
  getJiraDescriptions,
8
+ parseBranchChoices,
8
9
  prepareGitForRelease,
10
+ releaseBranchLabels,
9
11
  type ReleaseCreationResult,
12
+ releaseLabelFromBranch,
10
13
  type ReleaseType,
14
+ resolveReleaseBranch,
11
15
  } from './release-utils'
@@ -3,6 +3,12 @@ import { $ } from 'zx'
3
3
  import { createReleaseBranch } from 'src/integrations/gh'
4
4
  import { createJiraVersion, getProjectVersions, loadJiraConfigOptional } from 'src/integrations/jira'
5
5
  import type { JiraConfig } from 'src/integrations/jira'
6
+ import { OperationError } from 'src/lib/errors/operation-error'
7
+ import { displayLabel, formatBranchName, formatJiraName, parseBranchName, parseReleaseRef } from 'src/lib/release-id'
8
+ import type { ReleaseId } from 'src/lib/release-id'
9
+
10
+ /** Sentinel ref for deploying from the `dev` branch instead of a release branch. */
11
+ export const DEV_REF = 'dev'
6
12
 
7
13
  export type ReleaseType = 'regular' | 'hotfix'
8
14
 
@@ -39,7 +45,7 @@ export const prepareGitForRelease = async (type: ReleaseType = 'regular'): Promi
39
45
  }
40
46
 
41
47
  interface CreateSingleReleaseArgs {
42
- version: string
48
+ id: ReleaseId
43
49
  jiraConfig: JiraConfig
44
50
  description?: string
45
51
  type?: ReleaseType
@@ -49,9 +55,10 @@ interface CreateSingleReleaseArgs {
49
55
  * Create a single release by creating both Jira version and GitHub release branch
50
56
  */
51
57
  export const createSingleRelease = async (args: CreateSingleReleaseArgs): Promise<ReleaseCreationResult> => {
52
- const { version, jiraConfig, description, type = 'regular' } = args
53
- // 1. Create Jira version (mandatory)
54
- const versionName = `v${version}`
58
+ const { id, jiraConfig, description, type = 'regular' } = args
59
+ // 1. Create Jira version (mandatory). For versioned releases this is
60
+ // "v1.2.3" (byte-identical to before); for named releases it is "<name>".
61
+ const versionName = formatJiraName(id)
55
62
 
56
63
  const result = await createJiraVersion(
57
64
  {
@@ -68,10 +75,10 @@ export const createSingleRelease = async (args: CreateSingleReleaseArgs): Promis
68
75
  const jiraVersionUrl = `${jiraConfig.baseUrl}/projects/${result.version!.projectId}/versions/${result.version!.id}/tab/release-report-all-issues`
69
76
 
70
77
  // 2. Create GitHub release branch
71
- const releaseInfo = await createReleaseBranch({ version, jiraVersionUrl, type, description })
78
+ const releaseInfo = await createReleaseBranch({ id, jiraVersionUrl, type, description })
72
79
 
73
80
  return {
74
- version,
81
+ version: displayLabel(id),
75
82
  type,
76
83
  branchName: releaseInfo.branchName,
77
84
  prUrl: releaseInfo.prUrl,
@@ -130,32 +137,93 @@ interface FormatBranchChoicesArgs {
130
137
  types?: Map<string, ReleaseType>
131
138
  }
132
139
 
140
+ interface ParsedBranchChoice {
141
+ branch: string
142
+ id: ReleaseId
143
+ /** Human display label: `1.2.3` | `<name>`. */
144
+ label: string
145
+ }
146
+
147
+ /**
148
+ * Parse branches into release ids, dropping any that do not parse (lenient
149
+ * discovery source). Exported for unit testing the version/name/junk split.
150
+ */
151
+ export const parseBranchChoices = (branches: string[]): ParsedBranchChoice[] => {
152
+ return branches.flatMap((branch) => {
153
+ const id = parseBranchName(branch)
154
+
155
+ if (!id) return []
156
+
157
+ return [{ branch, id, label: displayLabel(id) }]
158
+ })
159
+ }
160
+
161
+ /**
162
+ * Resolve an operator-supplied release ref (version `1.2.3` / `v1.2.3` or name
163
+ * `checkout-redesign`) to its branch name (`release/v1.2.3` | `release/n/<name>`).
164
+ * Strict: surfaces a parse failure as an OperationError with remediation text.
165
+ */
166
+ export const resolveReleaseBranch = (versionArg: string): string => {
167
+ try {
168
+ return formatBranchName(parseReleaseRef(versionArg))
169
+ } catch (error) {
170
+ throw new OperationError(error, {
171
+ operation: `resolve release ref "${versionArg}"`,
172
+ remediation: 'pass a version (e.g. "1.2.5") or a release name (e.g. "checkout-redesign")',
173
+ })
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Render the human display label for a release branch. Returns the `dev`
179
+ * sentinel unchanged; otherwise derives `1.2.3` | `<name>` from the branch.
180
+ * Falls back to the raw branch when it does not parse as a release id.
181
+ */
182
+ export const releaseLabelFromBranch = (branch: string): string => {
183
+ if (branch === DEV_REF) return DEV_REF
184
+
185
+ const id = parseBranchName(branch)
186
+
187
+ return id ? displayLabel(id) : branch
188
+ }
189
+
190
+ /**
191
+ * Render human display labels for a list of release branches, dropping any
192
+ * branch that does not parse as a release id (lenient discovery contract).
193
+ */
194
+ export const releaseBranchLabels = (branches: string[]): string[] => {
195
+ return branches.flatMap((branch) => {
196
+ const id = parseBranchName(branch)
197
+
198
+ return id ? [displayLabel(id)] : []
199
+ })
200
+ }
201
+
133
202
  /**
134
203
  * Format release branch names as checkbox choices with aligned type tags and Jira descriptions
135
204
  */
136
205
  export const formatBranchChoices = (args: FormatBranchChoicesArgs): { name: string; value: string }[] => {
137
206
  const { branches, descriptions, types } = args
138
207
 
139
- const versionNames = branches.map((b) => {
140
- return b.replace('release/v', '')
141
- })
208
+ const parsed = parseBranchChoices(branches)
142
209
 
143
210
  const maxLen = Math.max(
144
- ...versionNames.map((v) => {
145
- return v.length
211
+ 0,
212
+ ...parsed.map((p) => {
213
+ return p.label.length
146
214
  }),
147
215
  )
148
216
 
149
- return branches.map((branch, i) => {
150
- const version = versionNames[i] as string
217
+ return parsed.map(({ branch, id, label }) => {
151
218
  const type = types ? types.get(branch) || 'regular' : undefined
152
- const desc = descriptions.get(`v${version}`)
153
- const padding = ' '.repeat(maxLen - version.length + 3)
219
+ // Jira-descriptions map is keyed by the Jira version NAME (`v1.2.3` | `<name>`).
220
+ const desc = descriptions.get(formatJiraName(id))
221
+ const padding = ' '.repeat(maxLen - label.length + 3)
154
222
 
155
- let name = type ? formatVersionLabel(version, type, maxLen) : version
223
+ let name = type ? formatVersionLabel(label, type, maxLen) : label
156
224
 
157
225
  if (desc) {
158
- name = type ? `${name} ${desc}` : `${version}${padding}${desc}`
226
+ name = type ? `${name} ${desc}` : `${label}${padding}${desc}`
159
227
  }
160
228
 
161
229
  return { name, value: branch }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { extractVersionBranches } from '../load-existing-versions'
4
+
5
+ const lsRemoteLine = (ref: string): string => {
6
+ return `0000000000000000000000000000000000000000\trefs/heads/${ref}`
7
+ }
8
+
9
+ describe('extractVersionBranches', () => {
10
+ it('keeps only semver version branches and returns no-v tokens', () => {
11
+ const stdout = [lsRemoteLine('release/v1.62.0'), lsRemoteLine('release/v1.64.5'), ''].join('\n')
12
+
13
+ expect(extractVersionBranches(stdout)).toEqual(['1.62.0', '1.64.5'])
14
+ })
15
+
16
+ it('ignores named release/n/* branches (irrelevant to next-bump math)', () => {
17
+ const stdout = [
18
+ lsRemoteLine('release/v1.0.0'),
19
+ lsRemoteLine('release/n/checkout-redesign'),
20
+ lsRemoteLine('release/n/zeta-feature'),
21
+ ].join('\n')
22
+
23
+ expect(extractVersionBranches(stdout)).toEqual(['1.0.0'])
24
+ })
25
+
26
+ it('ignores junk and non-release lines without throwing', () => {
27
+ const stdout = [
28
+ lsRemoteLine('release/garbage'),
29
+ lsRemoteLine('release/v2.3.4'),
30
+ 'malformed-line-without-tab',
31
+ lsRemoteLine('feature/login'),
32
+ '',
33
+ ].join('\n')
34
+
35
+ expect(extractVersionBranches(stdout)).toEqual(['2.3.4'])
36
+ })
37
+ })
@@ -1,12 +1,26 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
+ import { InvalidReleaseNameError, formatBranchName, formatJiraName, formatPrTitle } from '../../release-id'
4
+ import type { ReleaseId } from '../../release-id'
3
5
  import {
4
6
  NoPriorVersionsError,
5
7
  collectKnownVersions,
6
8
  computeNextVersion,
9
+ hasNextToken,
7
10
  parseReleaseSpec,
8
11
  resolveReleaseEntries,
9
12
  } from '../next-version'
13
+ import type { ReleaseEntry } from '../next-version'
14
+
15
+ const versionId = (raw: string): ReleaseId => {
16
+ const [major, minor, patch] = raw.split('.').map(Number)
17
+
18
+ return { kind: 'version', semver: { major: major!, minor: minor!, patch: patch! }, raw }
19
+ }
20
+
21
+ const nameId = (name: string): ReleaseId => {
22
+ return { kind: 'name', name, raw: name }
23
+ }
10
24
 
11
25
  describe('collectKnownVersions', () => {
12
26
  it('parses remote branch refs and Jira version names and dedupes', () => {
@@ -123,7 +137,7 @@ describe('parseReleaseSpec', () => {
123
137
  describe('resolveReleaseEntries', () => {
124
138
  const known = collectKnownVersions({ remoteBranches: ['release/v1.63.0'] })
125
139
 
126
- it('passes through explicit semver entries unchanged', () => {
140
+ it('passes through explicit semver entries, wrapping them in a version ReleaseId', () => {
127
141
  expect(
128
142
  resolveReleaseEntries(
129
143
  [
@@ -133,14 +147,14 @@ describe('resolveReleaseEntries', () => {
133
147
  known,
134
148
  ),
135
149
  ).toEqual([
136
- { version: '1.70.0', type: 'regular' },
137
- { version: '1.70.1', type: 'hotfix' },
150
+ { id: versionId('1.70.0'), type: 'regular' },
151
+ { id: versionId('1.70.1'), type: 'hotfix' },
138
152
  ])
139
153
  })
140
154
 
141
155
  it('resolves a single "next" using the entry type', () => {
142
156
  expect(resolveReleaseEntries([{ version: 'next', type: 'regular' }], known)).toEqual([
143
- { version: '1.64.0', type: 'regular' },
157
+ { id: versionId('1.64.0'), type: 'regular' },
144
158
  ])
145
159
  })
146
160
 
@@ -154,8 +168,8 @@ describe('resolveReleaseEntries', () => {
154
168
  known,
155
169
  ),
156
170
  ).toEqual([
157
- { version: '1.64.0', type: 'regular' },
158
- { version: '1.65.0', type: 'regular' },
171
+ { id: versionId('1.64.0'), type: 'regular' },
172
+ { id: versionId('1.65.0'), type: 'regular' },
159
173
  ])
160
174
  })
161
175
 
@@ -169,8 +183,8 @@ describe('resolveReleaseEntries', () => {
169
183
  known,
170
184
  ),
171
185
  ).toEqual([
172
- { version: '1.64.0', type: 'regular' },
173
- { version: '1.64.1', type: 'hotfix' },
186
+ { id: versionId('1.64.0'), type: 'regular' },
187
+ { id: versionId('1.64.1'), type: 'hotfix' },
174
188
  ])
175
189
  })
176
190
 
@@ -185,21 +199,21 @@ describe('resolveReleaseEntries', () => {
185
199
  known,
186
200
  ),
187
201
  ).toEqual([
188
- { version: '1.64.0', type: 'regular' },
189
- { version: '1.70.0', type: 'regular' },
190
- { version: '1.71.0', type: 'regular' },
202
+ { id: versionId('1.64.0'), type: 'regular' },
203
+ { id: versionId('1.70.0'), type: 'regular' },
204
+ { id: versionId('1.71.0'), type: 'regular' },
191
205
  ])
192
206
  })
193
207
 
194
208
  it('preserves description through resolution', () => {
195
209
  expect(resolveReleaseEntries([{ version: 'next', type: 'regular', description: 'Holiday' }], known)).toEqual([
196
- { version: '1.64.0', type: 'regular', description: 'Holiday' },
210
+ { id: versionId('1.64.0'), type: 'regular', description: 'Holiday' },
197
211
  ])
198
212
  })
199
213
 
200
214
  it('accepts case-insensitive "next"', () => {
201
215
  expect(resolveReleaseEntries([{ version: 'NEXT', type: 'regular' }], known)).toEqual([
202
- { version: '1.64.0', type: 'regular' },
216
+ { id: versionId('1.64.0'), type: 'regular' },
203
217
  ])
204
218
  })
205
219
 
@@ -214,4 +228,96 @@ describe('resolveReleaseEntries', () => {
214
228
  return resolveReleaseEntries([{ version: 'next', type: 'regular' }], [])
215
229
  }).toThrow(NoPriorVersionsError)
216
230
  })
231
+
232
+ describe('named entries', () => {
233
+ it('resolves a valid name into a name ReleaseId', () => {
234
+ expect(resolveReleaseEntries([{ name: 'checkout-redesign', type: 'regular' }], known)).toEqual([
235
+ { id: nameId('checkout-redesign'), type: 'regular' },
236
+ ])
237
+ })
238
+
239
+ it('preserves type and description on named entries (named hotfix allowed)', () => {
240
+ expect(
241
+ resolveReleaseEntries([{ name: 'checkout-redesign', type: 'hotfix', description: 'Q3 work' }], known),
242
+ ).toEqual([{ id: nameId('checkout-redesign'), type: 'hotfix', description: 'Q3 work' }])
243
+ })
244
+
245
+ it('never auto-bumps a named entry (no version interaction)', () => {
246
+ expect(
247
+ resolveReleaseEntries(
248
+ [
249
+ { name: 'first-thing', type: 'regular' },
250
+ { version: 'next', type: 'regular' },
251
+ ],
252
+ known,
253
+ ),
254
+ ).toEqual([
255
+ { id: nameId('first-thing'), type: 'regular' },
256
+ { id: versionId('1.64.0'), type: 'regular' },
257
+ ])
258
+ })
259
+
260
+ it('throws InvalidReleaseNameError on an invalid (non-kebab) name', () => {
261
+ expect(() => {
262
+ return resolveReleaseEntries([{ name: 'Checkout_Redesign', type: 'regular' }], known)
263
+ }).toThrow(InvalidReleaseNameError)
264
+ })
265
+
266
+ it('throws InvalidReleaseNameError when the name is the reserved word "next"', () => {
267
+ expect(() => {
268
+ return resolveReleaseEntries([{ name: 'next', type: 'regular' }], known)
269
+ }).toThrow(InvalidReleaseNameError)
270
+ })
271
+ })
272
+
273
+ describe('formatting round-trip (zero-regression for versioned, correct for named)', () => {
274
+ const resolve = (entry: Parameters<typeof resolveReleaseEntries>[0][number]): ReleaseEntry => {
275
+ return resolveReleaseEntries([entry], known)[0] as ReleaseEntry
276
+ }
277
+
278
+ it('versioned literal "1.62.0" produces byte-identical branch/PR/Jira output', () => {
279
+ const { id } = resolve({ version: '1.62.0', type: 'regular' })
280
+
281
+ expect(formatBranchName(id)).toBe('release/v1.62.0')
282
+ expect(formatPrTitle(id, 'regular')).toBe('Release v1.62.0')
283
+ expect(formatJiraName(id)).toBe('v1.62.0')
284
+ })
285
+
286
+ it('versioned "next" resolves then formats identically to the equivalent literal', () => {
287
+ const { id } = resolve({ version: 'next', type: 'regular' })
288
+
289
+ // known max is 1.63.0 → regular next is 1.64.0
290
+ expect(formatBranchName(id)).toBe('release/v1.64.0')
291
+ expect(formatPrTitle(id, 'hotfix')).toBe('Hotfix v1.64.0')
292
+ expect(formatJiraName(id)).toBe('v1.64.0')
293
+ })
294
+
295
+ it('named entry produces the named branch/PR/Jira output', () => {
296
+ const { id } = resolve({ name: 'checkout-redesign', type: 'regular' })
297
+
298
+ expect(formatBranchName(id)).toBe('release/n/checkout-redesign')
299
+ expect(formatPrTitle(id, 'regular')).toBe('Release checkout-redesign')
300
+ expect(formatJiraName(id)).toBe('checkout-redesign')
301
+ })
302
+ })
303
+ })
304
+
305
+ describe('hasNextToken', () => {
306
+ it('is true when any versioned entry is "next"', () => {
307
+ expect(
308
+ hasNextToken([
309
+ { name: 'checkout-redesign', type: 'regular' },
310
+ { version: 'next', type: 'regular' },
311
+ ]),
312
+ ).toBe(true)
313
+ })
314
+
315
+ it('is false for explicit versions and named entries only', () => {
316
+ expect(
317
+ hasNextToken([
318
+ { version: '1.2.3', type: 'regular' },
319
+ { name: 'next-thing', type: 'regular' },
320
+ ]),
321
+ ).toBe(false)
322
+ })
217
323
  })
@@ -4,10 +4,13 @@ export {
4
4
  computeNextVersion,
5
5
  type ExistingVersionsSources,
6
6
  hasNextToken,
7
+ type NamedReleaseInput,
7
8
  NEXT_TOKEN,
8
9
  NoPriorVersionsError,
9
10
  parseReleaseSpec,
10
11
  type ReleaseEntry,
12
+ type ReleaseInput,
13
+ type ReleaseSpec,
11
14
  resolveReleaseEntries,
12
15
  type SemVer,
13
16
  } from './next-version'
@@ -2,27 +2,46 @@ import { $ } from 'zx'
2
2
 
3
3
  import { getProjectVersions, loadJiraConfigOptional } from 'src/integrations/jira'
4
4
  import { logger } from 'src/lib/logger'
5
+ import { parseBranchName } from 'src/lib/release-id'
5
6
 
6
7
  import { collectKnownVersions } from './next-version'
7
8
  import type { SemVer } from './next-version'
8
9
 
10
+ /**
11
+ * Extract version-branch tokens from raw `git ls-remote` stdout. Each line is
12
+ * `<sha>\t<ref>`; refs are routed through release-id's lenient
13
+ * {@link parseBranchName} and only `kind: 'version'` ids are kept (named
14
+ * `release/n/*` branches are irrelevant to `next`-bump math and are dropped).
15
+ * Returns the no-`v` semver tokens (e.g. `1.2.3`) that
16
+ * {@link collectKnownVersions} parses as versions. Pure — no I/O — so it is
17
+ * unit-testable without the network.
18
+ */
19
+ export const extractVersionBranches = (lsRemoteStdout: string): string[] => {
20
+ return lsRemoteStdout
21
+ .split('\n')
22
+ .map((line) => {
23
+ const tab = line.indexOf('\t')
24
+
25
+ if (tab === -1) return null
26
+
27
+ return parseBranchName(line.slice(tab + 1))
28
+ })
29
+ .filter((id): id is NonNullable<typeof id> => {
30
+ return id !== null && id.kind === 'version'
31
+ })
32
+ .map((id) => {
33
+ return id.raw
34
+ })
35
+ }
36
+
9
37
  const parseRemoteRefs = async (): Promise<string[]> => {
10
38
  const previousQuiet = $.quiet
11
39
 
12
40
  try {
13
41
  $.quiet = true
14
42
  const result = await $`git ls-remote --heads origin 'release/v*'`
15
- const lines = result.stdout.split('\n')
16
-
17
- return lines
18
- .map((line) => {
19
- const tab = line.indexOf('\t')
20
-
21
- if (tab === -1) return ''
22
43
 
23
- return line.slice(tab + 1).replace(/^refs\/heads\//, '')
24
- })
25
- .filter(Boolean)
44
+ return extractVersionBranches(result.stdout)
26
45
  } finally {
27
46
  $.quiet = previousQuiet
28
47
  }