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.
- package/.env.example +4 -0
- package/dist/cli.js +14 -12
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +14 -12
- package/dist/mcp.js.map +4 -4
- package/package.json +1 -1
- package/src/commands/gh-release-create/gh-release-create.ts +19 -74
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +26 -2
- package/src/commands/gh-release-deploy/gh-release-deploy.ts +1 -1
- package/src/commands/worktrees-add/worktrees-add.ts +1 -1
- package/src/commands/worktrees-sync/worktrees-sync.ts +1 -1
- package/src/entry/cli.ts +1 -2
- package/src/entry/mcp.ts +1 -2
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +31 -0
- package/src/integrations/gh/gh-release-prs/index.ts +1 -1
- package/src/integrations/gh/index.ts +1 -1
- package/src/integrations/jira/api.ts +239 -43
- package/src/integrations/jira/index.ts +5 -1
- package/src/integrations/jira/types.ts +30 -3
|
@@ -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
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
40
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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:
|
|
89
|
-
|
|
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
|
-
* @
|
|
273
|
+
* @throws Error with detailed message if configuration is missing or invalid
|
|
274
|
+
* @returns Promise<JiraConfig>
|
|
97
275
|
*/
|
|
98
|
-
export
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
300
|
+
const projectId = Number.parseInt(projectIdStr!, 10)
|
|
117
301
|
|
|
118
302
|
if (Number.isNaN(projectId)) {
|
|
119
|
-
|
|
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
|
|
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'
|