infra-kit 0.1.51 → 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.
@@ -1,12 +1,12 @@
1
- /* eslint-disable sonarjs/cognitive-complexity */
2
1
  import confirm from '@inquirer/confirm'
3
2
  import process from 'node:process'
4
3
  import { z } from 'zod'
5
4
  import { $, question } from 'zx'
6
5
 
6
+ import { createReleaseBranch } from 'src/integrations/gh'
7
+ import { createJiraVersion, loadJiraConfig } from 'src/integrations/jira'
7
8
  import { logger } from 'src/lib/logger'
8
9
  import type { RequiredConfirmedOptionArg, ToolsExecutionResult } from 'src/types'
9
- import { createJiraVersion, loadJiraConfig } from 'src/integrations/jira'
10
10
 
11
11
  interface GhReleaseCreateArgs extends RequiredConfirmedOptionArg {
12
12
  versions: string
@@ -23,18 +23,7 @@ export const ghReleaseCreate = async (args: GhReleaseCreateArgs): Promise<ToolsE
23
23
  let _checkout = checkout
24
24
 
25
25
  // Load Jira config - it is now mandatory
26
- const jiraConfig = loadJiraConfig()
27
-
28
- if (!jiraConfig) {
29
- logger.error('Jira configuration is required but not found.')
30
- logger.error('Please configure the following environment variables in your .env file:')
31
- logger.error(' - JIRA_BASE_URL (e.g., https://your-domain.atlassian.net)')
32
- logger.error(' - JIRA_TOKEN (your Jira API token)')
33
- logger.error(' - JIRA_PROJECT_ID (numeric project ID)')
34
- process.exit(1)
35
- }
36
-
37
- logger.info('Jira integration enabled - versions will be created in Jira')
26
+ const jiraConfig = await loadJiraConfig()
38
27
 
39
28
  if (versions) {
40
29
  versionBranches = versions
@@ -88,10 +77,7 @@ export const ghReleaseCreate = async (args: GhReleaseCreateArgs): Promise<ToolsE
88
77
  }> = []
89
78
 
90
79
  for (const version of versionsList) {
91
- // 1. Create GitHub release branch
92
- const releaseInfo = await createReleaseBranch(version)
93
-
94
- // 2. Create Jira version (mandatory)
80
+ // 1. Create Jira version (mandatory)
95
81
  const versionName = `v${version}`
96
82
 
97
83
  const result = await createJiraVersion(
@@ -105,28 +91,22 @@ export const ghReleaseCreate = async (args: GhReleaseCreateArgs): Promise<ToolsE
105
91
  jiraConfig,
106
92
  )
107
93
 
108
- if (result.success) {
109
- // Construct user-friendly Jira URL using project key from API response
110
- const projectKey = result.version!.project
111
- const jiraVersionUrl = projectKey
112
- ? `${jiraConfig.baseUrl}/projects/${projectKey}/versions/${result.version!.id}/tab/release-report-all-issues`
113
- : ''
114
-
115
- releases.push({
116
- version,
117
- branchName: releaseInfo.branchName,
118
- prUrl: releaseInfo.prUrl,
119
- jiraVersionUrl,
120
- })
94
+ // 2. Create GitHub release branch
95
+ const releaseInfo = await createReleaseBranch(version)
96
+
97
+ // Construct user-friendly Jira URL using project key from API response
98
+ const jiraVersionUrl = `${jiraConfig.baseUrl}/projects/${result.version!.projectId}/versions/${result.version!.id}/tab/release-report-all-issues`
99
+
100
+ releases.push({
101
+ version,
102
+ branchName: releaseInfo.branchName,
103
+ prUrl: releaseInfo.prUrl,
104
+ jiraVersionUrl,
105
+ })
121
106
 
122
- logger.info(`Successfully created release: ${versionName}`)
123
- logger.info(` GitHub PR: ${releaseInfo.prUrl}`)
124
- logger.info(` Jira Version: ${jiraVersionUrl}`)
125
- } else {
126
- logger.error(`✗ Failed to create Jira version for ${versionName}: ${result.error}`)
127
- logger.error('Jira version creation is mandatory. Exiting...')
128
- process.exit(1)
129
- }
107
+ logger.info(`✅ Successfully created release: ${versionName}`)
108
+ logger.info(`🔗 GitHub PR: ${releaseInfo.prUrl}`)
109
+ logger.info(`🔗 Jira Version: ${jiraVersionUrl} \n`)
130
110
  }
131
111
 
132
112
  // If checkout option is enabled and we created only one branch, checkout to it
@@ -162,41 +142,6 @@ export const ghReleaseCreate = async (args: GhReleaseCreateArgs): Promise<ToolsE
162
142
  }
