Skip to content

Commit 0e949c2

Browse files
committed
update jira script
1 parent 6bd8765 commit 0e949c2

File tree

2 files changed

+157
-25
lines changed

2 files changed

+157
-25
lines changed

update_jira/index.js

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,26 @@ const { Octokit } = require('@octokit/rest')
33
const Jira = require('./../utils/jira')
44

55
const statusMap = {
6-
'master': 'Deployed to Production',
7-
'main': 'Deployed to Production',
8-
'staging': 'Deployed to Staging',
9-
'dev': 'Deployed to Staging'
6+
'master': {
7+
status: 'Deployed to Production',
8+
fields: {}
9+
},
10+
'main': {
11+
status: 'Deployed to Production',
12+
fields: {}
13+
},
14+
'staging': {
15+
status: 'Deployed to Staging',
16+
fields: {
17+
resolution: 'Done',
18+
}
19+
},
20+
'dev': {
21+
status: 'Deployed to Staging',
22+
fields: {
23+
resolution: 'Done'
24+
}
25+
}
1026
}
1127

1228
run()
@@ -33,7 +49,6 @@ async function run() {
3349

3450
if (GITHUB_EVENT_NAME === 'pull_request' || GITHUB_EVENT_NAME === 'pull_request_target') {
3551
const eventData = require(GITHUB_EVENT_PATH)
36-
3752
await handlePullRequestEvent(eventData, jiraUtil, GITHUB_REPOSITORY)
3853
return
3954
}
@@ -54,6 +69,42 @@ async function run() {
5469
}
5570
}
5671

