Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/handleJira.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ interface HandleJiraArg {
user?: {
login?: string;
} | null;
draft?: boolean;
state?: string;
merged?: boolean;
mergeable_state?: string;
};
requestedBy: string;
commentId: number;
Expand Down Expand Up @@ -120,7 +124,7 @@ export const handleJira = async ({ context, boardName, parentTaskKey, pr, reques
fields: {
project: { key: projectKey },
...(isSubtask ? { parent: { key: parentTaskKey } } : {}),
summary: `[PR #${pr.number}] ${pr.title}`,
summary: `${pr.title} [PR #${pr.number}]`,
issuetype: { name: isSubtask ? 'Sub-task' : 'Task' },
...(hasCommunityLabel ? { labels: ['community'] } : {}),
...(useFixVersions && milestoneName ? { fixVersions: [{ name: milestoneName }] } : {}),
Expand Down Expand Up @@ -183,6 +187,55 @@ export const handleJira = async ({ context, boardName, parentTaskKey, pr, reques
body: `${pr.body?.trim() || 'no description'} \n\n Task: [${task.key}]`,
});

async function updateJiraStatus(issueKey: string, targetStatus: string) {
const transitionsRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${issueKey}/transitions`);
if (!transitionsRes.ok) throw new Error('Failed to fetch Jira transitions');
const transitionsData = (await transitionsRes.json()) as { transitions: { id: string; to: { name: string } }[] };
const transitions = transitionsData.transitions;
const transition = transitions.find((t) => t.to.name.toLowerCase() === targetStatus.toLowerCase());
if (!transition) throw new Error(`No Jira transition found for status: ${targetStatus}`);
const transitionRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${issueKey}/transitions`, {
Comment on lines +191 to +197
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Jira transitions endpoints interpolate issueKey directly into the URL. To avoid path issues (and to be consistent with the existing projectHasVersion call), issueKey should be wrapped with encodeURIComponent when building /rest/api/3/issue/${...}/transitions.

Suggested change
const transitionsRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${issueKey}/transitions`);
if (!transitionsRes.ok) throw new Error('Failed to fetch Jira transitions');
const transitionsData = (await transitionsRes.json()) as { transitions: { id: string; to: { name: string } }[] };
const transitions = transitionsData.transitions;
const transition = transitions.find((t) => t.to.name.toLowerCase() === targetStatus.toLowerCase());
if (!transition) throw new Error(`No Jira transition found for status: ${targetStatus}`);
const transitionRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${issueKey}/transitions`, {
const transitionsRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`);
if (!transitionsRes.ok) throw new Error('Failed to fetch Jira transitions');
const transitionsData = (await transitionsRes.json()) as { transitions: { id: string; to: { name: string } }[] };
const transitions = transitionsData.transitions;
const transition = transitions.find((t) => t.to.name.toLowerCase() === targetStatus.toLowerCase());
if (!transition) throw new Error(`No Jira transition found for status: ${targetStatus}`);
const transitionRes = await jiraFetch(jiraBaseUrl, jiraApiToken, `/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`, {

Copilot uses AI. Check for mistakes.
method: 'POST',
body: JSON.stringify({ transition: { id: transition.id } }),
});
if (!transitionRes.ok) throw new Error(`Failed to transition Jira issue to ${targetStatus}`);
}
Comment on lines +192 to +202
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errors thrown from updateJiraStatus drop useful debugging info (HTTP status and response body). Including status and await res.text() in these error messages will make failures actionable, especially since this runs in automation and only posts the error message back to GitHub.

Copilot uses AI. Check for mistakes.

let jiraTargetStatus: string | null = null;

if (pr.merged) {
jiraTargetStatus = 'Done';
} else if (pr.mergeable_state && pr.mergeable_state.toLowerCase().includes('queue')) {
jiraTargetStatus = 'QA Tested';
} else if (pr.state === 'open' && pr.draft) {
jiraTargetStatus = 'In Progress';
} else if (pr.state === 'open') {
// Check for approval status
const { owner, repo } = context.repo();
Comment on lines +206 to +214
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Jira status transition logic relies on pr.merged, pr.mergeable_state, pr.state, and pr.draft, but the current command handler builds pr from the issue payload (see src/index.ts where only number/title/body/url/labels/milestone/user are passed). As a result these fields will be undefined and jiraTargetStatus will stay null, so the Jira transition never runs. Consider fetching the full PR inside handleJira (e.g., octokit.pulls.get) or making these fields required and updating the caller accordingly.

Suggested change
if (pr.merged) {
jiraTargetStatus = 'Done';
} else if (pr.mergeable_state && pr.mergeable_state.toLowerCase().includes('queue')) {
jiraTargetStatus = 'QA Tested';
} else if (pr.state === 'open' && pr.draft) {
jiraTargetStatus = 'In Progress';
} else if (pr.state === 'open') {
// Check for approval status
const { owner, repo } = context.repo();
// Fetch full PR details to ensure we have accurate status fields
const { owner, repo } = context.repo();
const pullRequest = await context.octokit.pulls.get({
owner,
repo,
pull_number: pr.number,
});
if (pullRequest.data.merged) {
jiraTargetStatus = 'Done';
} else if (pullRequest.data.mergeable_state && pullRequest.data.mergeable_state.toLowerCase().includes('queue')) {
jiraTargetStatus = 'QA Tested';
} else if (pullRequest.data.state === 'open' && pullRequest.data.draft) {
jiraTargetStatus = 'In Progress';
} else if (pullRequest.data.state === 'open') {
// Check for approval status

Copilot uses AI. Check for mistakes.
const reviews = await context.octokit.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
});
const approved = reviews.data.some((review: { state: string }) => review.state === 'APPROVED');
Comment on lines +215 to +220
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pulls.listReviews is paginated (defaults to 30). On PRs with many reviews, an older approval could be outside the first page and the status decision would be wrong. Use octokit.paginate (or request per_page: 100 and paginate) to ensure you consider all reviews, or avoid pagination entirely by using GraphQL reviewDecision.

Suggested change
const reviews = await context.octokit.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
});
const approved = reviews.data.some((review: { state: string }) => review.state === 'APPROVED');
const reviews = await context.octokit.paginate(
context.octokit.pulls.listReviews,
{
owner,
repo,
pull_number: pr.number,
per_page: 100,
},
);
const approved = reviews.some((review: { state: string }) => review.state === 'APPROVED');

Copilot uses AI. Check for mistakes.
if (approved) {
jiraTargetStatus = 'QA Tested';
} else {
jiraTargetStatus = 'Waiting Review';
}
Comment on lines +214 to +225
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviews.data.some(review.state === 'APPROVED') can report approved even when a later review requested changes (or the approval was dismissed), because the API returns a history of reviews. This can transition Jira to "QA Tested" incorrectly. Prefer using GraphQL reviewDecision (APPROVED/CHANGES_REQUESTED/REVIEW_REQUIRED) or computing the latest review state per reviewer before deciding.

Copilot uses AI. Check for mistakes.
}

if (task.key && jiraTargetStatus) {
try {
await updateJiraStatus(task.key, jiraTargetStatus);
} catch (err: unknown) {
await context.octokit.issues.createComment({
...context.issue(),
body: `⚠️ **Dionisio (Jira)**\n\nFailed to update Jira status to "${jiraTargetStatus}": ${(err as Error).message}`,
});
}
}

const warnings: string[] = [];
if (milestoneNotOnBoard && milestoneName) {
warnings.push(`The milestone **"${milestoneName}"** does not exist on the Jira board; the task was created without Fix version.`);
Expand Down