163
143
  }
164
144
 
165
- // #region Declarations
166
-
167
- // Function to create a release branch
168
- async function createReleaseBranch(
169
- version: string,
170
- ): Promise<{ branchName: string; prUrl: string }> {
171
- const branchName = `release/v${version}`
172
-
173
- try {
174
- await $`git switch dev`
175
- await $`git pull origin dev`
176
- await $`git checkout -b ${branchName}`
177
- await $`git push -u origin ${branchName}`
178
- await $`git commit --allow-empty-message --allow-empty --message ''`
179
- await $`git push origin ${branchName}`
180
-
181
- // Create PR and capture URL
182
- const prResult =
183
- await $`gh pr create --title "Release v${version}" --body "Release v${version}" --base dev --head ${branchName} --json url`
184
-
185
- const prData = JSON.parse(prResult.stdout)
186
-
187
- await $`git switch dev`
188
-
189
- return {
190
- branchName,
191
- prUrl: prData.url,
192
- }
193
- } catch (error: unknown) {
194
- logger.error({ error, branchName }, `Error creating release branch ${branchName}`)
195
- throw error
196
- }
197
- }
198
- // #endregion Declarations
199
-
200
145
  // MCP Tool Registration
201
146
  export const ghReleaseCreateMcpTool = {
202
147
  name: 'gh-release-create',
@@ -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'
@@ -39,3 +39,34 @@ export const getReleasePRs = async (): Promise<string[]> => {
39
39
  process.exit(1)
40
40
  }
41
41
  }
42
+
43
+ // Function to create a release branch
44
+ export const createReleaseBranch = async (version: string): Promise<{ branchName: string; prUrl: string }> => {
45
+ const branchName = `release/v${version}`
46
+
47
+ try {
48
+ await $`git switch dev`
49
+ await $`git pull origin dev`
50
+ await $`git checkout -b ${branchName}`
51
+ await $`git push -u origin ${branchName}`
52
+ await $`git commit --allow-empty-message --allow-empty --message ''`
53
+ await $`git push origin ${branchName}`
54
+
55
+ // Create PR and capture URL
56
+ const prResult =
57
+ await $`gh pr create --title "Release v${version}" --body "Release v${version}" --base dev --head ${branchName}`
58
+
59
+ const prLink = prResult.stdout.trim()
60
+
61
+ await $`git switch dev`
62
+
63
+ return {
64
+ branchName,
65
+ prUrl: prLink,
66
+ }
67
+ } catch (error: unknown) {
68
+ logger.error({ error, branchName }, `Error creating release branch ${branchName}`)
69
+
70
+ throw error
71
+ }
72
+ }
@@ -1 +1 @@
1
- export { getReleasePRs } from './gh-release-prs'
1
+ export { createReleaseBranch, getReleasePRs } from './gh-release-prs'
@@ -1,2 +1,2 @@
1
1
  export { validateGitHubCliAndAuth } from './gh-cli-auth'
2
- export { getReleasePRs } from './gh-release-prs'
2
+ export { createReleaseBranch, getReleasePRs } from './gh-release-prs'
@@ -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
  /**
@@ -14,47 +19,51 @@ import type {
14
19
  * @param config - Jira configuration (baseUrl, token, projectId)
15
20
  * @returns Result containing created version or error
16
21
  */
