infra-kit 0.1.78 → 0.1.80

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.
@@ -2,6 +2,8 @@ import process from 'node:process'
2
2
  import { $ } from 'zx'
3
3
 
4
4
  import { logger } from 'src/lib/logger'
5
+ import { getBaseBranch } from 'src/lib/release-utils'
6
+ import type { ReleaseType } from 'src/lib/release-utils'
5
7
  import { sortVersions } from 'src/lib/version-utils'
6
8
 
7
9
  interface ReleasePR {
@@ -12,31 +14,95 @@ interface ReleasePR {
12
14
  baseRefName: string
13
15
  }
14
16
 
17
+ export interface ReleasePRInfo {
18
+ branch: string
19
+ title: string
20
+ }
21
+
15
22
  /**
16
- * Fetch open release PRs from GitHub with 'Release' in the title and base 'dev'.
23
+ * Fetch all open release/hotfix PRs from GitHub.
24
+ * Searches both dev (regular) and main (hotfix) base branches.
25
+ * Returns deduplicated ReleasePR objects.
26
+ */
27
+ const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
28
+ const releasePRs =
29
+ await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName`
30
+
31
+ const hotfixPRs =
32
+ await $`gh pr list --search "Hotfix in:title" --base main --json number,title,headRefName,state,baseRefName`
33
+
34
+ const all: ReleasePR[] = [...JSON.parse(releasePRs.stdout), ...JSON.parse(hotfixPRs.stdout)]
35
+
36
+ // Deduplicate by headRefName
37
+ const seen = new Set<string>()
38
+
39
+ return all.filter((pr) => {
40
+ if (seen.has(pr.headRefName)) return false
41
+
42
+ seen.add(pr.headRefName)
43
+
44
+ return true
45
+ })
46
+ }
47
+
48
+ /**
49
+ * Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title and base 'dev'.
17
50
  * Returns an array of headRefName strings sorted by semver in ascending order.
18
- * Throws an error if fetching fails.
19
51
  *
20
52
  * @returns [release/v1.18.22, release/v1.18.23, release/v1.18.24] (sorted by semver)
21
53
  */
22
54
  export const getReleasePRs = async (): Promise<string[]> => {
23
55
  try {
24
- // Example of releasePRs.output: {"headRefName":"release/v1.8.0","number":665,"state":"OPEN","title":"WIP Release/v1.8.0"}
25
- const releasePRs =
26
- await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName`
56
+ const prs = await fetchAllReleasePRs()
27
57
 
28
- const releasePRsArray: ReleasePR[] = JSON.parse(releasePRs.stdout)
58
+ if (prs.length === 0) {
59
+ logger.error('❌ No release PRs found. Check the project folder for the script. Exiting...')
29
60
 
30
- if (releasePRsArray.length === 0) {
61
+ process.exit(1)
62
+ }
63
+
64
+ return sortVersions(
65
+ prs.map((pr) => {
66
+ return pr.headRefName
67
+ }),
68
+ )
69
+ } catch (error) {
70
+ logger.error({ error }, '❌ Error fetching release PRs')
71
+
72
+ process.exit(1)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Fetch open release PRs with title info (for detecting release type).
78
+ * Returns ReleasePRInfo objects sorted by semver.
79
+ */
80
+ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
81
+ try {
82
+ const prs = await fetchAllReleasePRs()
83
+
84
+ if (prs.length === 0) {
31
85
  logger.error('❌ No release PRs found. Check the project folder for the script. Exiting...')
32
86
  process.exit(1)
33
87
  }
34
88
 
35
- const headRefNames = releasePRsArray.map((pr) => {
36
- return pr.headRefName
89
+ const sortedBranches = sortVersions(
90
+ prs.map((pr) => {
91
+ return pr.headRefName
92
+ }),
93
+ )
94
+ const prByBranch = new Map(
95
+ prs.map((pr) => {
96
+ return [pr.headRefName, pr]
97
+ }),
98
+ )
99
+
100
+ return sortedBranches.map((branch) => {
101
+ return {
102
+ branch,
103
+ title: prByBranch.get(branch)!.title,
104
+ }
37
105
  })
38
-
39
- return sortVersions(headRefNames)
40
106
  } catch (error) {
41
107
  logger.error({ error }, '❌ Error fetching release PRs')
42
108
  process.exit(1)
@@ -46,21 +112,24 @@ export const getReleasePRs = async (): Promise<string[]> => {
46
112
  interface CreateReleaseBranchArgs {
47
113
  version: string
48
114
  jiraVersionUrl: string
115
+ type: ReleaseType
49
116
  }
50
117
 
51
118
  // Function to create a release branch
52
119
  export const createReleaseBranch = async (
53
120
  args: CreateReleaseBranchArgs,
54
121
  ): Promise<{ branchName: string; prUrl: string }> => {
55
- const { version, jiraVersionUrl } = args
122
+ const { version, jiraVersionUrl, type } = args
123
+ const titlePrefix = type === 'hotfix' ? 'Hotfix' : 'Release'
124
+ const baseBranch = getBaseBranch(type)
56
125
 
57
126
  const branchName = `release/v${version}`
58
127
 
59
128
  try {
60
129
  $.quiet = true
61
130
 
62
- await $`git switch dev`
63
- await $`git pull origin dev`
131
+ await $`git switch ${baseBranch}`
132
+ await $`git pull origin ${baseBranch}`
64
133
  await $`git checkout -b ${branchName}`
65
134
  await $`git push -u origin ${branchName}`
66
135
  await $`git commit --allow-empty-message --allow-empty --message ''`
@@ -68,11 +137,11 @@ export const createReleaseBranch = async (
68
137
 
69
138
  // Create PR and capture URL
70
139
  const prResult =
71
- await $`gh pr create --title "Release v${version}" --body "${jiraVersionUrl} \n" --base dev --head ${branchName}`
140
+ await $`gh pr create --title "${titlePrefix} v${version}" --body "${jiraVersionUrl} \n" --base ${baseBranch} --head ${branchName}`
72
141
 
73
142
  const prLink = prResult.stdout.trim()
74
143
 
75
- await $`git switch dev`
144
+ await $`git switch ${baseBranch}`
76
145
 
77
146
  $.quiet = false
78
147
 
@@ -1 +1,2 @@
1
- export { createReleaseBranch, getReleasePRs } from './gh-release-prs'
1
+ export { createReleaseBranch, getReleasePRs, getReleasePRsWithInfo } from './gh-release-prs'
2
+ export type { ReleasePRInfo } from './gh-release-prs'
@@ -1,2 +1,3 @@
1
1
  export { validateGitHubCliAndAuth } from './gh-cli-auth'
2
- export { createReleaseBranch, getReleasePRs } from './gh-release-prs'
2
+ export { createReleaseBranch, getReleasePRs, getReleasePRsWithInfo } from './gh-release-prs'
3
+ export type { ReleasePRInfo } from './gh-release-prs'
@@ -99,7 +99,7 @@ export const createJiraVersion = async (
99
99
  * @param config - Jira configuration
100
100
  * @returns Array of JiraVersion objects
101
101
  */
102
- const getProjectVersions = async (config: JiraConfig): Promise<JiraVersion[]> => {
102
+ export const getProjectVersions = async (config: JiraConfig): Promise<JiraVersion[]> => {
103
103
  try {
104
104
  const { baseUrl, token, email, projectId } = config
105
105
 
@@ -1,4 +1,10 @@
1
- export { createJiraVersion, deliverJiraRelease, loadJiraConfig, loadJiraConfigOptional } from './api.js'
1
+ export {
2
+ createJiraVersion,
3
+ deliverJiraRelease,
4
+ getProjectVersions,
5
+ loadJiraConfig,
6
+ loadJiraConfigOptional,
7
+ } from './api.js'
2
8
  export type {
3
9
  CreateJiraVersionParams,
4
10
  CreateJiraVersionResult,
@@ -30,7 +30,7 @@ const createCommandEcho = () => {
30
30
 
31
31
  /**
32
32
  * Track an option selection
33
- * @param flag The CLI flag (e.g., "--version")
33
+ * @param flag The CLI flag (e.g., "--versions")
34
34
  * @param value The selected value
35
35
  */
36
36
  addOption(flag: string, value: string | string[] | boolean): void {
@@ -60,9 +60,8 @@ const createCommandEcho = () => {
60
60
  .filter(Boolean)
61
61
  .join(' ')
62
62
 
63
+ logger.info(`📟 Equivalent command: \npnpm exec infra-kit ${commandName} ${formattedOptions}`)
63
64
  logger.info('')
64
- logger.info('# Equivalent command:')
65
- logger.info(`pnpm exec infra-kit ${commandName} ${formattedOptions}`)
66
65
  },
67
66
 
68
67
  /**
@@ -1 +1,11 @@
1
- export { createSingleRelease, prepareGitForRelease, type ReleaseCreationResult } from './release-utils'
1
+ export {
2
+ createSingleRelease,
3
+ detectReleaseType,
4
+ formatBranchChoices,
5
+ formatVersionLabel,
6
+ getBaseBranch,
7
+ getJiraDescriptions,
8
+ prepareGitForRelease,
9
+ type ReleaseCreationResult,
10
+ type ReleaseType,
11
+ } from './release-utils'
@@ -1,11 +1,22 @@
1
1
  import { $ } from 'zx'
2
2
 
3
3
  import { createReleaseBranch } from 'src/integrations/gh'
4
- import { createJiraVersion } from 'src/integrations/jira'
4
+ import { createJiraVersion, getProjectVersions, loadJiraConfigOptional } from 'src/integrations/jira'
5
5
  import type { JiraConfig } from 'src/integrations/jira'
6
6
 
7
+ export type ReleaseType = 'regular' | 'hotfix'
8
+
9
+ /**
10
+ * Get the base branch for a release type.
11
+ * Regular releases branch from/to dev, hotfixes branch from/to main.
12
+ */
13
+ export const getBaseBranch = (type: ReleaseType): string => {
14
+ return type === 'hotfix' ? 'main' : 'dev'
15
+ }
16
+
7
17
  export interface ReleaseCreationResult {
8
18
  version: string
19
+ type: ReleaseType
9
20
  branchName: string
10
21
  prUrl: string
11
22
  jiraVersionUrl: string
@@ -13,30 +24,32 @@ export interface ReleaseCreationResult {
13
24
 
14
25
  /**
15
26
  * Prepare git repository for release creation
16
- * Fetches latest changes, switches to dev branch, and pulls latest
27
+ * Fetches latest changes, switches to base branch, and pulls latest
17
28
  */
18
- export const prepareGitForRelease = async (): Promise<void> => {
29
+ export const prepareGitForRelease = async (type: ReleaseType = 'regular'): Promise<void> => {
30
+ const baseBranch = getBaseBranch(type)
31
+
19
32
  $.quiet = true
20
33
 
21
34
  await $`git fetch origin`
22
- await $`git switch dev`
23
- await $`git pull origin dev`
35
+ await $`git switch ${baseBranch}`
36
+ await $`git pull origin ${baseBranch}`
24
37
 
25
38
  $.quiet = false
26
39
  }
27
40
 
41
+ interface CreateSingleReleaseArgs {
42
+ version: string
43
+ jiraConfig: JiraConfig
44
+ description?: string
45
+ type?: ReleaseType
46
+ }
47
+
28
48
  /**
29
49
  * Create a single release by creating both Jira version and GitHub release branch
30
- * @param version - Version number (e.g., "1.2.5")
31
- * @param jiraConfig - Jira configuration object
32
- * @param description - Optional description for the Jira version
33
- * @returns Release creation result with URLs and metadata
34
50
  */
35
- export const createSingleRelease = async (
36
- version: string,
37
- jiraConfig: JiraConfig,
38
- description?: string,
39
- ): Promise<ReleaseCreationResult> => {
51
+ export const createSingleRelease = async (args: CreateSingleReleaseArgs): Promise<ReleaseCreationResult> => {
52
+ const { version, jiraConfig, description, type = 'regular' } = args
40
53
  // 1. Create Jira version (mandatory)
41
54
  const versionName = `v${version}`
42
55
 
@@ -55,12 +68,94 @@ export const createSingleRelease = async (
55
68
  const jiraVersionUrl = `${jiraConfig.baseUrl}/projects/${result.version!.projectId}/versions/${result.version!.id}/tab/release-report-all-issues`
56
69
 
57
70
  // 2. Create GitHub release branch
58
- const releaseInfo = await createReleaseBranch({ version, jiraVersionUrl })
71
+ const releaseInfo = await createReleaseBranch({ version, jiraVersionUrl, type })
59
72
 
60
73
  return {
61
74
  version,
75
+ type,
62
76
  branchName: releaseInfo.branchName,
63
77
  prUrl: releaseInfo.prUrl,
64
78
  jiraVersionUrl,
65
79
  }
66
80
  }
81
+
82
+ /**
83
+ * Fetch Jira version descriptions mapped by version name (e.g., "v1.2.5" → "Some description")
84
+ * Gracefully returns empty map if Jira is unavailable
85
+ */
86
+ export const getJiraDescriptions = async (): Promise<Map<string, string>> => {
87
+ const descriptions = new Map<string, string>()
88
+
89
+ const jiraConfig = await loadJiraConfigOptional()
90
+
91
+ if (!jiraConfig) return descriptions
92
+
93
+ try {
94
+ const versions = await getProjectVersions(jiraConfig)
95
+
96
+ for (const version of versions) {
97
+ if (version.description) {
98
+ descriptions.set(version.name, version.description)
99
+ }
100
+ }
101
+ } catch {
102
+ // Jira fetch failed, continue without descriptions
103
+ }
104
+
105
+ return descriptions
106
+ }
107
+
108
+ /**
109
+ * Format a version string with its release type tag, e.g. "1.2.5 [regular]"
110
+ * When maxVersionLength is provided, pads the version for alignment.
111
+ */
112
+ export const formatVersionLabel = (version: string, type: ReleaseType, maxVersionLength?: number): string => {
113
+ const padding = maxVersionLength ? ' '.repeat(maxVersionLength - version.length + 3) : ' '
114
+
115
+ return `${version}${padding}[${type}]`
116
+ }
117
+
118
+ /**
119
+ * Detect release type from PR title.
120
+ * PRs titled "Hotfix v..." are hotfix, everything else is regular.
121
+ */
122
+ export const detectReleaseType = (title: string): ReleaseType => {
123
+ return title.toLowerCase().startsWith('hotfix') ? 'hotfix' : 'regular'
124
+ }
125
+
126
+ interface FormatBranchChoicesArgs {
127
+ branches: string[]
128
+ descriptions: Map<string, string>
129
+ types?: Map<string, ReleaseType>
130
+ }
131
+
132
+ /**
133
+ * Format release branch names as checkbox choices with aligned type tags and Jira descriptions
134
+ */
135
+ export const formatBranchChoices = (args: FormatBranchChoicesArgs): { name: string; value: string }[] => {
136
+ const { branches, descriptions, types } = args
137
+ const versionNames = branches.map((b) => {
138
+ return b.replace('release/v', '')
139
+ })
140
+
141
+ const maxLen = Math.max(
142
+ ...versionNames.map((v) => {
143
+ return v.length
144
+ }),
145
+ )
146
+
147
+ return branches.map((branch, i) => {
148
+ const version = versionNames[i] as string
149
+ const type = types ? types.get(branch) || 'regular' : undefined
150
+ const desc = descriptions.get(`v${version}`)
151
+ const padding = ' '.repeat(maxLen - version.length + 3)
152
+
153
+ let name = type ? formatVersionLabel(version, type, maxLen) : version
154
+
155
+ if (desc) {
156
+ name = type ? `${name} ${desc}` : `${version}${padding}${desc}`
157
+ }
158
+
159
+ return { name, value: branch }
160
+ })
161
+ }