infra-kit 0.1.52 → 0.1.53

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.
@@ -5,6 +5,7 @@ import { z } from 'zod'
5
5
  import { $ } from 'zx'
6
6
 
7
7
  import { getReleasePRs } from 'src/integrations/gh'
8
+ import { deliverJiraRelease, loadJiraConfigOptional } from 'src/integrations/jira'
8
9
  import { logger } from 'src/lib/logger'
9
10
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
10
11
 
@@ -55,9 +56,7 @@ export const ghReleaseDeliver = async (args: GhReleaseDeliverArgs): Promise<Tool
55
56
  $.quiet = true
56
57
 
57
58
  await $`gh pr merge ${selectedReleaseBranch} --squash --admin --delete-branch` // TODO: add --body (AI for generate message)
58
-
59
59
  await $`gh pr create --base main --head dev --title "Release v${selectedReleaseBranch.replace('release/v', '')} (RC)" --body ""`
60
-
61
60
  await $`gh pr merge dev --squash --admin`
62
61
 
63
62
  $.quiet = false
@@ -71,6 +70,31 @@ export const ghReleaseDeliver = async (args: GhReleaseDeliverArgs): Promise<Tool
71
70
 
72
71
  $.quiet = false
73
72
 
73
+ // Deliver Jira release if Jira is configured
74
+ const jiraConfig = await loadJiraConfigOptional()
75
+
76
+ if (jiraConfig) {
77
+ try {
78
+ const versionName = selectedReleaseBranch.replace('release/', '')
79
+
80
+ await deliverJiraRelease({ versionName }, jiraConfig)
81
+ // const result = await deliverJiraRelease({ versionName }, jiraConfig)
82
+
83
+ // logger.info(
84
+ // {
85
+ // // versionId: result.version.id,
86
+ // versionName: result.version.name,
87
+ // // releaseDate: result.version.releaseDate,
88
+ // },
89
+ // 'Successfully delivered Jira release',
90
+ // )
91
+ } catch (error) {
92
+ logger.error({ error }, 'Failed to deliver Jira release (non-blocking)')
93
+ }
94
+ } else {
95
+ logger.info('🔔 Jira is not configured, skipping Jira release delivery')
96
+ }
97
+
74
98
  logger.info(`Successfully delivered ${selectedReleaseBranch} to production!`)
75
99
 
