Skip to content
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ LOG_LEVEL=debug

# Go to https://smee.io/new set this to the URL that you are redirected to.
WEBHOOK_PROXY_URL=

JIRA_BASE_URL=
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Given the code sends Authorization: Basic ${JIRA_API_TOKEN}, it’s unclear what exact format the env var should contain (raw API token vs base64-encoded email:token). Add a short comment in the example env file describing the expected format (and any additional required Jira identity like email) to prevent misconfiguration.

Suggested change
JIRA_BASE_URL=
JIRA_BASE_URL=
# Base64-encoded "jira_email:api_token" used for HTTP Basic auth, e.g.:
# echo -n 'you@example.com:your_jira_api_token' | base64

Copilot uses AI. Check for mistakes.
JIRA_API_TOKEN=
135 changes: 135 additions & 0 deletions src/handleJira.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Context } from 'probot';

interface HandleJiraArg {
context: Context;
boardName: string;
pr: {
number: number;
title: string;
body: string | null;
html_url: string;
labels: string[];
user?: {
login?: string;
} | null;
};
requestedBy: string;
commentId: number;
}

const getEnv = (name: string): string => {
const value = process.env[name];

if (!value?.trim()) {
throw new Error(`Missing required env var: ${name}`);
}

return value.trim();
};

export const handleJira = async ({ context, boardName, pr, requestedBy, commentId }: HandleJiraArg): Promise<string> => {
const jiraBaseUrl = getEnv('JIRA_BASE_URL').replace(/\/$/, '');
const jiraApiToken = getEnv('JIRA_API_TOKEN');
const hasCommunityLabel = pr.labels.some((label) => label.toLowerCase() === 'community');

const payload = {
fields: {
project: {
key: boardName,
},
summary: `[PR #${pr.number}] ${pr.title}`,
issuetype: {
name: 'Task',
},
...(hasCommunityLabel ? { labels: ['community'] } : {}),
description: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Task automatically created by dionisio-bot.',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: `PR: ${pr.html_url}`,
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: `PR description: ${pr.body?.trim() || 'no description'}`,
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: `PR author: ${pr.user?.login ?? 'unknown'}`,
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: `Requested by: ${requestedBy}`,
},
],
},
],
},
},
};

const response = await fetch(`${jiraBaseUrl}/rest/api/3/issue`, {
method: 'POST',
headers: {
'Authorization': `Basic ${jiraApiToken}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});

if (!response.ok) {
const body = await response.text();
throw new Error(`Jira request failed (${response.status}): ${body}`);
}
const task = (await response.json()) as { key?: string };

await context.octokit.issues.update({
...context.issue(),
body: `${pr.body?.trim() || 'no description'} \n\n Task: [${task.key}]`,
});
Comment on lines +129 to +132
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This updates the PR/issue body before verifying that the Jira response actually included an issue key, and it can end up writing Task: [undefined] into the PR description. Validate the Jira response (including key) first, then update GitHub with a well-formed link/identifier.

Copilot uses AI. Check for mistakes.

const data = (await response.json()) as { key?: string };
const issueKey = data.key;

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

response.json() is consumed twice (once into task and again into data). A Fetch Response body can only be read once, so the second call will throw at runtime. Parse the JSON once, validate the returned issue key, and reuse that object for subsequent steps.

Copilot uses AI. Check for mistakes.
if (!issueKey) {
throw new Error('Jira response did not return issue key');
}

const issueUrl = `${jiraBaseUrl}/browse/${issueKey}`;

await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: commentId,
content: '+1',
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This handler adds a +1 reaction itself, but src/index.ts also adds a +1 reaction on success. GitHub rejects duplicate reactions from the same user, so this can turn a successful Jira creation into a caught error and a -1 reaction. Keep the success reaction in only one place (preferably in the command handler in index.ts for consistency with other commands).

Suggested change
await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: commentId,
content: '+1',
});

Copilot uses AI. Check for mistakes.
return issueUrl;
};
50 changes: 50 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { handleBackport } from './handleBackport';
import { run } from './Queue';
import { consoleProps } from './createPullRequest';
import { handleRebase } from './handleRebase';
import { handleJira } from './handleJira';

export = (app: Probot) => {
app.log.useLevelLabels = false;
Expand Down Expand Up @@ -214,6 +215,55 @@ export = (app: Probot) => {
);
}
}

if (command === 'jira' && args?.trim()) {
const boardName = args.trim().replace(/^["']|["']$/g, '');

await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: 'eyes',
});

try {
await handleJira({
context,
boardName,
pr: {
number: pr.data.number,
title: pr.data.title,
body: pr.data.body,
html_url: pr.data.html_url,
labels: pr.data.labels.map((label) => label.name),
user: pr.data.user,
},
requestedBy: comment.user.login,
commentId: comment.id,
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

handleJira() returns an issue URL, but the return value is ignored. Either use it (e.g., post a comment with the created Jira link) or change handleJira to return void to avoid suggesting an unused result.

Copilot uses AI. Check for mistakes.

await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: '+1',
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This +1 reaction duplicates the +1 reaction currently created inside handleJira(), which can cause a 422 from the GitHub API for duplicate reactions and flip a success into the catch path. Ensure only one success reaction is created (either here or inside handleJira, not both).

Suggested change
await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: '+1',
});

Copilot uses AI. Check for mistakes.
} catch (e) {
await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: '-1',
});
console.log('handleJira->', e);
}
}

if (command === 'jira' && !args?.trim()) {
// reacts with thinking face
await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: 'confused',
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Comment says "thinking face" but the reaction used is confused. Update the comment to match the actual reaction (or change the reaction if a different one was intended).

Copilot uses AI. Check for mistakes.
}
});

app.on(['check_suite.requested'], async function check(context) {
Expand Down
Loading