infra-kit 0.1.109 → 0.1.110
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/.eslintcache +1 -1
- package/dist/cli.js +41 -41
- package/dist/cli.js.map +3 -3
- package/dist/mcp.js +35 -35
- package/dist/mcp.js.map +3 -3
- package/package.json +1 -1
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +2 -2
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +1 -1
- package/src/commands/release-create/release-create.ts +1 -1
- package/src/commands/release-desc-edit/release-desc-edit.ts +2 -2
- package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +2 -2
- package/src/integrations/cmux/__tests__/canonicalize-cmux-title.test.ts +1 -1
- package/src/integrations/cmux/workspace-title.ts +1 -1
- package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +10 -10
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +1 -1
- package/src/lib/git-utils/__tests__/git-utils.test.ts +4 -4
- package/src/lib/release-id/__tests__/release-id.test.ts +27 -19
- package/src/lib/release-id/release-id.ts +19 -18
- package/src/lib/release-utils/__tests__/release-utils.test.ts +8 -8
- package/src/lib/release-utils/release-utils.ts +1 -1
- package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +4 -4
- package/src/lib/version-utils/__tests__/next-version.test.ts +1 -1
- package/src/lib/version-utils/load-existing-versions.ts +1 -1
- package/src/lib/version-utils/version-utils.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {
|
|
|
31
31
|
|
|
32
32
|
commandEcho.start('release-deploy-all')
|
|
33
33
|
|
|
34
|
-
let selectedReleaseBranch = '' // "release/v1.8.0" | "release/
|
|
34
|
+
let selectedReleaseBranch = '' // "release/v1.8.0" | "release/checkout-redesign" | "dev"
|
|
35
35
|
|
|
36
36
|
if (version) {
|
|
37
37
|
selectedReleaseBranch = version === 'dev' ? 'dev' : resolveReleaseBranch(version)
|
|
@@ -143,7 +143,7 @@ export const ghReleaseDeployAllMcpTool = defineMcpTool({
|
|
|
143
143
|
version: z
|
|
144
144
|
.string()
|
|
145
145
|
.describe(
|
|
146
|
-
'Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign") — resolves to the release/vX.Y.Z or release
|
|
146
|
+
'Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign") — resolves to the release/vX.Y.Z or release/<name> branch. Pass "dev" to deploy from the dev branch instead. Required for MCP calls.',
|
|
147
147
|
),
|
|
148
148
|
env: z
|
|
149
149
|
.string()
|
|
@@ -232,7 +232,7 @@ export const ghReleaseDeploySelectedMcpTool = defineMcpTool({
|
|
|
232
232
|
version: z
|
|
233
233
|
.string()
|
|
234
234
|
.describe(
|
|
235
|
-
'Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign") — resolves to the release/vX.Y.Z or release
|
|
235
|
+
'Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign") — resolves to the release/vX.Y.Z or release/<name> branch. Pass "dev" to deploy from the dev branch instead. Required for MCP calls.',
|
|
236
236
|
),
|
|
237
237
|
env: z
|
|
238
238
|
.string()
|
|
@@ -379,7 +379,7 @@ export const releaseCreate = async (args: ReleaseCreateArgs) => {
|
|
|
379
379
|
export const releaseCreateMcpTool = defineMcpTool({
|
|
380
380
|
name: 'release-create',
|
|
381
381
|
description:
|
|
382
|
-
'Create one or more releases in a single call. Each entry in "releases" carries EITHER a "version" (semver or the literal token "next") OR a "name" (free-form kebab-case identifier) — exactly one is required and they are mutually exclusive. Each entry also has its own type (regular|hotfix, default regular) and optional description; all entries in one call must share the same type — mixed regular+hotfix batches are rejected (create them in separate invocations). For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch (release/v<semver> for versions, release
|
|
382
|
+
'Create one or more releases in a single call. Each entry in "releases" carries EITHER a "version" (semver or the literal token "next") OR a "name" (free-form kebab-case identifier) — exactly one is required and they are mutually exclusive. Each entry also has its own type (regular|hotfix, default regular) and optional description; all entries in one call must share the same type — mixed regular+hotfix batches are rejected (create them in separate invocations). For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch (release/v<semver> for versions, release/<name> for names), opens a GitHub release PR, and creates the matching Jira fix version (v<semver> for versions, <name> for names). The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially. Named releases never auto-bump and "next" is version-only. Must be run from the main repository checkout (not a linked worktree) on the matching base branch with a clean working tree. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
|
|
383
383
|
inputSchema: {
|
|
384
384
|
releases: z
|
|
385
385
|
.array(
|
|
@@ -208,7 +208,7 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
|
|
|
208
208
|
export const releaseDescEditMcpTool = defineMcpTool({
|
|
209
209
|
name: 'release-desc-edit',
|
|
210
210
|
description:
|
|
211
|
-
"Edit a release's description in Jira and in the matching GitHub release PR body. Accepts a release version or a release name: targets the Jira fix version named `v<version>` (versioned) or `<name>` (named) and the open PR on branch `release/v<version>` or `release
|
|
211
|
+
"Edit a release's description in Jira and in the matching GitHub release PR body. Accepts a release version or a release name: targets the Jira fix version named `v<version>` (versioned) or `<name>` (named) and the open PR on branch `release/v<version>` or `release/<name>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
|
|
212
212
|
inputSchema: {
|
|
213
213
|
version: z
|
|
214
214
|
.string()
|
|
@@ -217,7 +217,7 @@ export const releaseDescEditMcpTool = defineMcpTool({
|
|
|
217
217
|
},
|
|
218
218
|
outputSchema: {
|
|
219
219
|
version: z.string().describe('Release version'),
|
|
220
|
-
branch: z.string().describe('Release branch name (e.g. "release/v1.2.5" or "release/
|
|
220
|
+
branch: z.string().describe('Release branch name (e.g. "release/v1.2.5" or "release/checkout-redesign")'),
|
|
221
221
|
jiraVersionUrl: z.string().describe('Jira fix version URL'),
|
|
222
222
|
previousDescription: z.string().describe('The description before the update'),
|
|
223
223
|
newDescription: z.string().describe('The description after the update'),
|
|
@@ -25,7 +25,7 @@ vi.mock('src/lib/git-utils', () => {
|
|
|
25
25
|
|
|
26
26
|
const REPO = 'hulyo-monorepo'
|
|
27
27
|
const WORKTREE_DIR = '/repos/project-worktrees'
|
|
28
|
-
const BRANCHES = ['release/v1.48.0', 'release/
|
|
28
|
+
const BRANCHES = ['release/v1.48.0', 'release/checkout-redesign']
|
|
29
29
|
|
|
30
30
|
const titleFor = {
|
|
31
31
|
versioned: `${REPO} 1.48.0`,
|
|
@@ -68,7 +68,7 @@ describe('openCmux dedup', () => {
|
|
|
68
68
|
expect(result.opened).toEqual([titleFor.named])
|
|
69
69
|
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledTimes(1)
|
|
70
70
|
expect(openCmuxWorkspaceWithLayout).toHaveBeenCalledWith({
|
|
71
|
-
cwd: `${WORKTREE_DIR}/release/
|
|
71
|
+
cwd: `${WORKTREE_DIR}/release/checkout-redesign`,
|
|
72
72
|
title: titleFor.named,
|
|
73
73
|
})
|
|
74
74
|
})
|
|
@@ -48,7 +48,7 @@ describe('canonicalizeCmuxTitle', () => {
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
it('built title canonicalizes to a stable key for a named release', () => {
|
|
51
|
-
const built = buildCmuxWorkspaceTitle({ repoName: 'hulyo-monorepo', branch: 'release/
|
|
51
|
+
const built = buildCmuxWorkspaceTitle({ repoName: 'hulyo-monorepo', branch: 'release/checkout-redesign' })
|
|
52
52
|
|
|
53
53
|
expect(canonicalizeCmuxTitle(built)).toBe('hulyo-monorepo checkout-redesign')
|
|
54
54
|
})
|
|
@@ -10,7 +10,7 @@ interface BuildCmuxWorkspaceTitleArgs {
|
|
|
10
10
|
* `worktrees-remove`. Release branches are rendered via their release-id
|
|
11
11
|
* display label so the title reads e.g. `"hulyo-monorepo 1.48.0"` for
|
|
12
12
|
* `"release/v1.48.0"` and `"hulyo-monorepo checkout-redesign"` for
|
|
13
|
-
* `"release/
|
|
13
|
+
* `"release/checkout-redesign"`. Non-release branches (cmux titles them too)
|
|
14
14
|
* fall back to the raw branch string.
|
|
15
15
|
*/
|
|
16
16
|
export const buildCmuxWorkspaceTitle = (args: BuildCmuxWorkspaceTitleArgs): string => {
|
|
@@ -67,33 +67,33 @@ describe('getReleasePRs (discovery + sort)', () => {
|
|
|
67
67
|
|
|
68
68
|
it('sorts version branches first by semver ascending (numeric, 1.9.0 < 1.10.0), then names by createdAt', async () => {
|
|
69
69
|
responses.release = [
|
|
70
|
-
pr({ headRefName: 'release/
|
|
70
|
+
pr({ headRefName: 'release/zeta-feature', createdAt: '2026-01-10T00:00:00Z', title: 'Release zeta-feature' }),
|
|
71
71
|
pr({ headRefName: 'release/v1.10.0', createdAt: '2026-01-02T00:00:00Z' }),
|
|
72
|
-
pr({ headRefName: 'release/
|
|
72
|
+
pr({ headRefName: 'release/alpha-feature', createdAt: '2026-01-05T00:00:00Z', title: 'Release alpha-feature' }),
|
|
73
73
|
pr({ headRefName: 'release/v1.9.0', createdAt: '2026-01-01T00:00:00Z' }),
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
await expect(getReleasePRs()).resolves.toEqual([
|
|
77
77
|
'release/v1.9.0',
|
|
78
78
|
'release/v1.10.0',
|
|
79
|
-
'release/
|
|
80
|
-
'release/
|
|
79
|
+
'release/alpha-feature',
|
|
80
|
+
'release/zeta-feature',
|
|
81
81
|
])
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
it('filters out unparseable junk branches instead of throwing or NaN-sorting', async () => {
|
|
85
85
|
responses.release = [
|
|
86
|
-
pr({ headRefName: 'release/
|
|
86
|
+
pr({ headRefName: 'release/Bad_Name', createdAt: '2026-01-01T00:00:00Z' }),
|
|
87
87
|
pr({ headRefName: 'release/v1.2.3', createdAt: '2026-01-02T00:00:00Z' }),
|
|
88
88
|
pr({ headRefName: 'totally-not-a-release', createdAt: '2026-01-03T00:00:00Z' }),
|
|
89
89
|
pr({
|
|
90
|
-
headRefName: 'release/
|
|
90
|
+
headRefName: 'release/checkout-redesign',
|
|
91
91
|
createdAt: '2026-01-04T00:00:00Z',
|
|
92
92
|
title: 'Release checkout-redesign',
|
|
93
93
|
}),
|
|
94
94
|
]
|
|
95
95
|
|
|
96
|
-
await expect(getReleasePRs()).resolves.toEqual(['release/v1.2.3', 'release/
|
|
96
|
+
await expect(getReleasePRs()).resolves.toEqual(['release/v1.2.3', 'release/checkout-redesign'])
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
it('merges hotfix (base main) and release (base dev) sets', async () => {
|
|
@@ -119,14 +119,14 @@ describe('getReleasePRsWithInfo (discovery + sort)', () => {
|
|
|
119
119
|
|
|
120
120
|
it('returns branch/title/createdAt in the locked order with junk filtered', async () => {
|
|
121
121
|
responses.release = [
|
|
122
|
-
pr({ headRefName: 'release/
|
|
123
|
-
pr({ headRefName: 'release/
|
|
122
|
+
pr({ headRefName: 'release/beta-feature', createdAt: '2026-02-02T00:00:00Z', title: 'Release beta-feature' }),
|
|
123
|
+
pr({ headRefName: 'release/Bad_Name', createdAt: '2026-02-03T00:00:00Z', title: 'Release Bad_Name' }),
|
|
124
124
|
pr({ headRefName: 'release/v3.1.0', createdAt: '2026-02-01T00:00:00Z', title: 'Release v3.1.0' }),
|
|
125
125
|
]
|
|
126
126
|
|
|
127
127
|
await expect(getReleasePRsWithInfo()).resolves.toEqual([
|
|
128
128
|
{ branch: 'release/v3.1.0', title: 'Release v3.1.0', createdAt: '2026-02-01T00:00:00Z' },
|
|
129
|
-
{ branch: 'release/
|
|
129
|
+
{ branch: 'release/beta-feature', title: 'Release beta-feature', createdAt: '2026-02-02T00:00:00Z' },
|
|
130
130
|
])
|
|
131
131
|
})
|
|
132
132
|
})
|
|
@@ -77,7 +77,7 @@ const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
|
|
|
77
77
|
* (version branches first by semver ascending, then named branches by PR
|
|
78
78
|
* creation date). Unparseable head refs are filtered out.
|
|
79
79
|
*
|
|
80
|
-
* @returns [release/v1.18.22, release/v1.18.23, release/
|
|
80
|
+
* @returns [release/v1.18.22, release/v1.18.23, release/checkout-redesign]
|
|
81
81
|
*/
|
|
82
82
|
export const getReleasePRs = async (): Promise<string[]> => {
|
|
83
83
|
try {
|
|
@@ -23,8 +23,8 @@ describe('getCurrentWorktrees', () => {
|
|
|
23
23
|
worktreeList.stdout = [
|
|
24
24
|
asWorktreeLine('main'),
|
|
25
25
|
asWorktreeLine('release/v1.18.22'),
|
|
26
|
-
asWorktreeLine('release/
|
|
27
|
-
asWorktreeLine('release/
|
|
26
|
+
asWorktreeLine('release/checkout-redesign'),
|
|
27
|
+
asWorktreeLine('release/Bad_Name'),
|
|
28
28
|
asWorktreeLine('feature/login-page'),
|
|
29
29
|
'/repos/project abc1234 (bare)',
|
|
30
30
|
'',
|
|
@@ -32,13 +32,13 @@ describe('getCurrentWorktrees', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('returns versioned AND named release worktrees for type release', async () => {
|
|
35
|
-
await expect(getCurrentWorktrees('release')).resolves.toEqual(['release/v1.18.22', 'release/
|
|
35
|
+
await expect(getCurrentWorktrees('release')).resolves.toEqual(['release/v1.18.22', 'release/checkout-redesign'])
|
|
36
36
|
})
|
|
37
37
|
|
|
38
38
|
it('excludes junk release branches and non-release branches', async () => {
|
|
39
39
|
const branches = await getCurrentWorktrees('release')
|
|
40
40
|
|
|
41
|
-
expect(branches).not.toContain('release/
|
|
41
|
+
expect(branches).not.toContain('release/Bad_Name')
|
|
42
42
|
expect(branches).not.toContain('feature/login-page')
|
|
43
43
|
expect(branches).not.toContain('main')
|
|
44
44
|
})
|
|
@@ -29,13 +29,13 @@ describe('parseBranchName', () => {
|
|
|
29
29
|
expect(parseBranchName('release/v1.2.3')).toEqual(version(1, 2, 3))
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it('parses release
|
|
33
|
-
expect(parseBranchName('release/
|
|
32
|
+
it('parses release/<name> as a name', () => {
|
|
33
|
+
expect(parseBranchName('release/checkout-redesign')).toEqual(name('checkout-redesign'))
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
it('strips a leading refs/heads/ prefix for versions and names', () => {
|
|
37
37
|
expect(parseBranchName('refs/heads/release/v2.0.1')).toEqual(version(2, 0, 1))
|
|
38
|
-
expect(parseBranchName('refs/heads/release/
|
|
38
|
+
expect(parseBranchName('refs/heads/release/my-feature')).toEqual(name('my-feature'))
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it('trims surrounding whitespace', () => {
|
|
@@ -48,23 +48,31 @@ describe('parseBranchName', () => {
|
|
|
48
48
|
expect(parseBranchName('dev')).toBeNull()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('
|
|
52
|
-
expect(parseBranchName('release/foo')).
|
|
53
|
-
expect(parseBranchName('release/garbage')).
|
|
51
|
+
it('parses bare release/<kebab> as a name', () => {
|
|
52
|
+
expect(parseBranchName('release/foo')).toEqual(name('foo'))
|
|
53
|
+
expect(parseBranchName('release/garbage')).toEqual(name('garbage'))
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it('returns null for
|
|
56
|
+
it('returns null for malformed version branches that are also not valid names', () => {
|
|
57
57
|
expect(parseBranchName('release/v1.2')).toBeNull()
|
|
58
|
-
expect(parseBranchName('release/v1')).toBeNull()
|
|
59
58
|
expect(parseBranchName('release/v1.2.3.4')).toBeNull()
|
|
60
|
-
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('falls through to a name when the release/v segment is not valid semver', () => {
|
|
62
|
+
expect(parseBranchName('release/v1.2.3')).toEqual(version(1, 2, 3))
|
|
63
|
+
expect(parseBranchName('release/v1')).toEqual(name('v1'))
|
|
64
|
+
expect(parseBranchName('release/vfoo')).toEqual(name('vfoo'))
|
|
65
|
+
expect(parseBranchName('release/vnext')).toEqual(name('vnext'))
|
|
61
66
|
})
|
|
62
67
|
|
|
63
68
|
it('returns null for invalid names', () => {
|
|
64
|
-
expect(parseBranchName('release/
|
|
65
|
-
expect(parseBranchName('release/
|
|
66
|
-
expect(parseBranchName('release/
|
|
67
|
-
expect(parseBranchName('release/
|
|
69
|
+
expect(parseBranchName('release/Bad_Name')).toBeNull()
|
|
70
|
+
expect(parseBranchName('release/UPPER')).toBeNull()
|
|
71
|
+
expect(parseBranchName('release/main')).toBeNull()
|
|
72
|
+
expect(parseBranchName('release/')).toBeNull()
|
|
73
|
+
// The retired release/n/<name> form is no longer valid: 'n/checkout-redesign'
|
|
74
|
+
// contains a slash and fails the kebab-case rule.
|
|
75
|
+
expect(parseBranchName('release/n/checkout-redesign')).toBeNull()
|
|
68
76
|
})
|
|
69
77
|
|
|
70
78
|
it('never throws on arbitrary input', () => {
|
|
@@ -81,19 +89,19 @@ describe('parseBranchName', () => {
|
|
|
81
89
|
describe('parseReleaseRef', () => {
|
|
82
90
|
it('delegates a release branch ref to parseBranchName', () => {
|
|
83
91
|
expect(parseReleaseRef('release/v1.2.3')).toEqual(version(1, 2, 3))
|
|
84
|
-
expect(parseReleaseRef('release/
|
|
92
|
+
expect(parseReleaseRef('release/checkout-redesign')).toEqual(name('checkout-redesign'))
|
|
85
93
|
expect(parseReleaseRef('refs/heads/release/v3.4.5')).toEqual(version(3, 4, 5))
|
|
86
94
|
})
|
|
87
95
|
|
|
88
96
|
it('throws when a release branch ref is invalid', () => {
|
|
89
97
|
expect(() => {
|
|
90
|
-
return parseReleaseRef('release/
|
|
98
|
+
return parseReleaseRef('release/Bad_Name')
|
|
91
99
|
}).toThrow(InvalidReleaseRefError)
|
|
92
100
|
expect(() => {
|
|
93
101
|
return parseReleaseRef('release/v1.2')
|
|
94
102
|
}).toThrow(InvalidReleaseRefError)
|
|
95
103
|
expect(() => {
|
|
96
|
-
return parseReleaseRef('release/
|
|
104
|
+
return parseReleaseRef('release/main')
|
|
97
105
|
}).toThrow(InvalidReleaseRefError)
|
|
98
106
|
})
|
|
99
107
|
|
|
@@ -204,7 +212,7 @@ describe('validateName', () => {
|
|
|
204
212
|
describe('formatBranchName', () => {
|
|
205
213
|
it('formats versions and names', () => {
|
|
206
214
|
expect(formatBranchName(version(1, 2, 3))).toBe('release/v1.2.3')
|
|
207
|
-
expect(formatBranchName(name('checkout-redesign'))).toBe('release/
|
|
215
|
+
expect(formatBranchName(name('checkout-redesign'))).toBe('release/checkout-redesign')
|
|
208
216
|
})
|
|
209
217
|
})
|
|
210
218
|
|
|
@@ -244,13 +252,13 @@ describe('displayLabel', () => {
|
|
|
244
252
|
describe('isReleaseBranch', () => {
|
|
245
253
|
it('returns true for both branch schemes', () => {
|
|
246
254
|
expect(isReleaseBranch('release/v1.2.3')).toBe(true)
|
|
247
|
-
expect(isReleaseBranch('release/
|
|
255
|
+
expect(isReleaseBranch('release/checkout-redesign')).toBe(true)
|
|
248
256
|
expect(isReleaseBranch('refs/heads/release/v1.2.3')).toBe(true)
|
|
249
257
|
})
|
|
250
258
|
|
|
251
259
|
it('returns false for junk', () => {
|
|
252
260
|
expect(isReleaseBranch('feature/x')).toBe(false)
|
|
253
|
-
expect(isReleaseBranch('release/
|
|
261
|
+
expect(isReleaseBranch('release/Bad_Name')).toBe(false)
|
|
254
262
|
expect(isReleaseBranch('main')).toBe(false)
|
|
255
263
|
})
|
|
256
264
|
|
|
@@ -18,7 +18,6 @@ const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
|
18
18
|
|
|
19
19
|
const RELEASE_BRANCH_PREFIX = 'release/'
|
|
20
20
|
const VERSION_BRANCH_PREFIX = 'release/v'
|
|
21
|
-
const NAME_BRANCH_PREFIX = 'release/n/'
|
|
22
21
|
const REFS_HEADS_PREFIX = 'refs/heads/'
|
|
23
22
|
|
|
24
23
|
const NEXT_TOKEN = 'next'
|
|
@@ -91,33 +90,35 @@ export const validateName = (name: string): void => {
|
|
|
91
90
|
/**
|
|
92
91
|
* Lenient parse of a git branch name into a {@link ReleaseId}. Tolerates a
|
|
93
92
|
* leading `refs/heads/`. Returns `null` for anything that is not a valid
|
|
94
|
-
* `release/v<semver>` or `release
|
|
93
|
+
* `release/v<semver>` or `release/<name>` branch. Never throws.
|
|
94
|
+
*
|
|
95
|
+
* Precedence is version-first: `release/v<semver>` is a version, but if the
|
|
96
|
+
* segment after `release/v` is not a valid semver (e.g. `release/vnext`), the
|
|
97
|
+
* whole token after `release/` is treated as a candidate name instead.
|
|
95
98
|
*/
|
|
96
99
|
export const parseBranchName = (branch: string): ReleaseId | null => {
|
|
97
100
|
const stripped = stripRefsHeads(branch.trim())
|
|
98
101
|
|
|
102
|
+
if (!stripped.startsWith(RELEASE_BRANCH_PREFIX)) return null
|
|
103
|
+
|
|
99
104
|
if (stripped.startsWith(VERSION_BRANCH_PREFIX)) {
|
|
100
105
|
const semverPart = stripped.slice(VERSION_BRANCH_PREFIX.length)
|
|
101
106
|
const match = BRANCH_SEMVER_RE.exec(semverPart)
|
|
102
107
|
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
if (match) {
|
|
109
|
+
return makeVersion(Number(match[1]), Number(match[2]), Number(match[3]))
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
const namePart = stripped.slice(NAME_BRANCH_PREFIX.length)
|
|
113
|
+
const namePart = stripped.slice(RELEASE_BRANCH_PREFIX.length)
|
|
110
114
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { kind: 'name', name: namePart, raw: namePart }
|
|
115
|
+
try {
|
|
116
|
+
validateName(namePart)
|
|
117
|
+
} catch {
|
|
118
|
+
return null
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
return
|
|
121
|
+
return { kind: 'name', name: namePart, raw: namePart }
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/**
|
|
@@ -138,7 +139,7 @@ export const parseReleaseRef = (input: string): ReleaseId => {
|
|
|
138
139
|
|
|
139
140
|
if (!parsed) {
|
|
140
141
|
throw new InvalidReleaseRefError(
|
|
141
|
-
`"${input}" looks like a release branch but is not a valid release/v<semver> or release
|
|
142
|
+
`"${input}" looks like a release branch but is not a valid release/v<semver> or release/<name> ref.`,
|
|
142
143
|
)
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -168,11 +169,11 @@ export const parseReleaseRef = (input: string): ReleaseId => {
|
|
|
168
169
|
return { kind: 'name', name: trimmed, raw: trimmed }
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
/** Render the branch name for a release id: `release/v1.2.3` | `release
|
|
172
|
+
/** Render the branch name for a release id: `release/v1.2.3` | `release/<name>`. */
|
|
172
173
|
export const formatBranchName = (id: ReleaseId): string => {
|
|
173
174
|
if (id.kind === 'version') return `${VERSION_BRANCH_PREFIX}${id.raw}`
|
|
174
175
|
|
|
175
|
-
return `${
|
|
176
|
+
return `${RELEASE_BRANCH_PREFIX}${id.name}`
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/**
|
|
@@ -14,7 +14,7 @@ describe('parseBranchChoices', () => {
|
|
|
14
14
|
it('parses version and name branches and drops junk', () => {
|
|
15
15
|
const result = parseBranchChoices([
|
|
16
16
|
'release/v1.2.3',
|
|
17
|
-
'release/
|
|
17
|
+
'release/checkout-redesign',
|
|
18
18
|
'feature/not-a-release',
|
|
19
19
|
'release/v9.9', // malformed semver
|
|
20
20
|
])
|
|
@@ -31,7 +31,7 @@ describe('parseBranchChoices', () => {
|
|
|
31
31
|
|
|
32
32
|
expect(labels).toEqual(['1.2.3', 'checkout-redesign'])
|
|
33
33
|
expect(kinds).toEqual(['version', 'name'])
|
|
34
|
-
expect(branches).toEqual(['release/v1.2.3', 'release/
|
|
34
|
+
expect(branches).toEqual(['release/v1.2.3', 'release/checkout-redesign'])
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
it('returns an empty array when nothing parses', () => {
|
|
@@ -42,7 +42,7 @@ describe('parseBranchChoices', () => {
|
|
|
42
42
|
describe('formatBranchChoices', () => {
|
|
43
43
|
it('labels versions and names, keying Jira descriptions by the Jira version name', () => {
|
|
44
44
|
const choices = formatBranchChoices({
|
|
45
|
-
branches: ['release/v1.2.3', 'release/
|
|
45
|
+
branches: ['release/v1.2.3', 'release/checkout-redesign'],
|
|
46
46
|
// Jira descriptions are keyed by the Jira version NAME: `v1.2.3` | `<name>`.
|
|
47
47
|
descriptions: new Map([
|
|
48
48
|
['v1.2.3', 'version desc'],
|
|
@@ -54,16 +54,16 @@ describe('formatBranchChoices', () => {
|
|
|
54
54
|
expect(choices[0]?.value).toBe('release/v1.2.3')
|
|
55
55
|
expect(choices[0]?.name).toContain('1.2.3')
|
|
56
56
|
expect(choices[0]?.name).toContain('version desc')
|
|
57
|
-
expect(choices[1]?.value).toBe('release/
|
|
57
|
+
expect(choices[1]?.value).toBe('release/checkout-redesign')
|
|
58
58
|
expect(choices[1]?.name).toContain('checkout-redesign')
|
|
59
59
|
expect(choices[1]?.name).toContain('name desc')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it('includes the type tag when types are provided', () => {
|
|
63
63
|
const choices = formatBranchChoices({
|
|
64
|
-
branches: ['release/
|
|
64
|
+
branches: ['release/checkout-redesign'],
|
|
65
65
|
descriptions: new Map(),
|
|
66
|
-
types: new Map([['release/
|
|
66
|
+
types: new Map([['release/checkout-redesign', 'hotfix']]),
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
expect(choices[0]?.name).toContain('[hotfix]')
|
|
@@ -90,7 +90,7 @@ describe('resolveReleaseBranch', () => {
|
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
it('builds a name branch from a release name', () => {
|
|
93
|
-
expect(resolveReleaseBranch('checkout-redesign')).toBe('release/
|
|
93
|
+
expect(resolveReleaseBranch('checkout-redesign')).toBe('release/checkout-redesign')
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
it('throws an OperationError for junk input', () => {
|
|
@@ -113,7 +113,7 @@ describe('releaseLabelFromBranch', () => {
|
|
|
113
113
|
|
|
114
114
|
it('labels version and name branches', () => {
|
|
115
115
|
expect(releaseLabelFromBranch('release/v1.2.3')).toBe('1.2.3')
|
|
116
|
-
expect(releaseLabelFromBranch('release/
|
|
116
|
+
expect(releaseLabelFromBranch('release/checkout-redesign')).toBe('checkout-redesign')
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
it('falls back to the raw branch when it does not parse', () => {
|
|
@@ -160,7 +160,7 @@ export const parseBranchChoices = (branches: string[]): ParsedBranchChoice[] =>
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
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
|
|
163
|
+
* `checkout-redesign`) to its branch name (`release/v1.2.3` | `release/<name>`).
|
|
164
164
|
* Strict: surfaces a parse failure as an OperationError with remediation text.
|
|
165
165
|
*/
|
|
166
166
|
export const resolveReleaseBranch = (versionArg: string): string => {
|
|
@@ -13,11 +13,11 @@ describe('extractVersionBranches', () => {
|
|
|
13
13
|
expect(extractVersionBranches(stdout)).toEqual(['1.62.0', '1.64.5'])
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
it('ignores named release
|
|
16
|
+
it('ignores named release/<name> branches (irrelevant to next-bump math)', () => {
|
|
17
17
|
const stdout = [
|
|
18
18
|
lsRemoteLine('release/v1.0.0'),
|
|
19
|
-
lsRemoteLine('release/
|
|
20
|
-
lsRemoteLine('release/
|
|
19
|
+
lsRemoteLine('release/checkout-redesign'),
|
|
20
|
+
lsRemoteLine('release/zeta-feature'),
|
|
21
21
|
].join('\n')
|
|
22
22
|
|
|
23
23
|
expect(extractVersionBranches(stdout)).toEqual(['1.0.0'])
|
|
@@ -25,7 +25,7 @@ describe('extractVersionBranches', () => {
|
|
|
25
25
|
|
|
26
26
|
it('ignores junk and non-release lines without throwing', () => {
|
|
27
27
|
const stdout = [
|
|
28
|
-
lsRemoteLine('release/
|
|
28
|
+
lsRemoteLine('release/Bad_Name'),
|
|
29
29
|
lsRemoteLine('release/v2.3.4'),
|
|
30
30
|
'malformed-line-without-tab',
|
|
31
31
|
lsRemoteLine('feature/login'),
|
|
@@ -295,7 +295,7 @@ describe('resolveReleaseEntries', () => {
|
|
|
295
295
|
it('named entry produces the named branch/PR/Jira output', () => {
|
|
296
296
|
const { id } = resolve({ name: 'checkout-redesign', type: 'regular' })
|
|
297
297
|
|
|
298
|
-
expect(formatBranchName(id)).toBe('release/
|
|
298
|
+
expect(formatBranchName(id)).toBe('release/checkout-redesign')
|
|
299
299
|
expect(formatPrTitle(id, 'regular')).toBe('Release checkout-redesign')
|
|
300
300
|
expect(formatJiraName(id)).toBe('checkout-redesign')
|
|
301
301
|
})
|
|
@@ -11,7 +11,7 @@ import type { SemVer } from './next-version'
|
|
|
11
11
|
* Extract version-branch tokens from raw `git ls-remote` stdout. Each line is
|
|
12
12
|
* `<sha>\t<ref>`; refs are routed through release-id's lenient
|
|
13
13
|
* {@link parseBranchName} and only `kind: 'version'` ids are kept (named
|
|
14
|
-
* `release
|
|
14
|
+
* `release/<name>` branches are irrelevant to `next`-bump math and are dropped).
|
|
15
15
|
* Returns the no-`v` semver tokens (e.g. `1.2.3`) that
|
|
16
16
|
* {@link collectKnownVersions} parses as versions. Pure — no I/O — so it is
|
|
17
17
|
* unit-testable without the network.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SemVer-ONLY: this assumes a `v<semver>` token and will produce `NaN`s for
|
|
5
5
|
* anything else. Callers that may see branch names or named releases
|
|
6
|
-
* (`release
|
|
6
|
+
* (`release/<name>`) MUST use the `release-id` module (`parseBranchName` /
|
|
7
7
|
* `parseReleaseRef` + `compareReleaseIds`) instead. Only provably names-free
|
|
8
8
|
* callers (the `collectKnownVersions` / `next`-bump math path) may use this.
|
|
9
9
|
*/
|