76
100
  const structuredContent = {
@@ -3,8 +3,8 @@ import process from 'node:process'
3
3
  import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
- import { ENVs } from 'src/lib/constants'
7
6
  import { getReleasePRs } from 'src/integrations/gh'
7
+ import { ENVs } from 'src/lib/constants'
8
8
  import { logger } from 'src/lib/logger'
9
9
  import type { ToolsExecutionResult } from 'src/types'
10
10
 
@@ -4,8 +4,8 @@ import process from 'node:process'
4
4
  import { z } from 'zod'
5
5
  import { $ } from 'zx'
6
6
 
7
- import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
8
7
  import { getReleasePRs } from 'src/integrations/gh'
8
+ import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
9
9
  import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
10
10
  import { logger } from 'src/lib/logger'
11
11
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
@@ -3,8 +3,8 @@ import process from 'node:process'
3
3
  import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
- import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
7
6
  import { getReleasePRs } from 'src/integrations/gh'
7
+ import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'
8
8
  import { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'
9
9
  import { logger } from 'src/lib/logger'
10
10
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
package/src/entry/cli.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { loadEnvFromGitRoot } from 'src/lib/load-env'
2
-
3
1
  import { Command } from 'commander'
4
2
 
5
3
  // Commands
@@ -14,6 +12,7 @@ import { worktreesRemove } from 'src/commands/worktrees-remove'
14
12
  import { worktreesSync } from 'src/commands/worktrees-sync'
15
13
  // Integrations
16
14
  import { validateGitHubCliAndAuth } from 'src/integrations/gh'
15
+ import { loadEnvFromGitRoot } from 'src/lib/load-env'
17
16
 
18
17
  // Load .env before anything else
19
18
  await loadEnvFromGitRoot()
package/src/entry/mcp.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable antfu/no-top-level-await */
2
- import { loadEnvFromGitRoot } from 'src/lib/load-env'
3
-
4
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
3
  import process from 'node:process'
6
4
 
7
5
  import { setupErrorHandlers } from 'src/lib/error-handlers'
6
+ import { loadEnvFromGitRoot } from 'src/lib/load-env'
8
7
  import { initLoggerMcp } from 'src/lib/logger'
9
8
 
10
9
  import { createMcpServer } from '../mcp/server'
@@ -41,9 +41,7 @@ export const getReleasePRs = async (): Promise<string[]> => {
41
41
  }
42
42
 
43
43
  // Function to create a release branch
44
- export const createReleaseBranch = async (
45
- version: string,
46
- ): Promise<{ branchName: string; prUrl: string }> => {
44
+ export const createReleaseBranch = async (version: string): Promise<{ branchName: string; prUrl: string }> => {
47
45
  const branchName = `release/v${version}`
48
46
 
49
47
  try {
@@ -68,6 +66,7 @@ export const createReleaseBranch = async (
68
66
  }
69
67
  } catch (error: unknown) {
70
68
  logger.error({ error, branchName }, `Error creating release branch ${branchName}`)
69
+
71
70
  throw error
72
71
  }
73
72
  }
@@ -1,11 +1,16 @@
1
1
  import process from 'node:process'
2
2
 
3
3
  import { logger } from 'src/lib/logger'
4
+
4
5
  import type {
5
6
  CreateJiraVersionParams,
6
7
  CreateJiraVersionResult,
8
+ DeliverJiraReleaseParams,
9
+ DeliverJiraReleaseResult,
7
10
  JiraConfig,
8
11
  JiraVersion,
12
+ UpdateJiraVersionParams,
13
+ UpdateJiraVersionResult,
9
14
  } from './types.js'
10
15
 
11
16
  /**
@@ -30,7 +35,7 @@ export const createJiraVersion = async (
30
35
  name: params.name,
31
36
  projectId: params.projectId || projectId,
32
37
  description: params.description || '',
33
- // releaseDate,
38
+ // releaseDate,
34
39
  released: params.released ?? true,
35
40
  archived: params.archived ?? false,
36
41
  }
@@ -58,6 +63,7 @@ export const createJiraVersion = async (
58
63
 
59
64
  if (!response.ok) {
60
65
  const errorText = await response.text()
66
+
61
67
  logger.error(
62
68
  {
63
69
  status: response.status,
@@ -67,10 +73,7 @@ export const createJiraVersion = async (
67
73
  'Failed to create Jira version',
68
74
  )
69
75
 
70
- return {
71
- success: false,
72
- error: `HTTP ${response.status}: ${response.statusText}`,
73
- }
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
74
77
  }
75
78
 
76
79
  const version = (await response.json()) as JiraVersion
@@ -87,50 +90,239 @@ export const createJiraVersion = async (
87
90
  } catch (error) {
88
91
  logger.error({ error }, 'Error creating Jira version')
89
92
 
93
+ throw error
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Gets all versions for a project from Jira
99
+ * @param config - Jira configuration
100
+ * @returns Array of JiraVersion objects
101
+ */
102
+ const getProjectVersions = async (config: JiraConfig): Promise<JiraVersion[]> => {
103
+ try {
104
+ const { baseUrl, token, email, projectId } = config
105
+
106
+ const url = `${baseUrl}/rest/api/3/project/${projectId}/versions`
107
+ const credentials = btoa(`${email}:${token}`)
108
+
109
+ const response = await fetch(url, {
110
+ method: 'GET',
111
+ headers: {
112
+ Accept: 'application/json',
113
+ Authorization: `Basic ${credentials}`,
114
+ },
115
+ })
116
+
117
+ if (!response.ok) {
118
+ const errorText = await response.text()
119
+
120
+ logger.error(
121
+ {
122
+ status: response.status,
123
+ statusText: response.statusText,
124
+ error: errorText,
125
+ },
126
+ 'Failed to get Jira project versions',
127
+ )
128
+
129
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
130
+ }
131
+
132
+ const versions = (await response.json()) as JiraVersion[]
133
+
134
+ return versions
135
+ } catch (error) {
136
+ logger.error({ error }, 'Error getting Jira project versions')
137
+
138
+ throw error
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Finds a Jira version by name in the project
144
+ * @param versionName - Name of the version to find (e.g., "v1.33.10")
145
+ * @param config - Jira configuration
146
+ * @returns JiraVersion if found, null otherwise
147
+ */
148
+ const findVersionByName = async (versionName: string, config: JiraConfig): Promise<JiraVersion | null> => {
149
+ try {
150
+ const versions = await getProjectVersions(config)
151
+ const version = versions.find((v) => v.name === versionName)
152
+
153
+ return version || null
154
+ } catch (error) {
155
+ logger.error({ error, versionName }, 'Error finding Jira version by name')
156
+
157
+ throw error
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Updates an existing Jira version
163
+ * @param params - Update parameters
164
+ * @param config - Jira configuration
165
+ * @returns Result containing updated version or error
166
+ */
167
+ const updateJiraVersion = async (
168
+ params: UpdateJiraVersionParams,
169
+ config: JiraConfig,
170
+ ): Promise<UpdateJiraVersionResult> => {
171
+ try {
172
+ const { baseUrl, token, email } = config
173
+
174
+ // Prepare request body - only include fields that are provided
175
+ const requestBody: Record<string, any> = {
176
+ released: params.released ?? true,
177
+ archived: params.archived ?? false,
178
+ }
179
+
180
+ // Add releaseDate if provided, otherwise use current date when releasing
181
+ if (params.releaseDate) {
182
+ requestBody.releaseDate = params.releaseDate
183
+ } else if (params.released !== false) {
184
+ requestBody.releaseDate = new Date().toISOString().split('T')[0] // YYYY-MM-DD
185
+ }
186
+
187
+ if (params.description !== undefined) {
188
+ requestBody.description = params.description
189
+ }
190
+
191
+ const url = `${baseUrl}/rest/api/3/version/${params.versionId}`
192
+ const credentials = btoa(`${email}:${token}`)
193
+
194
+ const response = await fetch(url, {
195
+ method: 'PUT',
196
+ headers: {
197
+ Accept: 'application/json',
198
+ 'Content-Type': 'application/json',
199
+ Authorization: `Basic ${credentials}`,
200
+ },
201
+ body: JSON.stringify(requestBody),
202
+ })
203
+
204
+ if (!response.ok) {
205
+ const errorText = await response.text()
206
+
207
+ logger.error(
208
+ {
209
+ status: response.status,
210
+ statusText: response.statusText,
211
+ error: errorText,
212
+ },
213
+ 'Failed to update Jira version',
214
+ )
215
+
216
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
217
+ }
218
+
219
+ const version = (await response.json()) as JiraVersion
220
+
90
221
  return {
91
- success: false,
92
- error: error instanceof Error ? error.message : String(error),
222
+ success: true,
223
+ version,
93
224
  }
225
+ } catch (error) {
226
+ logger.error({ error }, 'Error updating Jira version')
227
+
228
+ throw error
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Delivers a Jira release by marking it as released with the current date
234
+ * @param params - Parameters containing the version name
235
+ * @param config - Jira configuration
236
+ * @returns Result containing updated version
237
+ * @throws Error if version not found or update fails
238
+ */
239
+ export const deliverJiraRelease = async (
240
+ params: DeliverJiraReleaseParams,
241
+ config: JiraConfig,
242
+ ): Promise<DeliverJiraReleaseResult> => {
243
+ try {
244
+ const { versionName } = params
245
+
246
+ // Find the version by name
247
+ const version = await findVersionByName(versionName, config)
248
+
249
+ if (!version) {
250
+ logger.error({ versionName }, 'Jira version not found')
251
+ throw new Error(`Version "${versionName}" not found in Jira project`)
252
+ }
253
+
254
+ // Update the version to mark it as released
255
+ const result = await updateJiraVersion(
256
+ {
257
+ versionId: version.id,
258
+ released: true,
259
+ releaseDate: new Date().toISOString().split('T')[0], // Current date in YYYY-MM-DD format
260
+ },
261
+ config,
262
+ )
263
+
264
+ return result
265
+ } catch (error) {
266
+ logger.error({ error }, 'Error delivering Jira release')
267
+ throw error
94
268
  }
95
269
  }
96
270
 
97
271
  /**
98
272
  * Loads Jira configuration from environment variables
99
- * @returns Jira config if all required env vars are present, null otherwise
273
+ * @throws Error with detailed message if configuration is missing or invalid
274
+ * @returns Promise<JiraConfig>
100
275
  */
101
- export const loadJiraConfig = (): JiraConfig | null => {
276
+ export const loadJiraConfig = async (): Promise<JiraConfig> => {
102
277
  const baseUrl = process.env.JIRA_BASE_URL
103
278
  const token = process.env.JIRA_TOKEN || process.env.JIRA_API_TOKEN
104
279
  const projectIdStr = process.env.JIRA_PROJECT_ID
105
280
  const email = process.env.JIRA_EMAIL
106
281
 
107
- if (!baseUrl || !token || !projectIdStr || !email) {
108
- logger.debug(
109
- {
110
- hasBaseUrl: !!baseUrl,
111
- hasToken: !!token,
112
- hasProjectId: !!projectIdStr,
113
- hasEmail: !!email,
114
- },
115
- 'Jira configuration incomplete, skipping Jira integration',
116
- )
117
- return null
282
+ const missingVars: string[] = []
283
+ if (!baseUrl) missingVars.push('JIRA_BASE_URL (e.g., https://your-domain.atlassian.net)')
284
+ if (!token) missingVars.push('JIRA_TOKEN or JIRA_API_TOKEN (your Jira API token)')
285
+ if (!projectIdStr) missingVars.push('JIRA_PROJECT_ID (numeric project ID)')
286
+ if (!email) missingVars.push('JIRA_EMAIL (your Jira email address)')
287
+
288
+ if (missingVars.length > 0) {
289
+ const errorMessage = [
290
+ 'Jira configuration is required but incomplete.',
291
+ 'Please configure the following environment variables:',
292
+ ...missingVars.map((v) => ` - ${v}`),
293
+ '',
294
+ 'You can set these in your .env file or as environment variables.',
295
+ ].join('\n')
296
+
297
+ throw new Error(errorMessage)
118
298
  }
119
299
 
120
- const projectId = Number.parseInt(projectIdStr, 10)
300
+ const projectId = Number.parseInt(projectIdStr!, 10)
121
301
 
122
302
  if (Number.isNaN(projectId)) {
123
- logger.warn(
124
- { projectIdStr },
125
- 'Invalid JIRA_PROJECT_ID (must be numeric), skipping Jira integration',
126
- )
127
- return null
303
+ throw new TypeError(`Invalid JIRA_PROJECT_ID: "${projectIdStr}" must be a numeric value (e.g., 10001)`)
128
304
  }
129
305
 
130
306
  return {
131
- baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash
132
- token,
307
+ baseUrl: baseUrl!.replace(/\/$/, ''), // Remove trailing slash
308
+ token: token!,
133
309
  projectId,
134
- email,
310
+ email: email!,
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Attempts to load Jira configuration from environment variables
316
+ * Returns null if configuration is missing or invalid (for optional Jira integration)
317
+ * @returns Promise<JiraConfig | null>
318
+ */
319
+ export const loadJiraConfigOptional = async (): Promise<JiraConfig | null> => {
320
+ try {
321
+ const config = await loadJiraConfig()
322
+
323
+ return config
324
+ } catch (error) {
325
+ logger.warn({ error }, 'Jira configuration not available, skipping Jira integration')
326
+ return null
135
327
  }
136
328
  }
@@ -1,7 +1,11 @@
1
- export { createJiraVersion, loadJiraConfig } from './api.js'
1
+ export { createJiraVersion, deliverJiraRelease, loadJiraConfig, loadJiraConfigOptional } from './api.js'
2
2
  export type {
3
3
  CreateJiraVersionParams,
4
4
  CreateJiraVersionResult,
5
+ DeliverJiraReleaseParams,
6
+ DeliverJiraReleaseResult,
5
7
  JiraConfig,
6
8
  JiraVersion,
9
+ UpdateJiraVersionParams,
10
+ UpdateJiraVersionResult,
7
11
  } from './types.js'
@@ -53,6 +53,33 @@ export interface JiraConfig {
53
53
 
54
54
  export interface CreateJiraVersionResult {
55
55
  success: boolean
56
- version?: JiraVersion
57
- error?: string
56
+ version: JiraVersion
57
+ }
58
+
59
+ export interface UpdateJiraVersionParams {
60
+ /** ID of the version to update */
61
+ versionId: string
62
+ /** Whether the version is released */
63
+ released?: boolean
64
+ /** Release date in ISO format (YYYY-MM-DD) */
65
+ releaseDate?: string
66
+ /** Description of the version */
67
+ description?: string
68
+ /** Whether the version is archived */
69
+ archived?: boolean
70
+ }
71
+
72
+ export interface UpdateJiraVersionResult {
73
+ success: boolean
74
+ version: JiraVersion
75
+ }
76
+
77
+ export interface DeliverJiraReleaseParams {
78
+ /** Name of the version to deliver (e.g., "v1.33.10") */
79
+ versionName: string
80
+ }
81
+
82
+ export interface DeliverJiraReleaseResult {
83
+ success: boolean
84
+ version: JiraVersion
58
85
  }