Skip to content
Draft
Show file tree
Hide file tree
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
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 '[email protected]:your_jira_api_token' | base64

Copilot uses AI. Check for mistakes.
JIRA_API_TOKEN=
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"github-cherry-pick": "^1.1.0",
"md-to-adf": "^0.6.4",
"probot": "^13.4.3",
"semver": "^7.6.0"
},
Expand Down
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';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mdToAdf = require('md-to-adf') as (markdown: string) => { toJSON: () => { type: string; version: number; content: unknown[] } };

const JIRA_ISSUE_KEY_REGEX = /^[A-Z][A-Z0-9]+-\d+$/i;

interface AdfBlock {
type: string;
content?: unknown[];
attrs?: unknown;
}

function prBodyToAdfContent(body: string | null): AdfBlock[] {
const raw = body?.trim() || 'no description';
try {
const adf = mdToAdf(raw);
const json = adf?.toJSON?.();
const content = json?.content;
if (Array.isArray(content) && content.length > 0) {
return content as AdfBlock[];
}
} catch {
// fallback to plain text
}
return [
{
type: 'paragraph',
content: [{ type: 'text', text: `PR description: ${raw}` }],
},
];
}

export const isJiraTaskKey = (arg: string): boolean => JIRA_ISSUE_KEY_REGEX.test(arg.trim());

interface HandleJiraArg {
context: Context;
boardName: string;
parentTaskKey?: string;
pr: {
number: number;
title: string;
body: string | null;
html_url: string;
labels: string[];
milestone?: string | null;
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, parentTaskKey, pr, requestedBy }: 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 isSubtask = Boolean(parentTaskKey);
const projectKey = parentTaskKey ? parentTaskKey.replace(/-\d+$/, '') : boardName;

const payload = {
fields: {
project: {
key: projectKey,
},
...(isSubtask ? { parent: { key: parentTaskKey } } : {}),
summary: `[PR #${pr.number}] ${pr.title}`,
issuetype: {
name: isSubtask ? 'Sub-task' : 'Task',
},
...(hasCommunityLabel ? { labels: ['community'] } : {}),
...(pr.milestone?.trim() ? { fixVersions: [{ name: pr.milestone.trim() }] } : {}),
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:' }],
},
...prBodyToAdfContent(pr.body),
{
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.

return task.key ?? '';
};
59 changes: 59 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, isJiraTaskKey } from './handleJira';

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

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

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

try {
await handleJira({
context,
boardName: rawArg,
...(asSubtask ? { parentTaskKey: rawArg } : {}),
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),
milestone: pr.data.milestone?.title ?? undefined,
user: pr.data.user,
},
requestedBy: comment.user.login,
commentId: comment.id,
});

await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: '+1',
});
Comment on lines +246 to +251
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);
} finally {
await context.octokit.reactions.deleteForIssueComment({
...context.issue(),
comment_id: comment.id,
content: 'eyes',
});
}
}

if (command === 'jira' && !args?.trim()) {
// reacts with thinking face
await context.octokit.reactions.createForIssueComment({
...context.issue(),
comment_id: comment.id,
content: 'confused',
});
Comment on lines +269 to +274
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
9 changes: 9 additions & 0 deletions src/types/md-to-adf.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module 'md-to-adf' {
interface AdfDocument {
toJSON(): { type: string; version: number; content: unknown[] };
}

function mdToAdf(markdown: string): AdfDocument;

export = mdToAdf;
}
27 changes: 27 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2463,6 +2463,13 @@ __metadata:
languageName: node
linkType: hard

"adf-builder@npm:^3.3.0":
version: 3.3.0
resolution: "adf-builder@npm:3.3.0"
checksum: 10c0/9a0e763ef2fb1138c9177bb14259241d9e2c728481072844ae1d9776aa99ab1a45849a4047609ed971a5506d7e102cd5dcc9dd5e25e526175c665a1f99535bc0
languageName: node
linkType: hard

"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2":
version: 7.1.3
resolution: "agent-base@npm:7.1.3"
Expand Down Expand Up @@ -3286,6 +3293,7 @@ __metadata:
eslint-plugin-prettier: "npm:^5.4.1"
github-cherry-pick: "npm:^1.1.0"
jest: "npm:^29.0.0"
md-to-adf: "npm:^0.6.4"
nock: "npm:^13.0.5"
prettier: "npm:^3.5.3"
probot: "npm:^13.4.3"
Expand Down Expand Up @@ -5492,13 +5500,32 @@ __metadata:
languageName: node
linkType: hard

"marked@npm:^0.8.2":
version: 0.8.2
resolution: "marked@npm:0.8.2"
bin:
marked: bin/marked
checksum: 10c0/afb59f634b161819815bd7df00db3d86b9e52973624a14c90b8584f9d72939233ced90893bc17a4e61211b5de94d22f9d59cb83ba480c6331f81e721b079a15b
languageName: node
linkType: hard

"math-intrinsics@npm:^1.1.0":
version: 1.1.0
resolution: "math-intrinsics@npm:1.1.0"
checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f
languageName: node
linkType: hard

"md-to-adf@npm:^0.6.4":
version: 0.6.4
resolution: "md-to-adf@npm:0.6.4"
dependencies:
adf-builder: "npm:^3.3.0"
marked: "npm:^0.8.2"
checksum: 10c0/ae3a2ee27c12dc9ce481414a70bab112eba09b3315e8a58f836048deae4adbaaa46f7d81bbc153fb96e7f45b6bab652bbadfba17e29b5626c0221743a1d6a214
languageName: node
linkType: hard

"meant@npm:^1.0.1":
version: 1.0.3
resolution: "meant@npm:1.0.3"
Expand Down