72+
/**
73+
* Prepare fields for Jira transition, converting names to IDs where needed
74+
*/
75+
async function prepareFields(fields, jiraUtil) {
76+
const preparedFields = {}
77+
78+
for (const [fieldName, fieldValue] of Object.entries(fields)) {
79+
if (fieldName === 'resolution' && typeof fieldValue === 'string') {
80+
// Look up resolution ID by name
81+
const resolutions = await jiraUtil.getFieldOptions('resolution')
82+
const resolution = resolutions.find(r => r.name === fieldValue)
83+
if (resolution) {
84+
preparedFields.resolution = { id: resolution.id }
85+
} else {
86+
console.warn(`Resolution "${fieldValue}" not found`)
87+
}
88+
} else if (fieldName === 'priority' && typeof fieldValue === 'string') {
89+
// Look up priority ID by name
90+
const priorities = await jiraUtil.getFieldOptions('priority')
91+
const priority = priorities.find(p => p.name === fieldValue)
92+
if (priority) {
93+
preparedFields.priority = { id: priority.id }
94+
}
95+
} else if (fieldName === 'assignee' && typeof fieldValue === 'string') {
96+
// For assignee, you might need to look up the user
97+
// This depends on your Jira configuration
98+
preparedFields.assignee = { name: fieldValue }
99+
} else {
100+
// Pass through other fields as-is
101+
preparedFields[fieldName] = fieldValue
102+
}
103+
}
104+
105+
return preparedFields
106+
}
107+
57108
/**
58109
* Handle pull request events (open, close, etc)
59110
*/
@@ -69,6 +120,7 @@ async function handlePullRequestEvent(eventData, jiraUtil) {
69120
console.log(`Found Jira issues: ${issueKeys.join(', ')}`)
70121

71122
let targetStatus = null
123+
let customFields = {}
72124
const targetBranch = pull_request.base.ref
73125

74126
switch (action) {
@@ -80,15 +132,21 @@ async function handlePullRequestEvent(eventData, jiraUtil) {
80132
case 'converted_to_draft':
81133
targetStatus = 'In Development'
82134
break
83-
case 'synchronize': {
135+
case 'synchronize':
84136
if (!pull_request.draft) {
85137
targetStatus = 'Code Review'
86138
}
87139
break
88-
}
89140
case 'closed':
90141
if (pull_request.merged) {
91-
targetStatus = statusMap[targetBranch] || 'Done'
142+
const branchConfig = statusMap[targetBranch]
143+
if (branchConfig) {
144+
targetStatus = branchConfig.status
145+
customFields = branchConfig.fields || {}
146+
} else {
147+
targetStatus = 'Done'
148+
customFields = { resolution: 'Done' }
149+
}
92150
} else {
93151
console.log('PR closed without merging, skipping status update')
94152
return
@@ -100,9 +158,11 @@ async function handlePullRequestEvent(eventData, jiraUtil) {
100158
}
101159

102160
if (targetStatus) {
161+
const preparedFields = await prepareFields(customFields, jiraUtil)
162+
103163
for (const issueKey of issueKeys) {
104164
try {
105-
await jiraUtil.transitionIssue(issueKey, targetStatus)
165+
await jiraUtil.transitionIssue(issueKey, targetStatus, ['Blocked', 'Rejected'], preparedFields)
106166
} catch (error) {
107167
console.error(`Failed to update ${issueKey}:`, error.message)
108168
}
@@ -128,16 +188,21 @@ async function handlePushEvent(branch, jiraUtil, githubRepository, githubToken)
128188
})
129189

130190
const { commit: { message: commitMessage } } = data
131-
const newStatus = statusMap[branch]
132-
if (!newStatus) {
191+
const branchConfig = statusMap[branch]
192+
if (!branchConfig) {
133193
console.log(`No status mapping for branch: ${branch}`)
134194
return
135195
}
136196

197+
const newStatus = branchConfig.status
198+
const customFields = branchConfig.fields || {}
199+
200+
const preparedFields = await prepareFields(customFields, jiraUtil)
201+
137202
// Handle special case: staging -> production bulk update
138203
if ((branch === 'master' || branch === 'main') && commitMessage.includes('from coursedog/staging')) {
139204
console.log('Bulk updating all Staging issues to Done')
140-
await jiraUtil.updateByStatus('Deployed to Staging', newStatus)
205+
await jiraUtil.updateByStatus('Deployed to Staging', newStatus, preparedFields)
141206
return
142207
}
143208

@@ -147,7 +212,7 @@ async function handlePushEvent(branch, jiraUtil, githubRepository, githubToken)
147212
const prNumber = prMatch[1]
148213
const prUrl = `${repositoryName}/pull/${prNumber}`
149214
console.log(`Updating issues mentioning PR ${prUrl} to status: ${newStatus}`)
150-
await jiraUtil.updateByPR(prUrl, newStatus)
215+
await jiraUtil.updateByPR(prUrl, newStatus, preparedFields)
151216
}
152217
}
153218

@@ -176,4 +241,3 @@ function extractJiraIssueKeys(pullRequest) {
176241

177242
return Array.from(keys)
178243
}
179-

utils/jira.js

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,9 @@ class Jira {
226226
* Search for issues with a specific status and update them
227227
* @param {string} currentStatus - Current status to search for
228228
* @param {string} newStatus - New status to transition to
229+
* @param {Object} fields - Additional fields to set during transition
229230
*/
230-
async updateByStatus(currentStatus, newStatus) {
231+
async updateByStatus(currentStatus, newStatus, fields = {}) {
231232
try {
232233
let jql = `status = "${currentStatus}"`
233234
const response = await this.request('/search', {
@@ -244,7 +245,7 @@ class Jira {
244245
console.log(`Found ${issues.length} issues in "${currentStatus}" status`)
245246

246247
for (const issue of issues) {
247-
await this.transitionIssue(issue.key, newStatus)
248+
await this.transitionIssue(issue.key, newStatus, ['Blocked', 'Rejected'], fields)
248249
}
249250

250251
return issues.length
@@ -258,8 +259,9 @@ class Jira {
258259
* Find issues that mention a PR URL and update their status
259260
* @param {string} prUrl - PR URL to search for (e.g., "myrepo/pull/123")
260261
* @param {string} newStatus - New status to transition to
262+
* @param {Object} fields - Additional fields to set during transition
261263
*/
262-
async updateByPR(prUrl, newStatus) {
264+
async updateByPR(prUrl, newStatus, fields = {}) {
263265
try {
264266
let jql = `text ~ "${prUrl}"`
265267
const response = await this.request('/search', {
@@ -276,7 +278,7 @@ class Jira {
276278
console.log(`Found ${issues.length} issues mentioning PR ${prUrl}`)
277279

278280
for (const issue of issues) {
279-
await this.transitionIssue(issue.key, newStatus)
281+
await this.transitionIssue(issue.key, newStatus, ['Blocked', 'Rejected'], fields)
280282
}
281283

282284
return issues.length
@@ -321,6 +323,54 @@ class Jira {
321323
}
322324
}
323325

326+
/**
327+
* Generic method to get field values by type
328+
* @param {string} fieldName - Field name (resolution, priority, etc)
329+
* @returns {Promise<Array>} Available options for the field
330+
*/
331+
async getFieldOptions(fieldName) {
332+
try {
333+
const fieldMappings = {
334+
'resolution': '/resolution',
335+
'priority': '/priority',
336+
'issuetype': '/issuetype',
337+
'component': '/component',
338+
'version': '/version'
339+
}
340+
341+
const endpoint = fieldMappings[fieldName]
342+
if (!endpoint) {
343+
console.log(`No endpoint mapping for field: ${fieldName}`)
344+
return []
345+
}
346+
347+
const response = await this.request(endpoint)
348+
const options = await response.json()
349+
return options
350+
} catch (error) {
351+
console.error(`Error getting ${fieldName} options:`, error.message)
352+
return []
353+
}
354+
}
355+
356+
/**
357+
* Get transition details including required fields
358+
* @param {string} issueKey - Jira issue key
359+
* @param {string} transitionId - Transition ID
360+
* @returns {Promise<Object>} Transition details
361+
*/
362+
async getTransitionDetails(issueKey, transitionId) {
363+
try {
364+
const response = await this.request(`/issue/${issueKey}/transitions?transitionId=${transitionId}&expand=transitions.fields`)
365+
const data = await response.json()
366+
const transition = data.transitions.find(t => t.id === transitionId)
367+
return transition || {}
368+
} catch (error) {
369+
console.error(`Error getting transition details:`, error.message)
370+
throw error
371+
}
372+
}
373+
324374
/**
325375
* Find the shortest path between two statuses using BFS, excluding paths through certain states
326376
* @param {Object} stateMachine - The workflow state machine
@@ -407,8 +457,9 @@ class Jira {
407457
* @param {string} issueKey - Jira issue key
408458
* @param {string} targetStatus - Target status name
409459
* @param {Array<string>} excludeStates - Array of state names to exclude from paths (optional)
460+
* @param {Object} fields - Additional fields to set during the final transition
410461
*/
411-
async transitionIssue(issueKey, targetStatusName, excludeStates = ['Blocked', 'Rejected']) {
462+
async transitionIssue(issueKey, targetStatusName, excludeStates = ['Blocked', 'Rejected'], fields = {}) {
412463
try {
413464
const issueResponse = await this.request(`/issue/${issueKey}?fields=status`)
414465
const issueData = await issueResponse.json()
@@ -439,7 +490,9 @@ class Jira {
439490
console.log(`Found shortest transition path with ${shortestPath.length} steps:`)
440491
shortestPath.forEach(t => console.log(` ${t.fromName}${t.toName} (${t.name})`))
441492

442-
for (const transition of shortestPath) {
493+
for (let i = 0; i < shortestPath.length; i++) {
494+
const transition = shortestPath[i]
495+
const isLastTransition = i === shortestPath.length - 1
443496
const availableTransitions = await this.getTransitions(issueKey)
444497

445498
const actualTransition = availableTransitions.find(t =>
@@ -453,13 +506,28 @@ class Jira {
453506
return false
454507
}
455508

509+
const transitionPayload = {
510+
transition: {
511+
id: actualTransition.id
512+
}
513+
}
514+
515+
if (isLastTransition && Object.keys(fields).length > 0) {
516+
transitionPayload.fields = fields
517+
}
518+
519+
const transitionDetails = await this.getTransitionDetails(issueKey, actualTransition.id)
520+
if (transitionDetails.fields) {
521+
for (const [fieldId, fieldInfo] of Object.entries(transitionDetails.fields)) {
522+
if (fieldInfo.required && !transitionPayload.fields?.[fieldId]) {
523+
console.warn(`Required field ${fieldId} (${fieldInfo.name}) not provided for transition to ${transition.toName}`)
524+
}
525+
}
526+
}
527+
456528
await this.request(`/issue/${issueKey}/transitions`, {
457529
method: 'POST',
458-
body: JSON.stringify({
459-
transition: {
460-
id: actualTransition.id
461-
}
462-
})
530+
body: JSON.stringify(transitionPayload)
463531
})
464532

465533
console.log(`✓ Transitioned ${issueKey}: ${transition.fromName}${transition.toName}`)

0 commit comments

Comments
 (0)