17
- export async function createJiraVersion(
22
+ export const createJiraVersion = async (
18
23
  params: CreateJiraVersionParams,
19
24
  config: JiraConfig,
20
- ): Promise<CreateJiraVersionResult> {
25
+ ): Promise<CreateJiraVersionResult> => {
21
26
  try {
22
- const { baseUrl, token, projectId } = config
27
+ const { baseUrl, token, email, projectId } = config
23
28
 
24
29
  // Use current date if not provided
25
- const releaseDate =
26
- params.releaseDate || new Date().toISOString().split('T')[0]
30
+ // const releaseDate =
31
+ // params.releaseDate || new Date().toISOString().split('T')[0] // 2025-12-06
27
32
 
28
33
  // Prepare request body
29
34
  const requestBody = {
30
35
  name: params.name,
31
36
  projectId: params.projectId || projectId,
32
- description: params.description || `Release version ${params.name}`,
33
- releaseDate,
37
+ description: params.description || '',
38
+ // releaseDate,
34
39
  released: params.released ?? true,
35
40
  archived: params.archived ?? false,
36
41
  }
37
42
 
38
- logger.info(
39
- { version: params.name, projectId: requestBody.projectId },
40
- 'Creating Jira version',
41
- )
43
+ // logger.info(
44
+ // { version: params.name, projectId: requestBody.projectId },
45
+ // 'Creating Jira version',
46
+ // )
42
47
 
43
48
  // Make API request
44
49
  const url = `${baseUrl}/rest/api/3/version`
45
50
 
51
+ // Create Basic auth credentials
52
+ const credentials = btoa(`${email}:${token}`)
53
+
46
54
  const response = await fetch(url, {
47
55
  method: 'POST',
48
56
  headers: {
49
57
  Accept: 'application/json',
50
58
  'Content-Type': 'application/json',
51
- Authorization: `Bearer ${token}`,
59
+ Authorization: `Basic ${credentials}`,
52
60
  },
53
61
  body: JSON.stringify(requestBody),
54
62
  })
55
63
 
56
64
  if (!response.ok) {
57
65
  const errorText = await response.text()
66
+
58
67
  logger.error(
59
68
  {
60
69
  status: response.status,
@@ -64,18 +73,15 @@ export async function createJiraVersion(
64
73
  'Failed to create Jira version',
65
74
  )
66
75
 
67
- return {
68
- success: false,
69
- error: `HTTP ${response.status}: ${response.statusText}`,
70
- }
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
71
77
  }
72
78
 
73
79
  const version = (await response.json()) as JiraVersion
74
80
 
75
- logger.info(
76
- { versionId: version.id, versionName: version.name },
77
- 'Successfully created Jira version',
78
- )
81
+ // logger.info(
82
+ // { versionId: version.id, versionName: version.name },
83
+ // 'Successfully created Jira version',
84
+ // )
79
85
 
80
86
  return {
81
87
  success: true,
@@ -84,49 +90,239 @@ export async function createJiraVersion(
84
90
  } catch (error) {
85
91
  logger.error({ error }, 'Error creating Jira version')
86
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
+
87
221
  return {
88
- success: false,
89
- error: error instanceof Error ? error.message : String(error),
222
+ success: true,
223
+ version,
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`)
90
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
91
268
  }
92
269
  }
93
270
 
94
271
  /**
95
272
  * Loads Jira configuration from environment variables
96
- * @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>
97
275
  */
98
- export function loadJiraConfig(): JiraConfig | null {
276
+ export const loadJiraConfig = async (): Promise<JiraConfig> => {
99
277
  const baseUrl = process.env.JIRA_BASE_URL
100
278
  const token = process.env.JIRA_TOKEN || process.env.JIRA_API_TOKEN
101
279
  const projectIdStr = process.env.JIRA_PROJECT_ID
102
280
  const email = process.env.JIRA_EMAIL
103
281
 
104
- if (!baseUrl || !token || !projectIdStr) {
105
- logger.debug(
106
- {
107
- hasBaseUrl: !!baseUrl,
108
- hasToken: !!token,
109
- hasProjectId: !!projectIdStr,
110
- },
111
- 'Jira configuration incomplete, skipping Jira integration',
112
- )
113
- 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)
114
298
  }
115
299
 
116
- const projectId = Number.parseInt(projectIdStr, 10)
300
+ const projectId = Number.parseInt(projectIdStr!, 10)
117
301
 
118
302
  if (Number.isNaN(projectId)) {
119
- logger.warn(
120
- { projectIdStr },
121
- 'Invalid JIRA_PROJECT_ID (must be numeric), skipping Jira integration',
122
- )
123
- return null
303
+ throw new TypeError(`Invalid JIRA_PROJECT_ID: "${projectIdStr}" must be a numeric value (e.g., 10001)`)
124
304
  }
125
305
 
126
306
  return {
127
- baseUrl: baseUrl.replace(/\/$/, ''), // Remove trailing slash
128
- token,
307
+ baseUrl: baseUrl!.replace(/\/$/, ''), // Remove trailing slash
308
+ token: token!,
129
309
  projectId,
130
- 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
131
327
  }
132
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'