@bike4mind/cli 0.2.46-fix-empty-text-content-block.20674 → 0.2.47-docs-clawdbot-comparison-analysis.20779
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/dist/{artifactExtractor-ABBCB64S.js → artifactExtractor-YPTNCKUP.js} +1 -1
- package/dist/{chunk-3ZQBWORW.js → chunk-4CRFUDAO.js} +2 -2
- package/dist/{chunk-MSB2SJJN.js → chunk-DO4DW2KK.js} +2 -2
- package/dist/{chunk-NSSADS6L.js → chunk-DOG7VPYY.js} +2 -2
- package/dist/{chunk-ND3GQYII.js → chunk-F22NXIOA.js} +8 -6
- package/dist/{chunk-GBJZWQ4U.js → chunk-IHZWVZA3.js} +1 -1
- package/dist/{chunk-TIDADNEG.js → chunk-MLAA2FGM.js} +43 -12
- package/dist/{chunk-4OFEAY5V.js → chunk-XOS5NYUQ.js} +60 -11
- package/dist/{chunk-LE6YGEIO.js → chunk-YIYLUPY4.js} +1399 -1260
- package/dist/commands/doctorCommand.js +1 -1
- package/dist/commands/headlessCommand.js +7 -7
- package/dist/commands/mcpCommand.js +2 -2
- package/dist/commands/updateCommand.js +1 -1
- package/dist/{create-JOUEDVPN.js → create-ZOAH5JSZ.js} +3 -3
- package/dist/index.js +61 -10
- package/dist/{llmMarkdownGenerator-EM2KVQMT.js → llmMarkdownGenerator-VANZUWX2.js} +1 -1
- package/dist/{markdownGenerator-W6EATTZM.js → markdownGenerator-43NO455H.js} +1 -1
- package/dist/{mementoService-WB7DZYBB.js → mementoService-GEQW3GJR.js} +3 -3
- package/dist/{src-P3OCGWLN.js → src-K6B2CD3H.js} +17 -1
- package/dist/{src-BROQPTKL.js → src-PXTRBTJE.js} +2 -2
- package/dist/{subtractCredits-QDM7MAU6.js → subtractCredits-6H5JMD3A.js} +3 -3
- package/package.json +8 -6
|
@@ -1677,6 +1677,83 @@ var CliToolResponseAction = z10.object({
|
|
|
1677
1677
|
content: z10.unknown().optional(),
|
|
1678
1678
|
error: z10.string().optional()
|
|
1679
1679
|
});
|
|
1680
|
+
var KeepCommandRequestAction = z10.object({
|
|
1681
|
+
action: z10.literal("keep_command_request"),
|
|
1682
|
+
accessToken: z10.string().optional(),
|
|
1683
|
+
commandType: z10.enum(["read_file", "list_directory", "run_tool"]),
|
|
1684
|
+
params: z10.record(z10.string(), z10.unknown()),
|
|
1685
|
+
requestId: z10.string()
|
|
1686
|
+
});
|
|
1687
|
+
var KeepCommandAction = z10.object({
|
|
1688
|
+
action: z10.literal("keep_command"),
|
|
1689
|
+
commandType: z10.enum(["read_file", "list_directory", "run_tool"]),
|
|
1690
|
+
params: z10.record(z10.string(), z10.unknown()),
|
|
1691
|
+
requestId: z10.string(),
|
|
1692
|
+
originConnectionId: z10.string()
|
|
1693
|
+
});
|
|
1694
|
+
var KeepCommandResponseAction = z10.object({
|
|
1695
|
+
action: z10.literal("keep_command_response"),
|
|
1696
|
+
requestId: z10.string(),
|
|
1697
|
+
originConnectionId: z10.string(),
|
|
1698
|
+
success: z10.boolean(),
|
|
1699
|
+
result: z10.unknown().optional(),
|
|
1700
|
+
error: z10.string().optional()
|
|
1701
|
+
});
|
|
1702
|
+
var KeepCommandResultAction = z10.object({
|
|
1703
|
+
action: z10.literal("keep_command_result"),
|
|
1704
|
+
requestId: z10.string(),
|
|
1705
|
+
success: z10.boolean(),
|
|
1706
|
+
result: z10.unknown().optional(),
|
|
1707
|
+
error: z10.string().optional()
|
|
1708
|
+
});
|
|
1709
|
+
var TilePositionSchema = z10.object({ x: z10.number(), y: z10.number() });
|
|
1710
|
+
var SceneCommandSchema = z10.discriminatedUnion("type", [
|
|
1711
|
+
z10.object({
|
|
1712
|
+
type: z10.literal("add_entity"),
|
|
1713
|
+
params: z10.object({
|
|
1714
|
+
id: z10.string(),
|
|
1715
|
+
spriteSheetId: z10.string(),
|
|
1716
|
+
position: TilePositionSchema,
|
|
1717
|
+
facing: z10.enum(["left", "right"]).optional(),
|
|
1718
|
+
animation: z10.string().optional(),
|
|
1719
|
+
visible: z10.boolean().optional(),
|
|
1720
|
+
zIndex: z10.number().optional(),
|
|
1721
|
+
metadata: z10.record(z10.string(), z10.unknown()).optional()
|
|
1722
|
+
})
|
|
1723
|
+
}),
|
|
1724
|
+
z10.object({ type: z10.literal("remove_entity"), id: z10.string() }),
|
|
1725
|
+
z10.object({
|
|
1726
|
+
type: z10.literal("walk_to"),
|
|
1727
|
+
id: z10.string(),
|
|
1728
|
+
target: TilePositionSchema,
|
|
1729
|
+
options: z10.object({ speed: z10.number().optional() }).optional()
|
|
1730
|
+
}),
|
|
1731
|
+
z10.object({ type: z10.literal("teleport"), id: z10.string(), position: TilePositionSchema }),
|
|
1732
|
+
z10.object({ type: z10.literal("play_animation"), id: z10.string(), animation: z10.string(), loop: z10.boolean().optional() }),
|
|
1733
|
+
z10.object({
|
|
1734
|
+
type: z10.literal("show_speech"),
|
|
1735
|
+
id: z10.string(),
|
|
1736
|
+
text: z10.string(),
|
|
1737
|
+
options: z10.object({
|
|
1738
|
+
duration: z10.number().optional(),
|
|
1739
|
+
style: z10.enum(["speech", "thought", "shout"]).optional()
|
|
1740
|
+
}).optional()
|
|
1741
|
+
}),
|
|
1742
|
+
z10.object({ type: z10.literal("clear_speech"), id: z10.string() }),
|
|
1743
|
+
z10.object({ type: z10.literal("set_visible"), id: z10.string(), visible: z10.boolean() }),
|
|
1744
|
+
z10.object({ type: z10.literal("set_facing"), id: z10.string(), facing: z10.enum(["left", "right"]) })
|
|
1745
|
+
]);
|
|
1746
|
+
var TavernSceneCommandRequestAction = z10.object({
|
|
1747
|
+
action: z10.literal("tavern_scene_command"),
|
|
1748
|
+
accessToken: z10.string().optional(),
|
|
1749
|
+
commands: z10.array(SceneCommandSchema),
|
|
1750
|
+
requestId: z10.string().optional()
|
|
1751
|
+
});
|
|
1752
|
+
var TavernSceneBroadcastAction = z10.object({
|
|
1753
|
+
action: z10.literal("tavern_scene_broadcast"),
|
|
1754
|
+
commands: z10.array(SceneCommandSchema),
|
|
1755
|
+
clientId: z10.string().optional()
|
|
1756
|
+
});
|
|
1680
1757
|
var SessionCreatedAction = shareableDocumentSchema.extend({
|
|
1681
1758
|
action: z10.literal("session.created"),
|
|
1682
1759
|
id: z10.string(),
|
|
@@ -1713,7 +1790,10 @@ var MessageDataToServer = z10.discriminatedUnion("action", [
|
|
|
1713
1790
|
VoiceSessionSendTranscriptAction,
|
|
1714
1791
|
VoiceSessionEndedAction,
|
|
1715
1792
|
CliCompletionRequestAction,
|
|
1716
|
-
CliToolRequestAction
|
|
1793
|
+
CliToolRequestAction,
|
|
1794
|
+
KeepCommandRequestAction,
|
|
1795
|
+
KeepCommandResponseAction,
|
|
1796
|
+
TavernSceneCommandRequestAction
|
|
1717
1797
|
]);
|
|
1718
1798
|
var MessageDataToClient = z10.discriminatedUnion("action", [
|
|
1719
1799
|
DataSubscriptionUpdateAction,
|
|
@@ -1743,7 +1823,10 @@ var MessageDataToClient = z10.discriminatedUnion("action", [
|
|
|
1743
1823
|
CliCompletionChunkAction,
|
|
1744
1824
|
CliCompletionDoneAction,
|
|
1745
1825
|
CliCompletionErrorAction,
|
|
1746
|
-
CliToolResponseAction
|
|
1826
|
+
CliToolResponseAction,
|
|
1827
|
+
KeepCommandAction,
|
|
1828
|
+
KeepCommandResultAction,
|
|
1829
|
+
TavernSceneBroadcastAction
|
|
1747
1830
|
]);
|
|
1748
1831
|
|
|
1749
1832
|
// ../../b4m-core/packages/common/dist/src/schemas/cliCompletions.js
|
|
@@ -2701,6 +2784,7 @@ var SettingKeySchema = z21.enum([
|
|
|
2701
2784
|
"EnableHelpChat",
|
|
2702
2785
|
// B4M PI SETTINGS
|
|
2703
2786
|
"EnableBmPi",
|
|
2787
|
+
"EnableBmPiJira",
|
|
2704
2788
|
// QUANTUM CANVASSER SETTINGS
|
|
2705
2789
|
"EnableQuantumCanvasser",
|
|
2706
2790
|
// CONTEXT TELEMETRY SETTINGS
|
|
@@ -3268,7 +3352,8 @@ var API_SERVICE_GROUPS = {
|
|
|
3268
3352
|
{ key: "EnableParallelToolExecution", order: 17 },
|
|
3269
3353
|
{ key: "EnableHelpChat", order: 18 },
|
|
3270
3354
|
{ key: "EnableBmPi", order: 19 },
|
|
3271
|
-
{ key: "
|
|
3355
|
+
{ key: "EnableBmPiJira", order: 20 },
|
|
3356
|
+
{ key: "EnableQuantumCanvasser", order: 21 }
|
|
3272
3357
|
]
|
|
3273
3358
|
},
|
|
3274
3359
|
NOTEBOOK: {
|
|
@@ -4479,6 +4564,16 @@ var settingsMap = {
|
|
|
4479
4564
|
group: API_SERVICE_GROUPS.EXPERIMENTAL.id,
|
|
4480
4565
|
order: 18
|
|
4481
4566
|
}),
|
|
4567
|
+
EnableBmPiJira: makeBooleanSetting({
|
|
4568
|
+
key: "EnableBmPiJira",
|
|
4569
|
+
name: "Enable B4M Pi \u2014 Jira Integration",
|
|
4570
|
+
defaultValue: false,
|
|
4571
|
+
description: 'Show Jira source toggle and Jira views in the B4M Pi dashboard. Requires "Enable B4M Pi" to be on.',
|
|
4572
|
+
category: "Experimental",
|
|
4573
|
+
group: API_SERVICE_GROUPS.EXPERIMENTAL.id,
|
|
4574
|
+
order: 19,
|
|
4575
|
+
dependsOn: "EnableBmPi"
|
|
4576
|
+
}),
|
|
4482
4577
|
// OptiHashi Settings
|
|
4483
4578
|
EnableQuantumCanvasser: makeBooleanSetting({
|
|
4484
4579
|
key: "EnableQuantumCanvasser",
|
|
@@ -7624,7 +7719,8 @@ function formatIssueDetails(issue, siteUrl) {
|
|
|
7624
7719
|
priority: issue.fields?.priority?.name,
|
|
7625
7720
|
assignee: issue.fields?.assignee ? {
|
|
7626
7721
|
accountId: issue.fields.assignee.accountId,
|
|
7627
|
-
displayName: issue.fields.assignee.displayName
|
|
7722
|
+
displayName: issue.fields.assignee.displayName,
|
|
7723
|
+
accountType: issue.fields.assignee.accountType
|
|
7628
7724
|
} : null,
|
|
7629
7725
|
reporter: issue.fields?.reporter ? {
|
|
7630
7726
|
accountId: issue.fields.reporter.accountId,
|
|
@@ -7648,7 +7744,8 @@ function formatSearchResults2(searchResult, siteUrl) {
|
|
|
7648
7744
|
total: searchResult.total || 0,
|
|
7649
7745
|
startAt: searchResult.startAt || 0,
|
|
7650
7746
|
maxResults: searchResult.maxResults || 0,
|
|
7651
|
-
issues
|
|
7747
|
+
issues,
|
|
7748
|
+
...searchResult.nextPageToken && { nextPageToken: searchResult.nextPageToken }
|
|
7652
7749
|
};
|
|
7653
7750
|
}
|
|
7654
7751
|
function formatProjectList(response, siteUrl) {
|
|
@@ -7703,7 +7800,7 @@ function formatTransitions(transitionsResponse) {
|
|
|
7703
7800
|
name: transition.name,
|
|
7704
7801
|
to: transition.to
|
|
7705
7802
|
})) : [];
|
|
7706
|
-
return transitions;
|
|
7803
|
+
return { transitions };
|
|
7707
7804
|
}
|
|
7708
7805
|
function formatUser(user) {
|
|
7709
7806
|
if (!user || typeof user !== "object" || user.error || user.errors) {
|
|
@@ -8056,1449 +8153,1483 @@ var AgileApi = class {
|
|
|
8056
8153
|
}
|
|
8057
8154
|
};
|
|
8058
8155
|
|
|
8059
|
-
// ../../b4m-core/packages/common/dist/src/jira/
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
return Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
8065
|
-
}
|
|
8066
|
-
function formatWebhook(webhook) {
|
|
8067
|
-
const daysUntilExpiry = daysUntil(webhook.expirationDate);
|
|
8068
|
-
return {
|
|
8069
|
-
id: webhook.id,
|
|
8070
|
-
events: webhook.events,
|
|
8071
|
-
jqlFilter: webhook.jqlFilter,
|
|
8072
|
-
expirationDate: webhook.expirationDate,
|
|
8073
|
-
daysUntilExpiry,
|
|
8074
|
-
isExpiringSoon: daysUntilExpiry < 7
|
|
8075
|
-
};
|
|
8156
|
+
// ../../b4m-core/packages/common/dist/src/jira/api.js
|
|
8157
|
+
var JIRA_MAX_ATTACHMENT_SIZE = 20 * 1024 * 1024;
|
|
8158
|
+
var JIRA_PROJECT_KEY_RE = /^[A-Z][A-Z0-9_]*$/i;
|
|
8159
|
+
function isValidProjectKey(key) {
|
|
8160
|
+
return JIRA_PROJECT_KEY_RE.test(key);
|
|
8076
8161
|
}
|
|
8077
|
-
function
|
|
8078
|
-
return
|
|
8079
|
-
webhooks: response.values.map(formatWebhook),
|
|
8080
|
-
total: response.total,
|
|
8081
|
-
hasMore: !response.isLast
|
|
8082
|
-
};
|
|
8162
|
+
function isValidIssueKey(key) {
|
|
8163
|
+
return /^[A-Z][A-Z0-9]*-\d+$/.test(key);
|
|
8083
8164
|
}
|
|
8084
|
-
function
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8165
|
+
function wikiMarkupToAdf(text) {
|
|
8166
|
+
const lines = text.split("\n");
|
|
8167
|
+
const content = [];
|
|
8168
|
+
let currentTableRows = [];
|
|
8169
|
+
let inTable = false;
|
|
8170
|
+
for (const line of lines) {
|
|
8171
|
+
const trimmedLine = line.trim();
|
|
8172
|
+
if (trimmedLine.startsWith("||") && trimmedLine.endsWith("||")) {
|
|
8173
|
+
inTable = true;
|
|
8174
|
+
const cells = trimmedLine.slice(2, -2).split("||").map((cell) => cell.trim());
|
|
8175
|
+
const headerRow = {
|
|
8176
|
+
type: "tableRow",
|
|
8177
|
+
content: cells.map((cell) => ({
|
|
8178
|
+
type: "tableHeader",
|
|
8179
|
+
attrs: {},
|
|
8180
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: cell || " " }] }]
|
|
8181
|
+
}))
|
|
8182
|
+
};
|
|
8183
|
+
currentTableRows.push(headerRow);
|
|
8184
|
+
continue;
|
|
8185
|
+
}
|
|
8186
|
+
if (trimmedLine.startsWith("|") && trimmedLine.endsWith("|") && !trimmedLine.startsWith("||")) {
|
|
8187
|
+
inTable = true;
|
|
8188
|
+
const cells = trimmedLine.slice(1, -1).split("|").map((cell) => cell.trim());
|
|
8189
|
+
const dataRow = {
|
|
8190
|
+
type: "tableRow",
|
|
8191
|
+
content: cells.map((cell) => ({
|
|
8192
|
+
type: "tableCell",
|
|
8193
|
+
attrs: {},
|
|
8194
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: cell || " " }] }]
|
|
8195
|
+
}))
|
|
8196
|
+
};
|
|
8197
|
+
currentTableRows.push(dataRow);
|
|
8198
|
+
continue;
|
|
8199
|
+
}
|
|
8200
|
+
if (inTable && currentTableRows.length > 0) {
|
|
8201
|
+
const table = {
|
|
8202
|
+
type: "table",
|
|
8203
|
+
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
8204
|
+
content: currentTableRows
|
|
8205
|
+
};
|
|
8206
|
+
content.push(table);
|
|
8207
|
+
currentTableRows = [];
|
|
8208
|
+
inTable = false;
|
|
8209
|
+
}
|
|
8210
|
+
if (trimmedLine) {
|
|
8211
|
+
content.push({
|
|
8212
|
+
type: "paragraph",
|
|
8213
|
+
content: [{ type: "text", text: trimmedLine }]
|
|
8214
|
+
});
|
|
8108
8215
|
}
|
|
8109
8216
|
}
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
}
|
|
8116
|
-
|
|
8117
|
-
|
|
8217
|
+
if (currentTableRows.length > 0) {
|
|
8218
|
+
const table = {
|
|
8219
|
+
type: "table",
|
|
8220
|
+
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
8221
|
+
content: currentTableRows
|
|
8222
|
+
};
|
|
8223
|
+
content.push(table);
|
|
8224
|
+
}
|
|
8225
|
+
if (content.length === 0) {
|
|
8226
|
+
content.push({ type: "paragraph", content: [{ type: "text", text: " " }] });
|
|
8227
|
+
}
|
|
8228
|
+
return { type: "doc", version: 1, content };
|
|
8118
8229
|
}
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
High: ":large_orange_circle:",
|
|
8122
|
-
Medium: ":large_yellow_circle:",
|
|
8123
|
-
Low: ":large_green_circle:",
|
|
8124
|
-
Lowest: ":white_circle:"
|
|
8125
|
-
};
|
|
8126
|
-
function getPriorityEmoji(priority) {
|
|
8127
|
-
if (!priority)
|
|
8128
|
-
return "";
|
|
8129
|
-
return PRIORITY_EMOJI[priority] || ":blue_circle:";
|
|
8230
|
+
function containsWikiTable(text) {
|
|
8231
|
+
return /^\|\|.+\|\|$/m.test(text) || /^\|(?!\|).+\|$/m.test(text);
|
|
8130
8232
|
}
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
8136
|
-
let action;
|
|
8137
|
-
let emoji;
|
|
8138
|
-
switch (webhookEvent) {
|
|
8139
|
-
case "jira:issue_created":
|
|
8140
|
-
action = "created";
|
|
8141
|
-
emoji = ":new:";
|
|
8142
|
-
break;
|
|
8143
|
-
case "jira:issue_updated":
|
|
8144
|
-
action = "updated";
|
|
8145
|
-
emoji = ":pencil2:";
|
|
8146
|
-
break;
|
|
8147
|
-
case "jira:issue_deleted":
|
|
8148
|
-
action = "deleted";
|
|
8149
|
-
emoji = ":wastebasket:";
|
|
8150
|
-
break;
|
|
8151
|
-
default:
|
|
8152
|
-
action = "changed";
|
|
8153
|
-
emoji = ":bell:";
|
|
8233
|
+
var JiraApi = class {
|
|
8234
|
+
config;
|
|
8235
|
+
constructor(config) {
|
|
8236
|
+
this.config = config;
|
|
8154
8237
|
}
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
}
|
|
8166
|
-
if (item.field === "priority") {
|
|
8167
|
-
return `Priority: ${from} \u2192 ${to}`;
|
|
8168
|
-
}
|
|
8169
|
-
return `${escapeSlackMrkdwn(item.field)}: ${from} \u2192 ${to}`;
|
|
8238
|
+
/**
|
|
8239
|
+
* Build URL with query parameters
|
|
8240
|
+
*/
|
|
8241
|
+
buildUrl(path, query = {}) {
|
|
8242
|
+
const base = `${this.config.apiBaseUrl}${path}`;
|
|
8243
|
+
const url = new URL(base);
|
|
8244
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
8245
|
+
if (value === void 0 || value === "")
|
|
8246
|
+
return;
|
|
8247
|
+
url.searchParams.append(key, String(value));
|
|
8170
8248
|
});
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8249
|
+
return url.toString();
|
|
8250
|
+
}
|
|
8251
|
+
/**
|
|
8252
|
+
* Build web URL for Jira UI links
|
|
8253
|
+
*/
|
|
8254
|
+
buildWebUrl(path) {
|
|
8255
|
+
if (!path)
|
|
8256
|
+
return "";
|
|
8257
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
8258
|
+
return `${this.config.webBaseUrl}/${cleanPath}`;
|
|
8259
|
+
}
|
|
8260
|
+
/**
|
|
8261
|
+
* Make authenticated HTTP request
|
|
8262
|
+
*/
|
|
8263
|
+
async request(method, path, options = {}) {
|
|
8264
|
+
const url = this.buildUrl(path, options.query);
|
|
8265
|
+
const headers = {
|
|
8266
|
+
Authorization: this.config.authHeader,
|
|
8267
|
+
Accept: "application/json",
|
|
8268
|
+
"Content-Type": "application/json"
|
|
8269
|
+
};
|
|
8270
|
+
const response = await fetch(url, {
|
|
8271
|
+
method,
|
|
8272
|
+
headers,
|
|
8273
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
8274
|
+
});
|
|
8275
|
+
const rateLimitInfo = parseRateLimitHeaders(response.headers);
|
|
8276
|
+
if (hasRateLimitInfo(rateLimitInfo)) {
|
|
8277
|
+
const logEntry = buildRateLimitLogEntry("jira", path, rateLimitInfo);
|
|
8278
|
+
console.error(JSON.stringify(logEntry));
|
|
8279
|
+
if (isNearLimit(rateLimitInfo)) {
|
|
8280
|
+
console.error(`[Jira] Rate limit warning: ${rateLimitInfo.usagePercent}% used (${rateLimitInfo.remaining}/${rateLimitInfo.limit} remaining)`);
|
|
8176
8281
|
}
|
|
8177
8282
|
}
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
text: {
|
|
8191
|
-
type: "mrkdwn",
|
|
8192
|
-
text: `*<${issueUrl}|${escapeSlackMrkdwn(issue.key)}>* ${escapeSlackMrkdwn(fields.summary)}`
|
|
8193
|
-
}
|
|
8194
|
-
},
|
|
8195
|
-
{
|
|
8196
|
-
type: "section",
|
|
8197
|
-
text: {
|
|
8198
|
-
type: "mrkdwn",
|
|
8199
|
-
text: [
|
|
8200
|
-
`${priorityEmoji} *Priority:* ${escapeSlackMrkdwn(fields.priority?.name || "None")}`,
|
|
8201
|
-
`*Status:* ${escapeSlackMrkdwn(fields.status.name)}`,
|
|
8202
|
-
`*Type:* ${escapeSlackMrkdwn(fields.issuetype.name)}`,
|
|
8203
|
-
`*Assignee:* ${escapeSlackMrkdwn(fields.assignee?.displayName || "Unassigned")}`
|
|
8204
|
-
].join(" | ")
|
|
8205
|
-
}
|
|
8283
|
+
if (response.status === 429 && (options._retryCount ?? 0) < 1) {
|
|
8284
|
+
const retryAfterMs = rateLimitInfo.retryAfterMs ?? 5e3;
|
|
8285
|
+
const jitterMs = Math.floor(Math.random() * 1e3);
|
|
8286
|
+
const delayMs = Math.min(retryAfterMs + jitterMs, 1e4);
|
|
8287
|
+
const logEntry = buildRateLimitLogEntry("jira", path, rateLimitInfo, true);
|
|
8288
|
+
console.error(JSON.stringify(logEntry));
|
|
8289
|
+
console.error(`[Jira] Rate limited on ${path}, retrying after ${delayMs}ms`);
|
|
8290
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
8291
|
+
return this.request(method, path, {
|
|
8292
|
+
...options,
|
|
8293
|
+
_retryCount: (options._retryCount ?? 0) + 1
|
|
8294
|
+
});
|
|
8206
8295
|
}
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8296
|
+
if (!response.ok) {
|
|
8297
|
+
const errorBody = await response.text();
|
|
8298
|
+
throw new Error(`Jira API error (${response.status}): ${errorBody}`);
|
|
8299
|
+
}
|
|
8300
|
+
if (response.status === 204) {
|
|
8301
|
+
return {};
|
|
8302
|
+
}
|
|
8303
|
+
const data = await response.json();
|
|
8304
|
+
return data;
|
|
8305
|
+
}
|
|
8306
|
+
// ============================================================================
|
|
8307
|
+
// High-Level API Methods
|
|
8308
|
+
// ============================================================================
|
|
8309
|
+
/**
|
|
8310
|
+
* Get issue by key or ID
|
|
8311
|
+
*/
|
|
8312
|
+
async getIssue(params) {
|
|
8313
|
+
const { issueKey, expand } = params;
|
|
8314
|
+
const issue = await this.request("GET", `/issue/${issueKey}`, {
|
|
8315
|
+
query: {
|
|
8316
|
+
expand: expand?.join(",")
|
|
8214
8317
|
}
|
|
8215
8318
|
});
|
|
8319
|
+
return formatIssueDetails(issue, this.config.siteUrl);
|
|
8216
8320
|
}
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
}
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
{
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8321
|
+
/**
|
|
8322
|
+
* Create a new issue
|
|
8323
|
+
*/
|
|
8324
|
+
async createIssue(params) {
|
|
8325
|
+
const { projectKey, summary, description, issueTypeName, assignee, labels, parentKey, ...customFields } = params;
|
|
8326
|
+
const fields = {
|
|
8327
|
+
project: { key: projectKey },
|
|
8328
|
+
summary,
|
|
8329
|
+
issuetype: { name: issueTypeName },
|
|
8330
|
+
...customFields
|
|
8331
|
+
};
|
|
8332
|
+
if (description) {
|
|
8333
|
+
if (containsWikiTable(description)) {
|
|
8334
|
+
console.log("[JIRA-ADF] Converting wiki table to ADF", { length: description.length });
|
|
8335
|
+
fields.description = wikiMarkupToAdf(description);
|
|
8336
|
+
} else {
|
|
8337
|
+
fields.description = {
|
|
8338
|
+
type: "doc",
|
|
8339
|
+
version: 1,
|
|
8340
|
+
content: [
|
|
8341
|
+
{
|
|
8342
|
+
type: "paragraph",
|
|
8343
|
+
content: [{ type: "text", text: description }]
|
|
8344
|
+
}
|
|
8345
|
+
]
|
|
8346
|
+
};
|
|
8238
8347
|
}
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
}
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
action = "commented on";
|
|
8254
|
-
emoji = ":speech_balloon:";
|
|
8255
|
-
break;
|
|
8256
|
-
case "comment_updated":
|
|
8257
|
-
action = "updated comment on";
|
|
8258
|
-
emoji = ":pencil:";
|
|
8259
|
-
break;
|
|
8260
|
-
case "comment_deleted":
|
|
8261
|
-
action = "deleted comment on";
|
|
8262
|
-
emoji = ":x:";
|
|
8263
|
-
break;
|
|
8264
|
-
default:
|
|
8265
|
-
action = "commented on";
|
|
8266
|
-
emoji = ":speech_balloon:";
|
|
8348
|
+
}
|
|
8349
|
+
if (assignee) {
|
|
8350
|
+
fields.assignee = { id: assignee };
|
|
8351
|
+
}
|
|
8352
|
+
if (labels && labels.length > 0) {
|
|
8353
|
+
fields.labels = labels;
|
|
8354
|
+
}
|
|
8355
|
+
if (parentKey) {
|
|
8356
|
+
fields.parent = { key: parentKey };
|
|
8357
|
+
}
|
|
8358
|
+
const result = await this.request("POST", "/issue", {
|
|
8359
|
+
body: { fields }
|
|
8360
|
+
});
|
|
8361
|
+
return formatIssueResponse(result, this.config.siteUrl);
|
|
8267
8362
|
}
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
}
|
|
8277
|
-
{
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8363
|
+
/**
|
|
8364
|
+
* Update an existing issue
|
|
8365
|
+
*/
|
|
8366
|
+
async updateIssue(params) {
|
|
8367
|
+
const { issueKey, summary, description, labels, ...customFields } = params;
|
|
8368
|
+
const fields = { ...customFields };
|
|
8369
|
+
if (summary) {
|
|
8370
|
+
fields.summary = summary;
|
|
8371
|
+
}
|
|
8372
|
+
if (description) {
|
|
8373
|
+
if (containsWikiTable(description)) {
|
|
8374
|
+
console.log("[JIRA-ADF] Converting wiki table to ADF (update)", { length: description.length });
|
|
8375
|
+
fields.description = wikiMarkupToAdf(description);
|
|
8376
|
+
} else {
|
|
8377
|
+
fields.description = {
|
|
8378
|
+
type: "doc",
|
|
8379
|
+
version: 1,
|
|
8380
|
+
content: [
|
|
8381
|
+
{
|
|
8382
|
+
type: "paragraph",
|
|
8383
|
+
content: [{ type: "text", text: description }]
|
|
8384
|
+
}
|
|
8385
|
+
]
|
|
8386
|
+
};
|
|
8282
8387
|
}
|
|
8283
8388
|
}
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
if (
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
text: {
|
|
8291
|
-
type: "mrkdwn",
|
|
8292
|
-
text: `> ${escapeSlackMrkdwn(bodyText).replace(/\n/g, "\n> ")}`
|
|
8293
|
-
}
|
|
8389
|
+
if (labels && labels.length > 0) {
|
|
8390
|
+
fields.labels = labels;
|
|
8391
|
+
}
|
|
8392
|
+
if (Object.keys(fields).length > 0) {
|
|
8393
|
+
await this.request("PUT", `/issue/${issueKey}`, {
|
|
8394
|
+
body: { fields }
|
|
8294
8395
|
});
|
|
8295
8396
|
}
|
|
8296
8397
|
}
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8398
|
+
/**
|
|
8399
|
+
* Search for issues using JQL
|
|
8400
|
+
*/
|
|
8401
|
+
async searchIssues(params) {
|
|
8402
|
+
const { jql, startAt = 0, maxResults = 50, fields, expand, nextPageToken } = params;
|
|
8403
|
+
const result = await this.request("GET", "/search/jql", {
|
|
8404
|
+
query: {
|
|
8405
|
+
jql,
|
|
8406
|
+
...nextPageToken ? { nextPageToken } : { startAt },
|
|
8407
|
+
maxResults,
|
|
8408
|
+
fields: fields?.join(",") || "*all",
|
|
8409
|
+
expand: expand?.join(",")
|
|
8303
8410
|
}
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
style: "primary"
|
|
8317
|
-
}
|
|
8318
|
-
]
|
|
8319
|
-
});
|
|
8320
|
-
return {
|
|
8321
|
-
blocks,
|
|
8322
|
-
text: `${emoji} ${escapeSlackMrkdwn(comment.author.displayName)} ${action} ${escapeSlackMrkdwn(issue.key)}: ${escapeSlackMrkdwn(issue.fields.summary)}`
|
|
8323
|
-
};
|
|
8324
|
-
}
|
|
8325
|
-
function formatSprintEventForSlack(event, siteUrl) {
|
|
8326
|
-
const { sprint, webhookEvent, user } = event;
|
|
8327
|
-
let action;
|
|
8328
|
-
let emoji;
|
|
8329
|
-
switch (webhookEvent) {
|
|
8330
|
-
case "sprint_created":
|
|
8331
|
-
action = "created";
|
|
8332
|
-
emoji = ":calendar:";
|
|
8333
|
-
break;
|
|
8334
|
-
case "sprint_started":
|
|
8335
|
-
action = "started";
|
|
8336
|
-
emoji = ":rocket:";
|
|
8337
|
-
break;
|
|
8338
|
-
case "sprint_closed":
|
|
8339
|
-
action = "completed";
|
|
8340
|
-
emoji = ":checkered_flag:";
|
|
8341
|
-
break;
|
|
8342
|
-
case "sprint_updated":
|
|
8343
|
-
action = "updated";
|
|
8344
|
-
emoji = ":pencil2:";
|
|
8345
|
-
break;
|
|
8346
|
-
case "sprint_deleted":
|
|
8347
|
-
action = "deleted";
|
|
8348
|
-
emoji = ":wastebasket:";
|
|
8349
|
-
break;
|
|
8350
|
-
default:
|
|
8351
|
-
action = "changed";
|
|
8352
|
-
emoji = ":bell:";
|
|
8353
|
-
}
|
|
8354
|
-
const sprintUrl = `${siteUrl}/secure/RapidBoard.jspa?rapidView=${sprint.originBoardId}`;
|
|
8355
|
-
const blocks = [
|
|
8356
|
-
{
|
|
8357
|
-
type: "header",
|
|
8358
|
-
text: {
|
|
8359
|
-
type: "plain_text",
|
|
8360
|
-
text: `${emoji} Sprint ${action}: ${escapeSlackMrkdwn(sprint.name)}`,
|
|
8361
|
-
emoji: true
|
|
8362
|
-
}
|
|
8363
|
-
},
|
|
8364
|
-
{
|
|
8365
|
-
type: "section",
|
|
8366
|
-
text: {
|
|
8367
|
-
type: "mrkdwn",
|
|
8368
|
-
text: [
|
|
8369
|
-
`*State:* ${escapeSlackMrkdwn(sprint.state)}`,
|
|
8370
|
-
sprint.startDate ? `*Start:* ${new Date(sprint.startDate).toLocaleDateString()}` : "",
|
|
8371
|
-
sprint.endDate ? `*End:* ${new Date(sprint.endDate).toLocaleDateString()}` : ""
|
|
8372
|
-
].filter(Boolean).join(" | ")
|
|
8373
|
-
}
|
|
8374
|
-
}
|
|
8375
|
-
];
|
|
8376
|
-
if (sprint.goal) {
|
|
8377
|
-
blocks.push({
|
|
8378
|
-
type: "section",
|
|
8379
|
-
text: {
|
|
8380
|
-
type: "mrkdwn",
|
|
8381
|
-
text: `*Goal:* ${escapeSlackMrkdwn(sprint.goal)}`
|
|
8382
|
-
}
|
|
8383
|
-
});
|
|
8384
|
-
}
|
|
8385
|
-
blocks.push({
|
|
8386
|
-
type: "context",
|
|
8387
|
-
elements: [
|
|
8388
|
-
{
|
|
8389
|
-
type: "mrkdwn",
|
|
8390
|
-
text: user?.displayName ? escapeSlackMrkdwn(user.displayName) : "System"
|
|
8391
|
-
}
|
|
8392
|
-
]
|
|
8393
|
-
});
|
|
8394
|
-
blocks.push({
|
|
8395
|
-
type: "actions",
|
|
8396
|
-
elements: [
|
|
8397
|
-
{
|
|
8398
|
-
type: "button",
|
|
8399
|
-
text: {
|
|
8400
|
-
type: "plain_text",
|
|
8401
|
-
text: "View Board",
|
|
8402
|
-
emoji: true
|
|
8403
|
-
},
|
|
8404
|
-
url: sprintUrl,
|
|
8405
|
-
style: "primary"
|
|
8406
|
-
}
|
|
8407
|
-
]
|
|
8408
|
-
});
|
|
8409
|
-
return {
|
|
8410
|
-
blocks,
|
|
8411
|
-
text: `${emoji} Sprint ${action}: ${escapeSlackMrkdwn(sprint.name)}`
|
|
8412
|
-
};
|
|
8413
|
-
}
|
|
8414
|
-
function formatGenericEventForSlack(eventType, payload, siteUrl) {
|
|
8415
|
-
const issue = payload.issue;
|
|
8416
|
-
const user = payload.user;
|
|
8417
|
-
const issueLink = payload.issueLink;
|
|
8418
|
-
const changelog = payload.changelog;
|
|
8419
|
-
const project = payload.project ?? issue?.fields?.project;
|
|
8420
|
-
const version = payload.version;
|
|
8421
|
-
const worklog = payload.worklog;
|
|
8422
|
-
const board = payload.board;
|
|
8423
|
-
const readableEvent = eventType.replace(/^jira:/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
8424
|
-
const blocks = [
|
|
8425
|
-
{
|
|
8426
|
-
type: "header",
|
|
8427
|
-
text: {
|
|
8428
|
-
type: "plain_text",
|
|
8429
|
-
text: `:bell: ${readableEvent}`,
|
|
8430
|
-
emoji: true
|
|
8431
|
-
}
|
|
8432
|
-
}
|
|
8433
|
-
];
|
|
8434
|
-
const details = [];
|
|
8435
|
-
if (issue?.key) {
|
|
8436
|
-
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
8437
|
-
blocks.push({
|
|
8438
|
-
type: "section",
|
|
8439
|
-
text: {
|
|
8440
|
-
type: "mrkdwn",
|
|
8441
|
-
text: `*<${issueUrl}|${escapeSlackMrkdwn(issue.key)}>* ${escapeSlackMrkdwn(issue.fields?.summary || "")}`
|
|
8411
|
+
});
|
|
8412
|
+
return formatSearchResults2(result, this.config.siteUrl);
|
|
8413
|
+
}
|
|
8414
|
+
/**
|
|
8415
|
+
* List all accessible projects
|
|
8416
|
+
*/
|
|
8417
|
+
async listProjects(params) {
|
|
8418
|
+
const projects = await this.request("GET", "/project/search", {
|
|
8419
|
+
query: {
|
|
8420
|
+
maxResults: params?.maxResults,
|
|
8421
|
+
query: params?.query,
|
|
8422
|
+
expand: params?.expand
|
|
8442
8423
|
}
|
|
8443
8424
|
});
|
|
8444
|
-
|
|
8445
|
-
if (issue.fields?.status?.name)
|
|
8446
|
-
meta.push(`*Status:* ${escapeSlackMrkdwn(issue.fields.status.name)}`);
|
|
8447
|
-
if (issue.fields?.issuetype?.name)
|
|
8448
|
-
meta.push(`*Type:* ${escapeSlackMrkdwn(issue.fields.issuetype.name)}`);
|
|
8449
|
-
if (issue.fields?.priority?.name) {
|
|
8450
|
-
const emoji = getPriorityEmoji(issue.fields.priority.name);
|
|
8451
|
-
meta.push(`${emoji} *Priority:* ${escapeSlackMrkdwn(issue.fields.priority.name)}`);
|
|
8452
|
-
}
|
|
8453
|
-
if (issue.fields?.assignee?.displayName)
|
|
8454
|
-
meta.push(`*Assignee:* ${escapeSlackMrkdwn(issue.fields.assignee.displayName)}`);
|
|
8455
|
-
if (meta.length > 0)
|
|
8456
|
-
details.push(meta.join(" | "));
|
|
8457
|
-
}
|
|
8458
|
-
if (issueLink?.issueLinkType) {
|
|
8459
|
-
const linkType = issueLink.issueLinkType;
|
|
8460
|
-
const linkDesc = escapeSlackMrkdwn(linkType.outwardName || linkType.name || "linked");
|
|
8461
|
-
details.push(`*Link type:* ${linkDesc}`);
|
|
8462
|
-
if (issueLink.sourceIssueId)
|
|
8463
|
-
details.push(`*Source issue ID:* ${issueLink.sourceIssueId}`);
|
|
8464
|
-
if (issueLink.destinationIssueId)
|
|
8465
|
-
details.push(`*Destination issue ID:* ${issueLink.destinationIssueId}`);
|
|
8466
|
-
}
|
|
8467
|
-
if (version?.name) {
|
|
8468
|
-
const versionParts = [`*Version:* ${escapeSlackMrkdwn(version.name)}`];
|
|
8469
|
-
if (version.description)
|
|
8470
|
-
versionParts.push(escapeSlackMrkdwn(version.description));
|
|
8471
|
-
if (version.released !== void 0)
|
|
8472
|
-
versionParts.push(version.released ? ":white_check_mark: Released" : "Unreleased");
|
|
8473
|
-
details.push(versionParts.join(" | "));
|
|
8474
|
-
}
|
|
8475
|
-
if (worklog) {
|
|
8476
|
-
const wlParts = [];
|
|
8477
|
-
if (worklog.author?.displayName)
|
|
8478
|
-
wlParts.push(`*By:* ${escapeSlackMrkdwn(worklog.author.displayName)}`);
|
|
8479
|
-
if (worklog.timeSpent)
|
|
8480
|
-
wlParts.push(`*Time:* ${escapeSlackMrkdwn(worklog.timeSpent)}`);
|
|
8481
|
-
if (wlParts.length > 0)
|
|
8482
|
-
details.push(wlParts.join(" | "));
|
|
8483
|
-
}
|
|
8484
|
-
if (board?.name) {
|
|
8485
|
-
details.push(`*Board:* ${escapeSlackMrkdwn(board.name)}`);
|
|
8486
|
-
}
|
|
8487
|
-
if (changelog?.items && changelog.items.length > 0) {
|
|
8488
|
-
const changes = changelog.items.slice(0, 3).map((item) => `\u2022 ${escapeSlackMrkdwn(item.field || "")}: ${escapeSlackMrkdwn(item.fromString || "None")} \u2192 ${escapeSlackMrkdwn(item.toString || "None")}`);
|
|
8489
|
-
if (changelog.items.length > 3) {
|
|
8490
|
-
changes.push(`_...and ${changelog.items.length - 3} more_`);
|
|
8491
|
-
}
|
|
8492
|
-
details.push(`*Changes:*
|
|
8493
|
-
${changes.join("\n")}`);
|
|
8425
|
+
return formatProjectList(projects, this.config.siteUrl);
|
|
8494
8426
|
}
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8427
|
+
/**
|
|
8428
|
+
* Get project details
|
|
8429
|
+
*/
|
|
8430
|
+
async getProject(params) {
|
|
8431
|
+
const { projectKey, expand } = params;
|
|
8432
|
+
const project = await this.request("GET", `/project/${projectKey}`, {
|
|
8433
|
+
query: {
|
|
8434
|
+
expand: expand?.join(",")
|
|
8501
8435
|
}
|
|
8502
8436
|
});
|
|
8437
|
+
return formatProjectResponse(project, this.config.siteUrl);
|
|
8503
8438
|
}
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
type: "context",
|
|
8512
|
-
elements: [{ type: "mrkdwn", text: contextParts.join(" \u2022 ") }]
|
|
8513
|
-
});
|
|
8439
|
+
/**
|
|
8440
|
+
* List issue types for a project
|
|
8441
|
+
*/
|
|
8442
|
+
async listIssueTypes(params) {
|
|
8443
|
+
const { projectKey } = params;
|
|
8444
|
+
const project = await this.getProject({ projectKey, expand: ["issueTypes"] });
|
|
8445
|
+
return project.issueTypes || [];
|
|
8514
8446
|
}
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8447
|
+
/**
|
|
8448
|
+
* Add comment to an issue
|
|
8449
|
+
*/
|
|
8450
|
+
async addComment(params) {
|
|
8451
|
+
const { issueKey, body } = params;
|
|
8452
|
+
const comment = await this.request("POST", `/issue/${issueKey}/comment`, {
|
|
8453
|
+
body: {
|
|
8454
|
+
body: {
|
|
8455
|
+
type: "doc",
|
|
8456
|
+
version: 1,
|
|
8457
|
+
content: [
|
|
8458
|
+
{
|
|
8459
|
+
type: "paragraph",
|
|
8460
|
+
content: [{ type: "text", text: body }]
|
|
8461
|
+
}
|
|
8462
|
+
]
|
|
8525
8463
|
}
|
|
8526
|
-
|
|
8464
|
+
}
|
|
8527
8465
|
});
|
|
8528
|
-
|
|
8529
|
-
const summary = issue?.key ? `${escapeSlackMrkdwn(issue.key)}: ${escapeSlackMrkdwn(issue.fields?.summary || readableEvent)}` : readableEvent;
|
|
8530
|
-
return {
|
|
8531
|
-
blocks,
|
|
8532
|
-
text: `:bell: ${summary}`
|
|
8533
|
-
};
|
|
8534
|
-
}
|
|
8535
|
-
|
|
8536
|
-
// ../../b4m-core/packages/common/dist/src/jira/webhook/api.js
|
|
8537
|
-
var WebhookApi = class {
|
|
8538
|
-
config;
|
|
8539
|
-
constructor(config) {
|
|
8540
|
-
this.config = config;
|
|
8466
|
+
return formatComment(comment);
|
|
8541
8467
|
}
|
|
8542
8468
|
/**
|
|
8543
|
-
*
|
|
8544
|
-
*
|
|
8545
|
-
* Note: Webhooks use a different base URL than other Jira APIs.
|
|
8469
|
+
* Get all statuses available in a project, grouped by issue type.
|
|
8470
|
+
* Uses GET /project/{projectKey}/statuses which returns every workflow status.
|
|
8546
8471
|
*/
|
|
8547
|
-
|
|
8548
|
-
const
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8472
|
+
async getProjectStatuses(params) {
|
|
8473
|
+
const { projectKey } = params;
|
|
8474
|
+
const result = await this.request("GET", `/project/${projectKey}/statuses`);
|
|
8475
|
+
const names = /* @__PURE__ */ new Set();
|
|
8476
|
+
for (const issueType of result) {
|
|
8477
|
+
for (const status of issueType.statuses) {
|
|
8478
|
+
names.add(status.name);
|
|
8552
8479
|
}
|
|
8553
8480
|
}
|
|
8554
|
-
return
|
|
8481
|
+
return Array.from(names).sort();
|
|
8555
8482
|
}
|
|
8556
8483
|
/**
|
|
8557
|
-
*
|
|
8484
|
+
* Get available transitions for an issue
|
|
8558
8485
|
*/
|
|
8559
|
-
async
|
|
8560
|
-
const
|
|
8561
|
-
const
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8486
|
+
async getTransitions(params) {
|
|
8487
|
+
const { issueKey } = params;
|
|
8488
|
+
const result = await this.request("GET", `/issue/${issueKey}/transitions`);
|
|
8489
|
+
return formatTransitions(result);
|
|
8490
|
+
}
|
|
8491
|
+
/**
|
|
8492
|
+
* Transition issue to a new status
|
|
8493
|
+
*/
|
|
8494
|
+
async transitionIssue(params) {
|
|
8495
|
+
const { issueKey, transitionId } = params;
|
|
8496
|
+
await this.request("POST", `/issue/${issueKey}/transitions`, {
|
|
8497
|
+
body: {
|
|
8498
|
+
transition: { id: transitionId }
|
|
8499
|
+
}
|
|
8570
8500
|
});
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
throw new Error(`Jira Webhook API error (${response.status}): ${errorBody}`);
|
|
8574
|
-
}
|
|
8575
|
-
if (response.status === 204) {
|
|
8576
|
-
return {};
|
|
8577
|
-
}
|
|
8578
|
-
const data = await response.json();
|
|
8579
|
-
return data;
|
|
8501
|
+
const result = { issueKey, transitionId };
|
|
8502
|
+
return formatTransitionResult(result, this.config.siteUrl);
|
|
8580
8503
|
}
|
|
8581
|
-
// ============================================================================
|
|
8582
|
-
// Webhook CRUD Operations
|
|
8583
|
-
// ============================================================================
|
|
8584
8504
|
/**
|
|
8585
|
-
*
|
|
8586
|
-
*
|
|
8587
|
-
* @param startAt - Pagination start index (default: 0)
|
|
8588
|
-
* @param maxResults - Max results per page (default: 50, max: 100)
|
|
8505
|
+
* Assign issue to a user
|
|
8589
8506
|
*/
|
|
8590
|
-
async
|
|
8591
|
-
const {
|
|
8592
|
-
|
|
8593
|
-
|
|
8507
|
+
async assignIssue(params) {
|
|
8508
|
+
const { issueKey, accountId } = params;
|
|
8509
|
+
await this.request("PUT", `/issue/${issueKey}/assignee`, {
|
|
8510
|
+
body: { accountId }
|
|
8594
8511
|
});
|
|
8595
|
-
return formatWebhookList(response);
|
|
8596
8512
|
}
|
|
8597
8513
|
/**
|
|
8598
|
-
*
|
|
8599
|
-
*
|
|
8600
|
-
* @param url - The URL where Jira will POST webhook events
|
|
8601
|
-
* @param events - Events to subscribe to
|
|
8602
|
-
* @param jqlFilter - Optional JQL filter (only matching issues trigger events)
|
|
8603
|
-
* @returns The created webhook ID
|
|
8514
|
+
* Delete an issue
|
|
8604
8515
|
*/
|
|
8605
|
-
async
|
|
8606
|
-
const {
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8516
|
+
async deleteIssue(params) {
|
|
8517
|
+
const { issueKey } = params;
|
|
8518
|
+
if (!issueKey) {
|
|
8519
|
+
throw new Error("issueKey is required to delete an issue.");
|
|
8520
|
+
}
|
|
8521
|
+
await this.request("DELETE", `/issue/${issueKey}`);
|
|
8522
|
+
}
|
|
8523
|
+
/**
|
|
8524
|
+
* Get current user information
|
|
8525
|
+
*/
|
|
8526
|
+
async getCurrentUser() {
|
|
8527
|
+
const user = await this.request("GET", "/myself");
|
|
8528
|
+
return formatUser(user);
|
|
8529
|
+
}
|
|
8530
|
+
/**
|
|
8531
|
+
* Search for users by query string (name, email, username)
|
|
8532
|
+
*/
|
|
8533
|
+
async searchUsers(params) {
|
|
8534
|
+
const { query, maxResults = 50 } = params;
|
|
8535
|
+
const users = await this.request("GET", "/user/search", {
|
|
8536
|
+
query: {
|
|
8537
|
+
query,
|
|
8538
|
+
maxResults
|
|
8539
|
+
}
|
|
8619
8540
|
});
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8541
|
+
return formatUserList(users);
|
|
8542
|
+
}
|
|
8543
|
+
/**
|
|
8544
|
+
* Find users assignable to issues in a specific project.
|
|
8545
|
+
* Uses GET /user/assignable/search?project=KEY which respects
|
|
8546
|
+
* project permissions and roles.
|
|
8547
|
+
*/
|
|
8548
|
+
async findAssignableUsers(params) {
|
|
8549
|
+
const pageSize = 50;
|
|
8550
|
+
const maxTotal = params.maxResults ?? 500;
|
|
8551
|
+
const allUsers = [];
|
|
8552
|
+
let startAt = 0;
|
|
8553
|
+
while (allUsers.length < maxTotal) {
|
|
8554
|
+
const page = await this.request("GET", "/user/assignable/search", {
|
|
8555
|
+
query: { project: params.projectKey, startAt, maxResults: pageSize }
|
|
8556
|
+
});
|
|
8557
|
+
if (!page || page.length === 0)
|
|
8558
|
+
break;
|
|
8559
|
+
for (const u of page) {
|
|
8560
|
+
if (u.active && u.accountType === "atlassian") {
|
|
8561
|
+
allUsers.push(u);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
if (page.length < pageSize)
|
|
8565
|
+
break;
|
|
8566
|
+
startAt += pageSize;
|
|
8623
8567
|
}
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8568
|
+
return formatUserList(allUsers);
|
|
8569
|
+
}
|
|
8570
|
+
/**
|
|
8571
|
+
* Bulk create multiple issues in a single API call.
|
|
8572
|
+
* Supports up to 50 issues per request (Jira API limit).
|
|
8573
|
+
* Ideal for creating multiple subtasks under a parent issue.
|
|
8574
|
+
*/
|
|
8575
|
+
async bulkCreateIssues(params) {
|
|
8576
|
+
const { issues } = params;
|
|
8577
|
+
if (issues.length === 0) {
|
|
8578
|
+
return { issues: [], errors: [] };
|
|
8627
8579
|
}
|
|
8628
|
-
if (
|
|
8629
|
-
throw new Error(
|
|
8580
|
+
if (issues.length > 50) {
|
|
8581
|
+
throw new Error("Bulk create is limited to 50 issues per request. Please split your request.");
|
|
8630
8582
|
}
|
|
8583
|
+
const issueUpdates = issues.map((issue) => {
|
|
8584
|
+
const { projectKey, summary, description, issueTypeName, assignee, labels, parentKey } = issue;
|
|
8585
|
+
const fields = {
|
|
8586
|
+
project: { key: projectKey },
|
|
8587
|
+
summary,
|
|
8588
|
+
issuetype: { name: issueTypeName }
|
|
8589
|
+
};
|
|
8590
|
+
if (description) {
|
|
8591
|
+
if (containsWikiTable(description)) {
|
|
8592
|
+
console.log("[JIRA-ADF] Converting wiki table to ADF (bulk)", { length: description.length });
|
|
8593
|
+
fields.description = wikiMarkupToAdf(description);
|
|
8594
|
+
} else {
|
|
8595
|
+
fields.description = {
|
|
8596
|
+
type: "doc",
|
|
8597
|
+
version: 1,
|
|
8598
|
+
content: [
|
|
8599
|
+
{
|
|
8600
|
+
type: "paragraph",
|
|
8601
|
+
content: [{ type: "text", text: description }]
|
|
8602
|
+
}
|
|
8603
|
+
]
|
|
8604
|
+
};
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
if (assignee) {
|
|
8608
|
+
fields.assignee = { id: assignee };
|
|
8609
|
+
}
|
|
8610
|
+
if (labels && labels.length > 0) {
|
|
8611
|
+
fields.labels = labels;
|
|
8612
|
+
}
|
|
8613
|
+
if (parentKey) {
|
|
8614
|
+
fields.parent = { key: parentKey };
|
|
8615
|
+
}
|
|
8616
|
+
return { update: {}, fields };
|
|
8617
|
+
});
|
|
8618
|
+
const result = await this.request("POST", "/issue/bulk", {
|
|
8619
|
+
body: { issueUpdates }
|
|
8620
|
+
});
|
|
8621
|
+
const formattedIssues = (result.issues || []).map((issue) => formatIssueResponse(issue, this.config.siteUrl));
|
|
8622
|
+
const formattedErrors = (result.errors || []).map((error) => ({
|
|
8623
|
+
status: error.status,
|
|
8624
|
+
message: Object.values(error.elementErrors?.errors || {}).join(", ") || "Unknown error",
|
|
8625
|
+
failedElementNumber: error.failedElementNumber
|
|
8626
|
+
}));
|
|
8631
8627
|
return {
|
|
8632
|
-
|
|
8628
|
+
issues: formattedIssues,
|
|
8629
|
+
errors: formattedErrors
|
|
8633
8630
|
};
|
|
8634
8631
|
}
|
|
8635
8632
|
/**
|
|
8636
|
-
*
|
|
8637
|
-
*
|
|
8638
|
-
*
|
|
8633
|
+
* Bulk transition multiple issues to new statuses in a single API call.
|
|
8634
|
+
* Uses the Jira Cloud Bulk Transition API.
|
|
8635
|
+
* Supports up to 1000 issues per request.
|
|
8639
8636
|
*
|
|
8640
|
-
*
|
|
8641
|
-
* @returns New expiration date
|
|
8637
|
+
* Note: This is an async operation - returns a task ID for tracking progress.
|
|
8642
8638
|
*/
|
|
8643
|
-
async
|
|
8644
|
-
const {
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8639
|
+
async bulkTransitionIssues(params) {
|
|
8640
|
+
const { issues } = params;
|
|
8641
|
+
console.error(`[Jira] bulkTransitionIssues called with ${issues.length} issue(s)`);
|
|
8642
|
+
if (issues.length === 0) {
|
|
8643
|
+
console.error("[Jira] bulkTransitionIssues: No issues provided, returning early");
|
|
8644
|
+
return { taskId: "", message: "No issues to transition", issueCount: 0 };
|
|
8645
|
+
}
|
|
8646
|
+
if (issues.length > 1e3) {
|
|
8647
|
+
console.error(`[Jira] bulkTransitionIssues: Exceeded 1000 issue limit (${issues.length})`);
|
|
8648
|
+
throw new Error("Bulk transition is limited to 1000 issues per request. Please split your request.");
|
|
8649
|
+
}
|
|
8650
|
+
const transitionGroups = {};
|
|
8651
|
+
for (const issue of issues) {
|
|
8652
|
+
if (!transitionGroups[issue.transitionId]) {
|
|
8653
|
+
transitionGroups[issue.transitionId] = [];
|
|
8654
|
+
}
|
|
8655
|
+
transitionGroups[issue.transitionId].push(issue.issueIdOrKey);
|
|
8656
|
+
}
|
|
8657
|
+
const bulkTransitionInputs = Object.entries(transitionGroups).map(([transitionId, selectedIssueIdsOrKeys]) => ({
|
|
8658
|
+
selectedIssueIdsOrKeys,
|
|
8659
|
+
transitionId
|
|
8660
|
+
}));
|
|
8661
|
+
console.error(`[Jira] bulkTransitionIssues: Grouped into ${bulkTransitionInputs.length} transition group(s)`);
|
|
8662
|
+
const result = await this.request("POST", "/bulk/issues/transition", {
|
|
8663
|
+
body: { bulkTransitionInputs }
|
|
8650
8664
|
});
|
|
8665
|
+
console.error(`[Jira] bulkTransitionIssues: Task created with ID ${result.taskId}`);
|
|
8651
8666
|
return {
|
|
8652
|
-
|
|
8667
|
+
taskId: result.taskId,
|
|
8668
|
+
message: `Bulk transition started for ${issues.length} issue(s).`,
|
|
8669
|
+
issueCount: issues.length
|
|
8653
8670
|
};
|
|
8654
8671
|
}
|
|
8655
8672
|
/**
|
|
8656
|
-
*
|
|
8673
|
+
* Bulk update labels on multiple issues in a single API call.
|
|
8674
|
+
* Uses the Jira Cloud Bulk Edit API.
|
|
8675
|
+
* Supports up to 1000 issues per request.
|
|
8657
8676
|
*
|
|
8658
|
-
*
|
|
8659
|
-
*/
|
|
8660
|
-
async deleteWebhooks(params) {
|
|
8661
|
-
const { webhookIds } = params;
|
|
8662
|
-
const body = {
|
|
8663
|
-
webhookIds
|
|
8664
|
-
};
|
|
8665
|
-
await this.request("DELETE", "/webhook", {
|
|
8666
|
-
body
|
|
8667
|
-
});
|
|
8668
|
-
}
|
|
8669
|
-
/**
|
|
8670
|
-
* Get a specific webhook by ID.
|
|
8677
|
+
* Note: This is an async operation - returns a task ID for tracking progress.
|
|
8671
8678
|
*
|
|
8672
|
-
*
|
|
8673
|
-
*
|
|
8679
|
+
* Why only labels? Jira's Bulk Edit API has significant limitations:
|
|
8680
|
+
* - Summary, description, assignee fields are NOT supported for bulk editing
|
|
8681
|
+
* - Priority and issueType require IDs and have inconsistent behavior
|
|
8682
|
+
* - Labels is the most reliable and commonly used bulk operation
|
|
8683
|
+
* See: https://community.atlassian.com/forums/Jira-questions/Bulk-Change-Description-Field/qaq-p/897762
|
|
8674
8684
|
*/
|
|
8675
|
-
async
|
|
8676
|
-
const {
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
return webhook || null;
|
|
8682
|
-
}
|
|
8683
|
-
};
|
|
8684
|
-
|
|
8685
|
-
// ../../b4m-core/packages/common/dist/src/jira/api.js
|
|
8686
|
-
var JIRA_MAX_ATTACHMENT_SIZE = 20 * 1024 * 1024;
|
|
8687
|
-
function isValidIssueKey(key) {
|
|
8688
|
-
return /^[A-Z][A-Z0-9]*-\d+$/.test(key);
|
|
8689
|
-
}
|
|
8690
|
-
function wikiMarkupToAdf(text) {
|
|
8691
|
-
const lines = text.split("\n");
|
|
8692
|
-
const content = [];
|
|
8693
|
-
let currentTableRows = [];
|
|
8694
|
-
let inTable = false;
|
|
8695
|
-
for (const line of lines) {
|
|
8696
|
-
const trimmedLine = line.trim();
|
|
8697
|
-
if (trimmedLine.startsWith("||") && trimmedLine.endsWith("||")) {
|
|
8698
|
-
inTable = true;
|
|
8699
|
-
const cells = trimmedLine.slice(2, -2).split("||").map((cell) => cell.trim());
|
|
8700
|
-
const headerRow = {
|
|
8701
|
-
type: "tableRow",
|
|
8702
|
-
content: cells.map((cell) => ({
|
|
8703
|
-
type: "tableHeader",
|
|
8704
|
-
attrs: {},
|
|
8705
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: cell || " " }] }]
|
|
8706
|
-
}))
|
|
8707
|
-
};
|
|
8708
|
-
currentTableRows.push(headerRow);
|
|
8709
|
-
continue;
|
|
8710
|
-
}
|
|
8711
|
-
if (trimmedLine.startsWith("|") && trimmedLine.endsWith("|") && !trimmedLine.startsWith("||")) {
|
|
8712
|
-
inTable = true;
|
|
8713
|
-
const cells = trimmedLine.slice(1, -1).split("|").map((cell) => cell.trim());
|
|
8714
|
-
const dataRow = {
|
|
8715
|
-
type: "tableRow",
|
|
8716
|
-
content: cells.map((cell) => ({
|
|
8717
|
-
type: "tableCell",
|
|
8718
|
-
attrs: {},
|
|
8719
|
-
content: [{ type: "paragraph", content: [{ type: "text", text: cell || " " }] }]
|
|
8720
|
-
}))
|
|
8721
|
-
};
|
|
8722
|
-
currentTableRows.push(dataRow);
|
|
8723
|
-
continue;
|
|
8724
|
-
}
|
|
8725
|
-
if (inTable && currentTableRows.length > 0) {
|
|
8726
|
-
const table = {
|
|
8727
|
-
type: "table",
|
|
8728
|
-
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
8729
|
-
content: currentTableRows
|
|
8730
|
-
};
|
|
8731
|
-
content.push(table);
|
|
8732
|
-
currentTableRows = [];
|
|
8733
|
-
inTable = false;
|
|
8685
|
+
async bulkUpdateIssues(params) {
|
|
8686
|
+
const { issueIdsOrKeys, labels } = params;
|
|
8687
|
+
console.error(`[Jira] bulkUpdateIssues called with ${issueIdsOrKeys.length} issue(s), action: ${labels.action}`);
|
|
8688
|
+
if (issueIdsOrKeys.length === 0) {
|
|
8689
|
+
console.error("[Jira] bulkUpdateIssues: No issues provided, returning early");
|
|
8690
|
+
return { taskId: "", message: "No issues to update", issueCount: 0 };
|
|
8734
8691
|
}
|
|
8735
|
-
if (
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
content: [{ type: "text", text: trimmedLine }]
|
|
8739
|
-
});
|
|
8692
|
+
if (issueIdsOrKeys.length > 1e3) {
|
|
8693
|
+
console.error(`[Jira] bulkUpdateIssues: Exceeded 1000 issue limit (${issueIdsOrKeys.length})`);
|
|
8694
|
+
throw new Error("Bulk update is limited to 1000 issues per request. Please split your request.");
|
|
8740
8695
|
}
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8696
|
+
const selectedActions = ["labels"];
|
|
8697
|
+
const editedFieldsInput = {
|
|
8698
|
+
labelsFields: [
|
|
8699
|
+
{
|
|
8700
|
+
fieldId: "labels",
|
|
8701
|
+
labels: labels.values.map((name) => ({ name })),
|
|
8702
|
+
bulkEditMultiSelectFieldOption: labels.action
|
|
8703
|
+
}
|
|
8704
|
+
]
|
|
8705
|
+
};
|
|
8706
|
+
console.error(`[Jira] bulkUpdateIssues: Updating labels [${labels.values.join(", ")}] with action ${labels.action}`);
|
|
8707
|
+
const result = await this.request("POST", "/bulk/issues/fields", {
|
|
8708
|
+
body: {
|
|
8709
|
+
selectedActions,
|
|
8710
|
+
selectedIssueIdsOrKeys: issueIdsOrKeys,
|
|
8711
|
+
editedFieldsInput
|
|
8712
|
+
}
|
|
8713
|
+
});
|
|
8714
|
+
console.error(`[Jira] bulkUpdateIssues: Task created with ID ${result.taskId}`);
|
|
8715
|
+
return {
|
|
8716
|
+
taskId: result.taskId,
|
|
8717
|
+
message: `Bulk label update started for ${issueIdsOrKeys.length} issue(s). Action: ${labels.action}.`,
|
|
8718
|
+
issueCount: issueIdsOrKeys.length
|
|
8747
8719
|
};
|
|
8748
|
-
content.push(table);
|
|
8749
|
-
}
|
|
8750
|
-
if (content.length === 0) {
|
|
8751
|
-
content.push({ type: "paragraph", content: [{ type: "text", text: " " }] });
|
|
8752
|
-
}
|
|
8753
|
-
return { type: "doc", version: 1, content };
|
|
8754
|
-
}
|
|
8755
|
-
function containsWikiTable(text) {
|
|
8756
|
-
return /^\|\|.+\|\|$/m.test(text) || /^\|(?!\|).+\|$/m.test(text);
|
|
8757
|
-
}
|
|
8758
|
-
var JiraApi = class {
|
|
8759
|
-
config;
|
|
8760
|
-
constructor(config) {
|
|
8761
|
-
this.config = config;
|
|
8762
8720
|
}
|
|
8763
8721
|
/**
|
|
8764
|
-
*
|
|
8722
|
+
* Get watchers for an issue
|
|
8765
8723
|
*/
|
|
8766
|
-
|
|
8767
|
-
const
|
|
8768
|
-
const
|
|
8769
|
-
|
|
8770
|
-
if (value === void 0 || value === "")
|
|
8771
|
-
return;
|
|
8772
|
-
url.searchParams.append(key, String(value));
|
|
8773
|
-
});
|
|
8774
|
-
return url.toString();
|
|
8724
|
+
async getWatchers(params) {
|
|
8725
|
+
const { issueKey } = params;
|
|
8726
|
+
const result = await this.request("GET", `/issue/${issueKey}/watchers`);
|
|
8727
|
+
return formatWatchers(result);
|
|
8775
8728
|
}
|
|
8776
8729
|
/**
|
|
8777
|
-
*
|
|
8730
|
+
* Add a watcher to an issue
|
|
8778
8731
|
*/
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8732
|
+
async addWatcher(params) {
|
|
8733
|
+
const { issueKey, accountId } = params;
|
|
8734
|
+
await this.request("POST", `/issue/${issueKey}/watchers`, {
|
|
8735
|
+
body: accountId
|
|
8736
|
+
});
|
|
8784
8737
|
}
|
|
8785
8738
|
/**
|
|
8786
|
-
*
|
|
8739
|
+
* Remove a watcher from an issue
|
|
8787
8740
|
*/
|
|
8788
|
-
async
|
|
8789
|
-
const
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
Accept: "application/json",
|
|
8793
|
-
"Content-Type": "application/json"
|
|
8794
|
-
};
|
|
8795
|
-
const response = await fetch(url, {
|
|
8796
|
-
method,
|
|
8797
|
-
headers,
|
|
8798
|
-
body: options.body ? JSON.stringify(options.body) : void 0
|
|
8741
|
+
async removeWatcher(params) {
|
|
8742
|
+
const { issueKey, accountId } = params;
|
|
8743
|
+
await this.request("DELETE", `/issue/${issueKey}/watchers`, {
|
|
8744
|
+
query: { accountId }
|
|
8799
8745
|
});
|
|
8800
|
-
const rateLimitInfo = parseRateLimitHeaders(response.headers);
|
|
8801
|
-
if (hasRateLimitInfo(rateLimitInfo)) {
|
|
8802
|
-
const logEntry = buildRateLimitLogEntry("jira", path, rateLimitInfo);
|
|
8803
|
-
console.error(JSON.stringify(logEntry));
|
|
8804
|
-
if (isNearLimit(rateLimitInfo)) {
|
|
8805
|
-
console.error(`[Jira] Rate limit warning: ${rateLimitInfo.usagePercent}% used (${rateLimitInfo.remaining}/${rateLimitInfo.limit} remaining)`);
|
|
8806
|
-
}
|
|
8807
|
-
}
|
|
8808
|
-
if (response.status === 429 && (options._retryCount ?? 0) < 1) {
|
|
8809
|
-
const retryAfterMs = rateLimitInfo.retryAfterMs ?? 5e3;
|
|
8810
|
-
const jitterMs = Math.floor(Math.random() * 1e3);
|
|
8811
|
-
const delayMs = Math.min(retryAfterMs + jitterMs, 1e4);
|
|
8812
|
-
const logEntry = buildRateLimitLogEntry("jira", path, rateLimitInfo, true);
|
|
8813
|
-
console.error(JSON.stringify(logEntry));
|
|
8814
|
-
console.error(`[Jira] Rate limited on ${path}, retrying after ${delayMs}ms`);
|
|
8815
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
8816
|
-
return this.request(method, path, {
|
|
8817
|
-
...options,
|
|
8818
|
-
_retryCount: (options._retryCount ?? 0) + 1
|
|
8819
|
-
});
|
|
8820
|
-
}
|
|
8821
|
-
if (!response.ok) {
|
|
8822
|
-
const errorBody = await response.text();
|
|
8823
|
-
throw new Error(`Jira API error (${response.status}): ${errorBody}`);
|
|
8824
|
-
}
|
|
8825
|
-
if (response.status === 204) {
|
|
8826
|
-
return {};
|
|
8827
|
-
}
|
|
8828
|
-
const data = await response.json();
|
|
8829
|
-
return data;
|
|
8830
8746
|
}
|
|
8831
8747
|
// ============================================================================
|
|
8832
|
-
//
|
|
8748
|
+
// Issue Link Operations
|
|
8833
8749
|
// ============================================================================
|
|
8834
8750
|
/**
|
|
8835
|
-
* Get issue
|
|
8751
|
+
* Get all available issue link types in the Jira instance
|
|
8836
8752
|
*/
|
|
8837
|
-
async
|
|
8838
|
-
const
|
|
8839
|
-
|
|
8840
|
-
query: {
|
|
8841
|
-
expand: expand?.join(",")
|
|
8842
|
-
}
|
|
8843
|
-
});
|
|
8844
|
-
return formatIssueDetails(issue, this.config.siteUrl);
|
|
8753
|
+
async getIssueLinkTypes() {
|
|
8754
|
+
const result = await this.request("GET", "/issueLinkType");
|
|
8755
|
+
return formatIssueLinkTypes(result);
|
|
8845
8756
|
}
|
|
8846
8757
|
/**
|
|
8847
|
-
*
|
|
8758
|
+
* Get all issue links for a specific issue
|
|
8848
8759
|
*/
|
|
8849
|
-
async
|
|
8850
|
-
const {
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
summary,
|
|
8854
|
-
issuetype: { name: issueTypeName },
|
|
8855
|
-
...customFields
|
|
8856
|
-
};
|
|
8857
|
-
if (description) {
|
|
8858
|
-
if (containsWikiTable(description)) {
|
|
8859
|
-
console.log("[JIRA-ADF] Converting wiki table to ADF", { length: description.length });
|
|
8860
|
-
fields.description = wikiMarkupToAdf(description);
|
|
8861
|
-
} else {
|
|
8862
|
-
fields.description = {
|
|
8863
|
-
type: "doc",
|
|
8864
|
-
version: 1,
|
|
8865
|
-
content: [
|
|
8866
|
-
{
|
|
8867
|
-
type: "paragraph",
|
|
8868
|
-
content: [{ type: "text", text: description }]
|
|
8869
|
-
}
|
|
8870
|
-
]
|
|
8871
|
-
};
|
|
8872
|
-
}
|
|
8873
|
-
}
|
|
8874
|
-
if (assignee) {
|
|
8875
|
-
fields.assignee = { id: assignee };
|
|
8876
|
-
}
|
|
8877
|
-
if (labels && labels.length > 0) {
|
|
8878
|
-
fields.labels = labels;
|
|
8879
|
-
}
|
|
8880
|
-
if (parentKey) {
|
|
8881
|
-
fields.parent = { key: parentKey };
|
|
8760
|
+
async getIssueLinks(params) {
|
|
8761
|
+
const { issueKey } = params;
|
|
8762
|
+
if (!isValidIssueKey(issueKey)) {
|
|
8763
|
+
throw new Error(`Invalid issue key format: ${issueKey}. Expected format: PROJECT-123`);
|
|
8882
8764
|
}
|
|
8883
|
-
const
|
|
8884
|
-
|
|
8765
|
+
const issue = await this.request("GET", `/issue/${issueKey}`, {
|
|
8766
|
+
query: { fields: "issuelinks" }
|
|
8885
8767
|
});
|
|
8886
|
-
return
|
|
8768
|
+
return formatIssueLinks(issue.fields?.issuelinks || [], this.config.siteUrl);
|
|
8887
8769
|
}
|
|
8888
8770
|
/**
|
|
8889
|
-
*
|
|
8771
|
+
* Create a link between two issues.
|
|
8772
|
+
* Uses intuitive sourceIssue/targetIssue terminology:
|
|
8773
|
+
* - sourceIssue: The issue doing the action (e.g., PROJ-1 in "PROJ-1 blocks PROJ-2")
|
|
8774
|
+
* - targetIssue: The issue being acted upon (e.g., PROJ-2 in "PROJ-1 blocks PROJ-2")
|
|
8775
|
+
*
|
|
8776
|
+
* This maps to Jira's API as: sourceIssue → outwardIssue, targetIssue → inwardIssue
|
|
8777
|
+
*
|
|
8778
|
+
* @returns void - Jira returns 201 with no body on success
|
|
8890
8779
|
*/
|
|
8891
|
-
async
|
|
8892
|
-
const {
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
fields.summary = summary;
|
|
8780
|
+
async createIssueLink(params) {
|
|
8781
|
+
const { linkType, sourceIssue, targetIssue } = params;
|
|
8782
|
+
if (!isValidIssueKey(sourceIssue)) {
|
|
8783
|
+
throw new Error(`Invalid source issue key format: ${sourceIssue}. Expected format: PROJECT-123`);
|
|
8896
8784
|
}
|
|
8897
|
-
if (
|
|
8898
|
-
|
|
8899
|
-
console.log("[JIRA-ADF] Converting wiki table to ADF (update)", { length: description.length });
|
|
8900
|
-
fields.description = wikiMarkupToAdf(description);
|
|
8901
|
-
} else {
|
|
8902
|
-
fields.description = {
|
|
8903
|
-
type: "doc",
|
|
8904
|
-
version: 1,
|
|
8905
|
-
content: [
|
|
8906
|
-
{
|
|
8907
|
-
type: "paragraph",
|
|
8908
|
-
content: [{ type: "text", text: description }]
|
|
8909
|
-
}
|
|
8910
|
-
]
|
|
8911
|
-
};
|
|
8912
|
-
}
|
|
8785
|
+
if (!isValidIssueKey(targetIssue)) {
|
|
8786
|
+
throw new Error(`Invalid target issue key format: ${targetIssue}. Expected format: PROJECT-123`);
|
|
8913
8787
|
}
|
|
8914
|
-
|
|
8915
|
-
|
|
8788
|
+
const linkTypes = await this.getIssueLinkTypes();
|
|
8789
|
+
const matchedType = linkTypes.find((t) => t.name.toLowerCase() === linkType.toLowerCase());
|
|
8790
|
+
if (!matchedType) {
|
|
8791
|
+
const availableTypes = linkTypes.map((t) => t.name).join(", ");
|
|
8792
|
+
throw new Error(`Invalid link type: "${linkType}". Available types: ${availableTypes}`);
|
|
8916
8793
|
}
|
|
8917
|
-
await this.request("
|
|
8918
|
-
body: {
|
|
8794
|
+
await this.request("POST", "/issueLink", {
|
|
8795
|
+
body: {
|
|
8796
|
+
type: { name: matchedType.name },
|
|
8797
|
+
outwardIssue: { key: sourceIssue },
|
|
8798
|
+
inwardIssue: { key: targetIssue }
|
|
8799
|
+
}
|
|
8919
8800
|
});
|
|
8920
8801
|
}
|
|
8921
8802
|
/**
|
|
8922
|
-
*
|
|
8803
|
+
* Delete an issue link by its ID
|
|
8923
8804
|
*/
|
|
8924
|
-
async
|
|
8925
|
-
const {
|
|
8926
|
-
|
|
8927
|
-
query: {
|
|
8928
|
-
jql,
|
|
8929
|
-
startAt,
|
|
8930
|
-
maxResults,
|
|
8931
|
-
fields: fields?.join(",") || "*all",
|
|
8932
|
-
expand: expand?.join(",")
|
|
8933
|
-
}
|
|
8934
|
-
});
|
|
8935
|
-
return formatSearchResults2(result, this.config.siteUrl);
|
|
8805
|
+
async deleteIssueLink(params) {
|
|
8806
|
+
const { linkId } = params;
|
|
8807
|
+
await this.request("DELETE", `/issueLink/${linkId}`);
|
|
8936
8808
|
}
|
|
8937
8809
|
/**
|
|
8938
|
-
*
|
|
8810
|
+
* Find a specific issue link between two issues by their keys and link type.
|
|
8811
|
+
* Searches bidirectionally - works regardless of which issue key is provided first.
|
|
8812
|
+
*
|
|
8813
|
+
* @returns The link ID if found, null if no matching link exists
|
|
8939
8814
|
*/
|
|
8940
|
-
async
|
|
8941
|
-
const
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8815
|
+
async findIssueLink(params) {
|
|
8816
|
+
const { issueKey, linkedIssueKey, linkType } = params;
|
|
8817
|
+
if (!isValidIssueKey(issueKey)) {
|
|
8818
|
+
throw new Error(`Invalid issue key format: ${issueKey}. Expected format: PROJECT-123`);
|
|
8819
|
+
}
|
|
8820
|
+
if (!isValidIssueKey(linkedIssueKey)) {
|
|
8821
|
+
throw new Error(`Invalid linked issue key format: ${linkedIssueKey}. Expected format: PROJECT-123`);
|
|
8822
|
+
}
|
|
8823
|
+
const links = await this.getIssueLinks({ issueKey });
|
|
8824
|
+
const linkTypeLower = linkType.toLowerCase();
|
|
8825
|
+
for (const link of links) {
|
|
8826
|
+
if (link.type.name.toLowerCase() !== linkTypeLower) {
|
|
8827
|
+
continue;
|
|
8946
8828
|
}
|
|
8947
|
-
|
|
8948
|
-
|
|
8829
|
+
const outwardKey = link.outwardIssue?.key;
|
|
8830
|
+
const inwardKey = link.inwardIssue?.key;
|
|
8831
|
+
if (outwardKey === linkedIssueKey || inwardKey === linkedIssueKey) {
|
|
8832
|
+
return link.id;
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8835
|
+
return null;
|
|
8949
8836
|
}
|
|
8837
|
+
// ============================================================================
|
|
8838
|
+
// Project Role & Member Operations
|
|
8839
|
+
// ============================================================================
|
|
8950
8840
|
/**
|
|
8951
|
-
* Get project
|
|
8841
|
+
* Get all project roles for a project.
|
|
8842
|
+
* Returns a list of roles with their IDs.
|
|
8952
8843
|
*/
|
|
8953
|
-
async
|
|
8954
|
-
const { projectKey
|
|
8955
|
-
const
|
|
8956
|
-
|
|
8957
|
-
expand: expand?.join(",")
|
|
8958
|
-
}
|
|
8959
|
-
});
|
|
8960
|
-
return formatProjectResponse(project, this.config.siteUrl);
|
|
8844
|
+
async getProjectRoles(params) {
|
|
8845
|
+
const { projectKey } = params;
|
|
8846
|
+
const rolesMap = await this.request("GET", `/project/${projectKey}/role`);
|
|
8847
|
+
return formatProjectRoles(rolesMap);
|
|
8961
8848
|
}
|
|
8962
8849
|
/**
|
|
8963
|
-
*
|
|
8850
|
+
* Get members (actors) for a specific project role.
|
|
8851
|
+
* Returns the role details with a simplified list of user and group members.
|
|
8964
8852
|
*/
|
|
8965
|
-
async
|
|
8966
|
-
const { projectKey } = params;
|
|
8967
|
-
const
|
|
8968
|
-
return
|
|
8853
|
+
async getProjectRoleMembers(params) {
|
|
8854
|
+
const { projectKey, roleId } = params;
|
|
8855
|
+
const role = await this.request("GET", `/project/${projectKey}/role/${roleId}`);
|
|
8856
|
+
return formatProjectRoleMembers(role);
|
|
8969
8857
|
}
|
|
8970
8858
|
/**
|
|
8971
|
-
*
|
|
8859
|
+
* Get all project members grouped by role.
|
|
8860
|
+
* Convenience method that fetches all roles then all members for each role.
|
|
8861
|
+
* Deduplicates users who appear in multiple roles.
|
|
8972
8862
|
*/
|
|
8973
|
-
async
|
|
8974
|
-
const {
|
|
8975
|
-
const
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
]
|
|
8863
|
+
async getAllProjectMembers(params) {
|
|
8864
|
+
const { projectKey } = params;
|
|
8865
|
+
const roles = await this.getProjectRoles({ projectKey });
|
|
8866
|
+
const roleDetails = await Promise.all(roles.map((role) => this.getProjectRoleMembers({ projectKey, roleId: role.id })));
|
|
8867
|
+
const memberMap = /* @__PURE__ */ new Map();
|
|
8868
|
+
for (const role of roleDetails) {
|
|
8869
|
+
for (const member of role.members) {
|
|
8870
|
+
const key = member.accountId || member.groupName || member.displayName;
|
|
8871
|
+
const existing = memberMap.get(key);
|
|
8872
|
+
if (existing) {
|
|
8873
|
+
existing.roles.push(role.name);
|
|
8874
|
+
} else {
|
|
8875
|
+
memberMap.set(key, { ...member, roles: [role.name] });
|
|
8986
8876
|
}
|
|
8987
8877
|
}
|
|
8988
|
-
}
|
|
8989
|
-
return
|
|
8878
|
+
}
|
|
8879
|
+
return {
|
|
8880
|
+
projectKey,
|
|
8881
|
+
roles: roleDetails.map((r) => ({
|
|
8882
|
+
name: r.name,
|
|
8883
|
+
id: r.id,
|
|
8884
|
+
members: r.members
|
|
8885
|
+
})),
|
|
8886
|
+
allMembers: Array.from(memberMap.values())
|
|
8887
|
+
};
|
|
8990
8888
|
}
|
|
8889
|
+
// ============================================================================
|
|
8890
|
+
// Attachment Operations
|
|
8891
|
+
// ============================================================================
|
|
8991
8892
|
/**
|
|
8992
|
-
*
|
|
8893
|
+
* List all attachments for an issue
|
|
8993
8894
|
*/
|
|
8994
|
-
async
|
|
8895
|
+
async listAttachments(params) {
|
|
8995
8896
|
const { issueKey } = params;
|
|
8996
|
-
|
|
8997
|
-
|
|
8897
|
+
if (!isValidIssueKey(issueKey)) {
|
|
8898
|
+
throw new Error(`Invalid issue key format: ${issueKey}. Expected format: PROJECT-123`);
|
|
8899
|
+
}
|
|
8900
|
+
const issue = await this.request("GET", `/issue/${issueKey}`, {
|
|
8901
|
+
query: { fields: "attachment" }
|
|
8902
|
+
});
|
|
8903
|
+
const attachments = issue.fields?.attachment || [];
|
|
8904
|
+
return attachments.map((att) => ({
|
|
8905
|
+
id: att.id,
|
|
8906
|
+
self: att.self,
|
|
8907
|
+
filename: att.filename,
|
|
8908
|
+
author: att.author,
|
|
8909
|
+
created: att.created,
|
|
8910
|
+
size: att.size,
|
|
8911
|
+
mimeType: att.mimeType,
|
|
8912
|
+
content: att.content,
|
|
8913
|
+
thumbnail: att.thumbnail
|
|
8914
|
+
}));
|
|
8998
8915
|
}
|
|
8999
8916
|
/**
|
|
9000
|
-
*
|
|
8917
|
+
* Upload an attachment to an issue.
|
|
8918
|
+
* Jira requires multipart/form-data with X-Atlassian-Token: no-check header.
|
|
8919
|
+
*
|
|
8920
|
+
* @param params.issueKey - The issue key (e.g., PROJ-123)
|
|
8921
|
+
* @param params.filename - Name for the uploaded file
|
|
8922
|
+
* @param params.content - Base64-encoded file content
|
|
8923
|
+
* @param params.mimeType - MIME type (auto-detected if omitted)
|
|
9001
8924
|
*/
|
|
9002
|
-
async
|
|
9003
|
-
const { issueKey,
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
8925
|
+
async uploadAttachment(params) {
|
|
8926
|
+
const { issueKey, filename, content, mimeType } = params;
|
|
8927
|
+
if (!isValidIssueKey(issueKey)) {
|
|
8928
|
+
throw new Error(`Invalid issue key format: ${issueKey}. Expected format: PROJECT-123`);
|
|
8929
|
+
}
|
|
8930
|
+
const binaryContent = Buffer.from(content, "base64");
|
|
8931
|
+
if (binaryContent.length > JIRA_MAX_ATTACHMENT_SIZE) {
|
|
8932
|
+
throw new Error(`File size (${Math.round(binaryContent.length / 1024 / 1024)}MB) exceeds maximum allowed size (${JIRA_MAX_ATTACHMENT_SIZE / 1024 / 1024}MB)`);
|
|
8933
|
+
}
|
|
8934
|
+
const detectedMimeType = mimeType || detectMimeType(filename);
|
|
8935
|
+
const formData = new FormData();
|
|
8936
|
+
const blob = new Blob([binaryContent], { type: detectedMimeType });
|
|
8937
|
+
formData.append("file", blob, filename);
|
|
8938
|
+
const url = `${this.config.apiBaseUrl}/issue/${issueKey}/attachments`;
|
|
8939
|
+
const response = await fetch(url, {
|
|
8940
|
+
method: "POST",
|
|
8941
|
+
headers: {
|
|
8942
|
+
Authorization: this.config.authHeader,
|
|
8943
|
+
Accept: "application/json",
|
|
8944
|
+
"X-Atlassian-Token": "no-check"
|
|
8945
|
+
// Required for attachment uploads
|
|
8946
|
+
},
|
|
8947
|
+
body: formData
|
|
9008
8948
|
});
|
|
9009
|
-
|
|
9010
|
-
|
|
8949
|
+
if (!response.ok) {
|
|
8950
|
+
const errorBody = await response.text();
|
|
8951
|
+
if (response.status === 413) {
|
|
8952
|
+
throw new Error(`File too large: The attachment exceeds Jira's maximum file size limit.`);
|
|
8953
|
+
}
|
|
8954
|
+
throw new Error(`Jira attachment upload error (${response.status}): ${errorBody}`);
|
|
8955
|
+
}
|
|
8956
|
+
const result = await response.json();
|
|
8957
|
+
return result;
|
|
9011
8958
|
}
|
|
9012
8959
|
/**
|
|
9013
|
-
*
|
|
8960
|
+
* Download an attachment by ID.
|
|
8961
|
+
* Returns the file content as base64-encoded string.
|
|
9014
8962
|
*/
|
|
9015
|
-
async
|
|
9016
|
-
const {
|
|
9017
|
-
await this.request("
|
|
9018
|
-
|
|
8963
|
+
async downloadAttachment(params) {
|
|
8964
|
+
const { attachmentId } = params;
|
|
8965
|
+
const metadata = await this.request("GET", `/attachment/${attachmentId}`);
|
|
8966
|
+
if (!metadata.content) {
|
|
8967
|
+
throw new Error(`Attachment ${attachmentId} has no download URL`);
|
|
8968
|
+
}
|
|
8969
|
+
const response = await fetch(metadata.content, {
|
|
8970
|
+
method: "GET",
|
|
8971
|
+
headers: {
|
|
8972
|
+
Authorization: this.config.authHeader
|
|
8973
|
+
}
|
|
9019
8974
|
});
|
|
8975
|
+
if (!response.ok) {
|
|
8976
|
+
throw new Error(`Failed to download attachment (${response.status}): ${response.statusText}`);
|
|
8977
|
+
}
|
|
8978
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
8979
|
+
const content = Buffer.from(arrayBuffer).toString("base64");
|
|
8980
|
+
return {
|
|
8981
|
+
filename: metadata.filename,
|
|
8982
|
+
mimeType: metadata.mimeType,
|
|
8983
|
+
size: metadata.size,
|
|
8984
|
+
content
|
|
8985
|
+
};
|
|
9020
8986
|
}
|
|
9021
8987
|
/**
|
|
9022
|
-
* Delete an
|
|
8988
|
+
* Delete an attachment by ID.
|
|
9023
8989
|
*/
|
|
9024
|
-
async
|
|
9025
|
-
const {
|
|
9026
|
-
|
|
9027
|
-
throw new Error("issueKey is required to delete an issue.");
|
|
9028
|
-
}
|
|
9029
|
-
await this.request("DELETE", `/issue/${issueKey}`);
|
|
8990
|
+
async deleteAttachment(params) {
|
|
8991
|
+
const { attachmentId } = params;
|
|
8992
|
+
await this.request("DELETE", `/attachment/${attachmentId}`);
|
|
9030
8993
|
}
|
|
8994
|
+
// ============================================================================
|
|
8995
|
+
// Agile API Access (Jira Software - Boards, Sprints)
|
|
8996
|
+
// ============================================================================
|
|
8997
|
+
_agileApi = null;
|
|
9031
8998
|
/**
|
|
9032
|
-
* Get
|
|
8999
|
+
* Get the Agile API client for board and sprint operations.
|
|
9000
|
+
* Lazily instantiated on first access.
|
|
9033
9001
|
*/
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9002
|
+
get agile() {
|
|
9003
|
+
if (!this._agileApi) {
|
|
9004
|
+
this._agileApi = new AgileApi(this.config);
|
|
9005
|
+
}
|
|
9006
|
+
return this._agileApi;
|
|
9037
9007
|
}
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9008
|
+
};
|
|
9009
|
+
|
|
9010
|
+
// ../../b4m-core/packages/common/dist/src/jira/webhook/format.js
|
|
9011
|
+
function daysUntil(dateString) {
|
|
9012
|
+
const expirationDate = new Date(dateString);
|
|
9013
|
+
const now = /* @__PURE__ */ new Date();
|
|
9014
|
+
const diffMs = expirationDate.getTime() - now.getTime();
|
|
9015
|
+
return Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
|
|
9016
|
+
}
|
|
9017
|
+
function formatWebhook(webhook) {
|
|
9018
|
+
const daysUntilExpiry = daysUntil(webhook.expirationDate);
|
|
9019
|
+
return {
|
|
9020
|
+
id: webhook.id,
|
|
9021
|
+
events: webhook.events,
|
|
9022
|
+
jqlFilter: webhook.jqlFilter,
|
|
9023
|
+
expirationDate: webhook.expirationDate,
|
|
9024
|
+
daysUntilExpiry,
|
|
9025
|
+
isExpiringSoon: daysUntilExpiry < 7
|
|
9026
|
+
};
|
|
9027
|
+
}
|
|
9028
|
+
function formatWebhookList(response) {
|
|
9029
|
+
return {
|
|
9030
|
+
webhooks: response.values.map(formatWebhook),
|
|
9031
|
+
total: response.total,
|
|
9032
|
+
hasMore: !response.isLast
|
|
9033
|
+
};
|
|
9034
|
+
}
|
|
9035
|
+
function extractAdfText(body, maxLength = 500) {
|
|
9036
|
+
if (!body || typeof body !== "object")
|
|
9037
|
+
return "";
|
|
9038
|
+
const doc = body;
|
|
9039
|
+
if (doc.type !== "doc" || !Array.isArray(doc.content))
|
|
9040
|
+
return "";
|
|
9041
|
+
const parts = [];
|
|
9042
|
+
let totalLength = 0;
|
|
9043
|
+
function walk(nodes) {
|
|
9044
|
+
for (const node of nodes) {
|
|
9045
|
+
if (totalLength >= maxLength)
|
|
9046
|
+
return;
|
|
9047
|
+
if (!node || typeof node !== "object")
|
|
9048
|
+
continue;
|
|
9049
|
+
const n = node;
|
|
9050
|
+
if (n.type === "text" && typeof n.text === "string") {
|
|
9051
|
+
const remaining = maxLength - totalLength;
|
|
9052
|
+
const text = n.text.slice(0, remaining);
|
|
9053
|
+
parts.push(text);
|
|
9054
|
+
totalLength += text.length;
|
|
9055
|
+
}
|
|
9056
|
+
if (Array.isArray(n.content)) {
|
|
9057
|
+
walk(n.content);
|
|
9047
9058
|
}
|
|
9048
|
-
});
|
|
9049
|
-
return formatUserList(users);
|
|
9050
|
-
}
|
|
9051
|
-
/**
|
|
9052
|
-
* Bulk create multiple issues in a single API call.
|
|
9053
|
-
* Supports up to 50 issues per request (Jira API limit).
|
|
9054
|
-
* Ideal for creating multiple subtasks under a parent issue.
|
|
9055
|
-
*/
|
|
9056
|
-
async bulkCreateIssues(params) {
|
|
9057
|
-
const { issues } = params;
|
|
9058
|
-
if (issues.length === 0) {
|
|
9059
|
-
return { issues: [], errors: [] };
|
|
9060
9059
|
}
|
|
9061
|
-
|
|
9062
|
-
|
|
9060
|
+
}
|
|
9061
|
+
walk(doc.content);
|
|
9062
|
+
const result = parts.join("");
|
|
9063
|
+
if (totalLength >= maxLength)
|
|
9064
|
+
return result + "...";
|
|
9065
|
+
return result;
|
|
9066
|
+
}
|
|
9067
|
+
function escapeSlackMrkdwn(text) {
|
|
9068
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\*/g, "\u2217").replace(/_/g, "\uFF3F").replace(/~/g, "\u223C").replace(/`/g, "\u02CB");
|
|
9069
|
+
}
|
|
9070
|
+
var PRIORITY_EMOJI = {
|
|
9071
|
+
Highest: ":red_circle:",
|
|
9072
|
+
High: ":large_orange_circle:",
|
|
9073
|
+
Medium: ":large_yellow_circle:",
|
|
9074
|
+
Low: ":large_green_circle:",
|
|
9075
|
+
Lowest: ":white_circle:"
|
|
9076
|
+
};
|
|
9077
|
+
function getPriorityEmoji(priority) {
|
|
9078
|
+
if (!priority)
|
|
9079
|
+
return "";
|
|
9080
|
+
return PRIORITY_EMOJI[priority] || ":blue_circle:";
|
|
9081
|
+
}
|
|
9082
|
+
function formatIssueEventForSlack(event, siteUrl) {
|
|
9083
|
+
const { issue, webhookEvent, user, changelog } = event;
|
|
9084
|
+
const { fields } = issue;
|
|
9085
|
+
const priorityEmoji = getPriorityEmoji(fields.priority?.name);
|
|
9086
|
+
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
9087
|
+
let action;
|
|
9088
|
+
let emoji;
|
|
9089
|
+
switch (webhookEvent) {
|
|
9090
|
+
case "jira:issue_created":
|
|
9091
|
+
action = "created";
|
|
9092
|
+
emoji = ":new:";
|
|
9093
|
+
break;
|
|
9094
|
+
case "jira:issue_updated":
|
|
9095
|
+
action = "updated";
|
|
9096
|
+
emoji = ":pencil2:";
|
|
9097
|
+
break;
|
|
9098
|
+
case "jira:issue_deleted":
|
|
9099
|
+
action = "deleted";
|
|
9100
|
+
emoji = ":wastebasket:";
|
|
9101
|
+
break;
|
|
9102
|
+
default:
|
|
9103
|
+
action = "changed";
|
|
9104
|
+
emoji = ":bell:";
|
|
9105
|
+
}
|
|
9106
|
+
let changelogText = "";
|
|
9107
|
+
if (changelog?.items && changelog.items.length > 0) {
|
|
9108
|
+
const changes = changelog.items.slice(0, 3).map((item) => {
|
|
9109
|
+
const from = escapeSlackMrkdwn(item.fromString || "None");
|
|
9110
|
+
const to = escapeSlackMrkdwn(item.toString || "None");
|
|
9111
|
+
if (item.field === "status") {
|
|
9112
|
+
return `Status: ${from} \u2192 ${to}`;
|
|
9113
|
+
}
|
|
9114
|
+
if (item.field === "assignee") {
|
|
9115
|
+
return `Assignee: ${escapeSlackMrkdwn(item.fromString || "Unassigned")} \u2192 ${escapeSlackMrkdwn(item.toString || "Unassigned")}`;
|
|
9116
|
+
}
|
|
9117
|
+
if (item.field === "priority") {
|
|
9118
|
+
return `Priority: ${from} \u2192 ${to}`;
|
|
9119
|
+
}
|
|
9120
|
+
return `${escapeSlackMrkdwn(item.field)}: ${from} \u2192 ${to}`;
|
|
9121
|
+
});
|
|
9122
|
+
if (changes.length > 0) {
|
|
9123
|
+
changelogText = "\n" + changes.map((c) => `\u2022 ${c}`).join("\n");
|
|
9124
|
+
if (changelog.items.length > 3) {
|
|
9125
|
+
changelogText += `
|
|
9126
|
+
_...and ${changelog.items.length - 3} more changes_`;
|
|
9127
|
+
}
|
|
9063
9128
|
}
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
if (containsWikiTable(description)) {
|
|
9073
|
-
console.log("[JIRA-ADF] Converting wiki table to ADF (bulk)", { length: description.length });
|
|
9074
|
-
fields.description = wikiMarkupToAdf(description);
|
|
9075
|
-
} else {
|
|
9076
|
-
fields.description = {
|
|
9077
|
-
type: "doc",
|
|
9078
|
-
version: 1,
|
|
9079
|
-
content: [
|
|
9080
|
-
{
|
|
9081
|
-
type: "paragraph",
|
|
9082
|
-
content: [{ type: "text", text: description }]
|
|
9083
|
-
}
|
|
9084
|
-
]
|
|
9085
|
-
};
|
|
9086
|
-
}
|
|
9129
|
+
}
|
|
9130
|
+
const blocks = [
|
|
9131
|
+
{
|
|
9132
|
+
type: "header",
|
|
9133
|
+
text: {
|
|
9134
|
+
type: "plain_text",
|
|
9135
|
+
text: `${emoji} Issue ${action}: ${escapeSlackMrkdwn(issue.key)}`,
|
|
9136
|
+
emoji: true
|
|
9087
9137
|
}
|
|
9088
|
-
|
|
9089
|
-
|
|
9138
|
+
},
|
|
9139
|
+
{
|
|
9140
|
+
type: "section",
|
|
9141
|
+
text: {
|
|
9142
|
+
type: "mrkdwn",
|
|
9143
|
+
text: `*<${issueUrl}|${escapeSlackMrkdwn(issue.key)}>* ${escapeSlackMrkdwn(fields.summary)}`
|
|
9090
9144
|
}
|
|
9091
|
-
|
|
9092
|
-
|
|
9145
|
+
},
|
|
9146
|
+
{
|
|
9147
|
+
type: "section",
|
|
9148
|
+
text: {
|
|
9149
|
+
type: "mrkdwn",
|
|
9150
|
+
text: [
|
|
9151
|
+
`${priorityEmoji} *Priority:* ${escapeSlackMrkdwn(fields.priority?.name || "None")}`,
|
|
9152
|
+
`*Status:* ${escapeSlackMrkdwn(fields.status.name)}`,
|
|
9153
|
+
`*Type:* ${escapeSlackMrkdwn(fields.issuetype.name)}`,
|
|
9154
|
+
`*Assignee:* ${escapeSlackMrkdwn(fields.assignee?.displayName || "Unassigned")}`
|
|
9155
|
+
].join(" | ")
|
|
9093
9156
|
}
|
|
9094
|
-
|
|
9095
|
-
|
|
9157
|
+
}
|
|
9158
|
+
];
|
|
9159
|
+
if (changelogText) {
|
|
9160
|
+
blocks.push({
|
|
9161
|
+
type: "section",
|
|
9162
|
+
text: {
|
|
9163
|
+
type: "mrkdwn",
|
|
9164
|
+
text: `*Changes:*${changelogText}`
|
|
9096
9165
|
}
|
|
9097
|
-
return { update: {}, fields };
|
|
9098
|
-
});
|
|
9099
|
-
const result = await this.request("POST", "/issue/bulk", {
|
|
9100
|
-
body: { issueUpdates }
|
|
9101
9166
|
});
|
|
9102
|
-
const formattedIssues = (result.issues || []).map((issue) => formatIssueResponse(issue, this.config.siteUrl));
|
|
9103
|
-
const formattedErrors = (result.errors || []).map((error) => ({
|
|
9104
|
-
status: error.status,
|
|
9105
|
-
message: Object.values(error.elementErrors?.errors || {}).join(", ") || "Unknown error",
|
|
9106
|
-
failedElementNumber: error.failedElementNumber
|
|
9107
|
-
}));
|
|
9108
|
-
return {
|
|
9109
|
-
issues: formattedIssues,
|
|
9110
|
-
errors: formattedErrors
|
|
9111
|
-
};
|
|
9112
9167
|
}
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9168
|
+
blocks.push({
|
|
9169
|
+
type: "context",
|
|
9170
|
+
elements: [
|
|
9171
|
+
{
|
|
9172
|
+
type: "mrkdwn",
|
|
9173
|
+
text: `${escapeSlackMrkdwn(user?.displayName || "Someone")} \u2022 ${escapeSlackMrkdwn(fields.project.name)} (${escapeSlackMrkdwn(fields.project.key)})`
|
|
9174
|
+
}
|
|
9175
|
+
]
|
|
9176
|
+
});
|
|
9177
|
+
blocks.push({
|
|
9178
|
+
type: "actions",
|
|
9179
|
+
elements: [
|
|
9180
|
+
{
|
|
9181
|
+
type: "button",
|
|
9182
|
+
text: {
|
|
9183
|
+
type: "plain_text",
|
|
9184
|
+
text: "View in Jira",
|
|
9185
|
+
emoji: true
|
|
9186
|
+
},
|
|
9187
|
+
url: issueUrl,
|
|
9188
|
+
style: "primary"
|
|
9189
|
+
}
|
|
9190
|
+
]
|
|
9191
|
+
});
|
|
9192
|
+
return {
|
|
9193
|
+
blocks,
|
|
9194
|
+
text: `${emoji} ${escapeSlackMrkdwn(issue.key)} ${action}: ${escapeSlackMrkdwn(fields.summary)}`
|
|
9195
|
+
};
|
|
9196
|
+
}
|
|
9197
|
+
function formatCommentEventForSlack(event, siteUrl) {
|
|
9198
|
+
const { issue, comment, webhookEvent } = event;
|
|
9199
|
+
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
9200
|
+
let action;
|
|
9201
|
+
let emoji;
|
|
9202
|
+
switch (webhookEvent) {
|
|
9203
|
+
case "comment_created":
|
|
9204
|
+
action = "commented on";
|
|
9205
|
+
emoji = ":speech_balloon:";
|
|
9206
|
+
break;
|
|
9207
|
+
case "comment_updated":
|
|
9208
|
+
action = "updated comment on";
|
|
9209
|
+
emoji = ":pencil:";
|
|
9210
|
+
break;
|
|
9211
|
+
case "comment_deleted":
|
|
9212
|
+
action = "deleted comment on";
|
|
9213
|
+
emoji = ":x:";
|
|
9214
|
+
break;
|
|
9215
|
+
default:
|
|
9216
|
+
action = "commented on";
|
|
9217
|
+
emoji = ":speech_balloon:";
|
|
9218
|
+
}
|
|
9219
|
+
const blocks = [
|
|
9220
|
+
{
|
|
9221
|
+
type: "header",
|
|
9222
|
+
text: {
|
|
9223
|
+
type: "plain_text",
|
|
9224
|
+
text: `${emoji} ${escapeSlackMrkdwn(comment.author.displayName)} ${action} ${escapeSlackMrkdwn(issue.key)}`,
|
|
9225
|
+
emoji: true
|
|
9226
|
+
}
|
|
9227
|
+
},
|
|
9228
|
+
{
|
|
9229
|
+
type: "section",
|
|
9230
|
+
text: {
|
|
9231
|
+
type: "mrkdwn",
|
|
9232
|
+
text: `*<${issueUrl}|${escapeSlackMrkdwn(issue.key)}>* ${escapeSlackMrkdwn(issue.fields.summary)}`
|
|
9233
|
+
}
|
|
9126
9234
|
}
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9235
|
+
];
|
|
9236
|
+
if (webhookEvent !== "comment_deleted") {
|
|
9237
|
+
const bodyText = extractAdfText(comment.body);
|
|
9238
|
+
if (bodyText) {
|
|
9239
|
+
blocks.push({
|
|
9240
|
+
type: "section",
|
|
9241
|
+
text: {
|
|
9242
|
+
type: "mrkdwn",
|
|
9243
|
+
text: `> ${escapeSlackMrkdwn(bodyText).replace(/\n/g, "\n> ")}`
|
|
9244
|
+
}
|
|
9245
|
+
});
|
|
9130
9246
|
}
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9247
|
+
}
|
|
9248
|
+
blocks.push({
|
|
9249
|
+
type: "context",
|
|
9250
|
+
elements: [
|
|
9251
|
+
{
|
|
9252
|
+
type: "mrkdwn",
|
|
9253
|
+
text: `${escapeSlackMrkdwn(issue.fields.project.name)} \u2022 ${escapeSlackMrkdwn(issue.fields.issuetype.name)}`
|
|
9254
|
+
}
|
|
9255
|
+
]
|
|
9256
|
+
}, {
|
|
9257
|
+
type: "actions",
|
|
9258
|
+
elements: [
|
|
9259
|
+
{
|
|
9260
|
+
type: "button",
|
|
9261
|
+
text: {
|
|
9262
|
+
type: "plain_text",
|
|
9263
|
+
text: "View in Jira",
|
|
9264
|
+
emoji: true
|
|
9265
|
+
},
|
|
9266
|
+
url: issueUrl,
|
|
9267
|
+
style: "primary"
|
|
9268
|
+
}
|
|
9269
|
+
]
|
|
9270
|
+
});
|
|
9271
|
+
return {
|
|
9272
|
+
blocks,
|
|
9273
|
+
text: `${emoji} ${escapeSlackMrkdwn(comment.author.displayName)} ${action} ${escapeSlackMrkdwn(issue.key)}: ${escapeSlackMrkdwn(issue.fields.summary)}`
|
|
9274
|
+
};
|
|
9275
|
+
}
|
|
9276
|
+
function formatSprintEventForSlack(event, siteUrl) {
|
|
9277
|
+
const { sprint, webhookEvent, user } = event;
|
|
9278
|
+
let action;
|
|
9279
|
+
let emoji;
|
|
9280
|
+
switch (webhookEvent) {
|
|
9281
|
+
case "sprint_created":
|
|
9282
|
+
action = "created";
|
|
9283
|
+
emoji = ":calendar:";
|
|
9284
|
+
break;
|
|
9285
|
+
case "sprint_started":
|
|
9286
|
+
action = "started";
|
|
9287
|
+
emoji = ":rocket:";
|
|
9288
|
+
break;
|
|
9289
|
+
case "sprint_closed":
|
|
9290
|
+
action = "completed";
|
|
9291
|
+
emoji = ":checkered_flag:";
|
|
9292
|
+
break;
|
|
9293
|
+
case "sprint_updated":
|
|
9294
|
+
action = "updated";
|
|
9295
|
+
emoji = ":pencil2:";
|
|
9296
|
+
break;
|
|
9297
|
+
case "sprint_deleted":
|
|
9298
|
+
action = "deleted";
|
|
9299
|
+
emoji = ":wastebasket:";
|
|
9300
|
+
break;
|
|
9301
|
+
default:
|
|
9302
|
+
action = "changed";
|
|
9303
|
+
emoji = ":bell:";
|
|
9304
|
+
}
|
|
9305
|
+
const sprintUrl = `${siteUrl}/secure/RapidBoard.jspa?rapidView=${sprint.originBoardId}`;
|
|
9306
|
+
const blocks = [
|
|
9307
|
+
{
|
|
9308
|
+
type: "header",
|
|
9309
|
+
text: {
|
|
9310
|
+
type: "plain_text",
|
|
9311
|
+
text: `${emoji} Sprint ${action}: ${escapeSlackMrkdwn(sprint.name)}`,
|
|
9312
|
+
emoji: true
|
|
9313
|
+
}
|
|
9314
|
+
},
|
|
9315
|
+
{
|
|
9316
|
+
type: "section",
|
|
9317
|
+
text: {
|
|
9318
|
+
type: "mrkdwn",
|
|
9319
|
+
text: [
|
|
9320
|
+
`*State:* ${escapeSlackMrkdwn(sprint.state)}`,
|
|
9321
|
+
sprint.startDate ? `*Start:* ${new Date(sprint.startDate).toLocaleDateString()}` : "",
|
|
9322
|
+
sprint.endDate ? `*End:* ${new Date(sprint.endDate).toLocaleDateString()}` : ""
|
|
9323
|
+
].filter(Boolean).join(" | ")
|
|
9135
9324
|
}
|
|
9136
|
-
transitionGroups[issue.transitionId].push(issue.issueIdOrKey);
|
|
9137
9325
|
}
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9326
|
+
];
|
|
9327
|
+
if (sprint.goal) {
|
|
9328
|
+
blocks.push({
|
|
9329
|
+
type: "section",
|
|
9330
|
+
text: {
|
|
9331
|
+
type: "mrkdwn",
|
|
9332
|
+
text: `*Goal:* ${escapeSlackMrkdwn(sprint.goal)}`
|
|
9333
|
+
}
|
|
9145
9334
|
});
|
|
9146
|
-
console.error(`[Jira] bulkTransitionIssues: Task created with ID ${result.taskId}`);
|
|
9147
|
-
return {
|
|
9148
|
-
taskId: result.taskId,
|
|
9149
|
-
message: `Bulk transition started for ${issues.length} issue(s).`,
|
|
9150
|
-
issueCount: issues.length
|
|
9151
|
-
};
|
|
9152
9335
|
}
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9336
|
+
blocks.push({
|
|
9337
|
+
type: "context",
|
|
9338
|
+
elements: [
|
|
9339
|
+
{
|
|
9340
|
+
type: "mrkdwn",
|
|
9341
|
+
text: user?.displayName ? escapeSlackMrkdwn(user.displayName) : "System"
|
|
9342
|
+
}
|
|
9343
|
+
]
|
|
9344
|
+
});
|
|
9345
|
+
blocks.push({
|
|
9346
|
+
type: "actions",
|
|
9347
|
+
elements: [
|
|
9348
|
+
{
|
|
9349
|
+
type: "button",
|
|
9350
|
+
text: {
|
|
9351
|
+
type: "plain_text",
|
|
9352
|
+
text: "View Board",
|
|
9353
|
+
emoji: true
|
|
9354
|
+
},
|
|
9355
|
+
url: sprintUrl,
|
|
9356
|
+
style: "primary"
|
|
9357
|
+
}
|
|
9358
|
+
]
|
|
9359
|
+
});
|
|
9360
|
+
return {
|
|
9361
|
+
blocks,
|
|
9362
|
+
text: `${emoji} Sprint ${action}: ${escapeSlackMrkdwn(sprint.name)}`
|
|
9363
|
+
};
|
|
9364
|
+
}
|
|
9365
|
+
function formatGenericEventForSlack(eventType, payload, siteUrl) {
|
|
9366
|
+
const issue = payload.issue;
|
|
9367
|
+
const user = payload.user;
|
|
9368
|
+
const issueLink = payload.issueLink;
|
|
9369
|
+
const changelog = payload.changelog;
|
|
9370
|
+
const project = payload.project ?? issue?.fields?.project;
|
|
9371
|
+
const version = payload.version;
|
|
9372
|
+
const worklog = payload.worklog;
|
|
9373
|
+
const board = payload.board;
|
|
9374
|
+
const readableEvent = eventType.replace(/^jira:/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
9375
|
+
const blocks = [
|
|
9376
|
+
{
|
|
9377
|
+
type: "header",
|
|
9378
|
+
text: {
|
|
9379
|
+
type: "plain_text",
|
|
9380
|
+
text: `:bell: ${readableEvent}`,
|
|
9381
|
+
emoji: true
|
|
9382
|
+
}
|
|
9176
9383
|
}
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
};
|
|
9187
|
-
console.error(`[Jira] bulkUpdateIssues: Updating labels [${labels.values.join(", ")}] with action ${labels.action}`);
|
|
9188
|
-
const result = await this.request("POST", "/bulk/issues/fields", {
|
|
9189
|
-
body: {
|
|
9190
|
-
selectedActions,
|
|
9191
|
-
selectedIssueIdsOrKeys: issueIdsOrKeys,
|
|
9192
|
-
editedFieldsInput
|
|
9384
|
+
];
|
|
9385
|
+
const details = [];
|
|
9386
|
+
if (issue?.key) {
|
|
9387
|
+
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
9388
|
+
blocks.push({
|
|
9389
|
+
type: "section",
|
|
9390
|
+
text: {
|
|
9391
|
+
type: "mrkdwn",
|
|
9392
|
+
text: `*<${issueUrl}|${escapeSlackMrkdwn(issue.key)}>* ${escapeSlackMrkdwn(issue.fields?.summary || "")}`
|
|
9193
9393
|
}
|
|
9194
9394
|
});
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9395
|
+
const meta = [];
|
|
9396
|
+
if (issue.fields?.status?.name)
|
|
9397
|
+
meta.push(`*Status:* ${escapeSlackMrkdwn(issue.fields.status.name)}`);
|
|
9398
|
+
if (issue.fields?.issuetype?.name)
|
|
9399
|
+
meta.push(`*Type:* ${escapeSlackMrkdwn(issue.fields.issuetype.name)}`);
|
|
9400
|
+
if (issue.fields?.priority?.name) {
|
|
9401
|
+
const emoji = getPriorityEmoji(issue.fields.priority.name);
|
|
9402
|
+
meta.push(`${emoji} *Priority:* ${escapeSlackMrkdwn(issue.fields.priority.name)}`);
|
|
9403
|
+
}
|
|
9404
|
+
if (issue.fields?.assignee?.displayName)
|
|
9405
|
+
meta.push(`*Assignee:* ${escapeSlackMrkdwn(issue.fields.assignee.displayName)}`);
|
|
9406
|
+
if (meta.length > 0)
|
|
9407
|
+
details.push(meta.join(" | "));
|
|
9201
9408
|
}
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9409
|
+
if (issueLink?.issueLinkType) {
|
|
9410
|
+
const linkType = issueLink.issueLinkType;
|
|
9411
|
+
const linkDesc = escapeSlackMrkdwn(linkType.outwardName || linkType.name || "linked");
|
|
9412
|
+
details.push(`*Link type:* ${linkDesc}`);
|
|
9413
|
+
if (issueLink.sourceIssueId)
|
|
9414
|
+
details.push(`*Source issue ID:* ${issueLink.sourceIssueId}`);
|
|
9415
|
+
if (issueLink.destinationIssueId)
|
|
9416
|
+
details.push(`*Destination issue ID:* ${issueLink.destinationIssueId}`);
|
|
9209
9417
|
}
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
});
|
|
9418
|
+
if (version?.name) {
|
|
9419
|
+
const versionParts = [`*Version:* ${escapeSlackMrkdwn(version.name)}`];
|
|
9420
|
+
if (version.description)
|
|
9421
|
+
versionParts.push(escapeSlackMrkdwn(version.description));
|
|
9422
|
+
if (version.released !== void 0)
|
|
9423
|
+
versionParts.push(version.released ? ":white_check_mark: Released" : "Unreleased");
|
|
9424
|
+
details.push(versionParts.join(" | "));
|
|
9218
9425
|
}
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9426
|
+
if (worklog) {
|
|
9427
|
+
const wlParts = [];
|
|
9428
|
+
if (worklog.author?.displayName)
|
|
9429
|
+
wlParts.push(`*By:* ${escapeSlackMrkdwn(worklog.author.displayName)}`);
|
|
9430
|
+
if (worklog.timeSpent)
|
|
9431
|
+
wlParts.push(`*Time:* ${escapeSlackMrkdwn(worklog.timeSpent)}`);
|
|
9432
|
+
if (wlParts.length > 0)
|
|
9433
|
+
details.push(wlParts.join(" | "));
|
|
9227
9434
|
}
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
// ============================================================================
|
|
9231
|
-
/**
|
|
9232
|
-
* Get all available issue link types in the Jira instance
|
|
9233
|
-
*/
|
|
9234
|
-
async getIssueLinkTypes() {
|
|
9235
|
-
const result = await this.request("GET", "/issueLinkType");
|
|
9236
|
-
return formatIssueLinkTypes(result);
|
|
9435
|
+
if (board?.name) {
|
|
9436
|
+
details.push(`*Board:* ${escapeSlackMrkdwn(board.name)}`);
|
|
9237
9437
|
}
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
const { issueKey } = params;
|
|
9243
|
-
if (!isValidIssueKey(issueKey)) {
|
|
9244
|
-
throw new Error(`Invalid issue key format: ${issueKey}. Expected format: PROJECT-123`);
|
|
9438
|
+
if (changelog?.items && changelog.items.length > 0) {
|
|
9439
|
+
const changes = changelog.items.slice(0, 3).map((item) => `\u2022 ${escapeSlackMrkdwn(item.field || "")}: ${escapeSlackMrkdwn(item.fromString || "None")} \u2192 ${escapeSlackMrkdwn(item.toString || "None")}`);
|
|
9440
|
+
if (changelog.items.length > 3) {
|
|
9441
|
+
changes.push(`_...and ${changelog.items.length - 3} more_`);
|
|
9245
9442
|
}
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
});
|
|
9249
|
-
return formatIssueLinks(issue.fields?.issuelinks || [], this.config.siteUrl);
|
|
9443
|
+
details.push(`*Changes:*
|
|
9444
|
+
${changes.join("\n")}`);
|
|
9250
9445
|
}
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
* This maps to Jira's API as: sourceIssue → outwardIssue, targetIssue → inwardIssue
|
|
9258
|
-
*
|
|
9259
|
-
* @returns void - Jira returns 201 with no body on success
|
|
9260
|
-
*/
|
|
9261
|
-
async createIssueLink(params) {
|
|
9262
|
-
const { linkType, sourceIssue, targetIssue } = params;
|
|
9263
|
-
if (!isValidIssueKey(sourceIssue)) {
|
|
9264
|
-
throw new Error(`Invalid source issue key format: ${sourceIssue}. Expected format: PROJECT-123`);
|
|
9265
|
-
}
|
|
9266
|
-
if (!isValidIssueKey(targetIssue)) {
|
|
9267
|
-
throw new Error(`Invalid target issue key format: ${targetIssue}. Expected format: PROJECT-123`);
|
|
9268
|
-
}
|
|
9269
|
-
const linkTypes = await this.getIssueLinkTypes();
|
|
9270
|
-
const matchedType = linkTypes.find((t) => t.name.toLowerCase() === linkType.toLowerCase());
|
|
9271
|
-
if (!matchedType) {
|
|
9272
|
-
const availableTypes = linkTypes.map((t) => t.name).join(", ");
|
|
9273
|
-
throw new Error(`Invalid link type: "${linkType}". Available types: ${availableTypes}`);
|
|
9274
|
-
}
|
|
9275
|
-
await this.request("POST", "/issueLink", {
|
|
9276
|
-
body: {
|
|
9277
|
-
type: { name: matchedType.name },
|
|
9278
|
-
outwardIssue: { key: sourceIssue },
|
|
9279
|
-
inwardIssue: { key: targetIssue }
|
|
9446
|
+
if (details.length > 0) {
|
|
9447
|
+
blocks.push({
|
|
9448
|
+
type: "section",
|
|
9449
|
+
text: {
|
|
9450
|
+
type: "mrkdwn",
|
|
9451
|
+
text: details.join("\n")
|
|
9280
9452
|
}
|
|
9281
9453
|
});
|
|
9282
9454
|
}
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9455
|
+
const contextParts = [];
|
|
9456
|
+
if (user?.displayName)
|
|
9457
|
+
contextParts.push(escapeSlackMrkdwn(user.displayName));
|
|
9458
|
+
if (project?.name)
|
|
9459
|
+
contextParts.push(`${escapeSlackMrkdwn(project.name)}${project.key ? ` (${escapeSlackMrkdwn(project.key)})` : ""}`);
|
|
9460
|
+
if (contextParts.length > 0) {
|
|
9461
|
+
blocks.push({
|
|
9462
|
+
type: "context",
|
|
9463
|
+
elements: [{ type: "mrkdwn", text: contextParts.join(" \u2022 ") }]
|
|
9464
|
+
});
|
|
9289
9465
|
}
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
}
|
|
9304
|
-
const links = await this.getIssueLinks({ issueKey });
|
|
9305
|
-
const linkTypeLower = linkType.toLowerCase();
|
|
9306
|
-
for (const link of links) {
|
|
9307
|
-
if (link.type.name.toLowerCase() !== linkTypeLower) {
|
|
9308
|
-
continue;
|
|
9309
|
-
}
|
|
9310
|
-
const outwardKey = link.outwardIssue?.key;
|
|
9311
|
-
const inwardKey = link.inwardIssue?.key;
|
|
9312
|
-
if (outwardKey === linkedIssueKey || inwardKey === linkedIssueKey) {
|
|
9313
|
-
return link.id;
|
|
9314
|
-
}
|
|
9315
|
-
}
|
|
9316
|
-
return null;
|
|
9466
|
+
if (issue?.key) {
|
|
9467
|
+
const issueUrl = `${siteUrl}/browse/${issue.key}`;
|
|
9468
|
+
blocks.push({
|
|
9469
|
+
type: "actions",
|
|
9470
|
+
elements: [
|
|
9471
|
+
{
|
|
9472
|
+
type: "button",
|
|
9473
|
+
text: { type: "plain_text", text: "View in Jira", emoji: true },
|
|
9474
|
+
url: issueUrl,
|
|
9475
|
+
style: "primary"
|
|
9476
|
+
}
|
|
9477
|
+
]
|
|
9478
|
+
});
|
|
9317
9479
|
}
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9480
|
+
const summary = issue?.key ? `${escapeSlackMrkdwn(issue.key)}: ${escapeSlackMrkdwn(issue.fields?.summary || readableEvent)}` : readableEvent;
|
|
9481
|
+
return {
|
|
9482
|
+
blocks,
|
|
9483
|
+
text: `:bell: ${summary}`
|
|
9484
|
+
};
|
|
9485
|
+
}
|
|
9486
|
+
|
|
9487
|
+
// ../../b4m-core/packages/common/dist/src/jira/webhook/api.js
|
|
9488
|
+
var WebhookApi = class {
|
|
9489
|
+
config;
|
|
9490
|
+
constructor(config) {
|
|
9491
|
+
this.config = config;
|
|
9329
9492
|
}
|
|
9330
9493
|
/**
|
|
9331
|
-
*
|
|
9332
|
-
*
|
|
9494
|
+
* Build URL for webhook API endpoints.
|
|
9495
|
+
*
|
|
9496
|
+
* Note: Webhooks use a different base URL than other Jira APIs.
|
|
9333
9497
|
*/
|
|
9334
|
-
|
|
9335
|
-
const
|
|
9336
|
-
const
|
|
9337
|
-
|
|
9498
|
+
buildUrl(path, query = {}) {
|
|
9499
|
+
const url = new URL(`${this.config.apiBaseUrl}${path}`);
|
|
9500
|
+
for (const [key, value] of Object.entries(query)) {
|
|
9501
|
+
if (value !== void 0) {
|
|
9502
|
+
url.searchParams.append(key, String(value));
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
return url.toString();
|
|
9338
9506
|
}
|
|
9339
9507
|
/**
|
|
9340
|
-
*
|
|
9341
|
-
* Convenience method that fetches all roles then all members for each role.
|
|
9342
|
-
* Deduplicates users who appear in multiple roles.
|
|
9508
|
+
* Make an authenticated request to the Jira Webhook API.
|
|
9343
9509
|
*/
|
|
9344
|
-
async
|
|
9345
|
-
const
|
|
9346
|
-
const
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
for (const member of role.members) {
|
|
9351
|
-
const key = member.accountId || member.groupName || member.displayName;
|
|
9352
|
-
const existing = memberMap.get(key);
|
|
9353
|
-
if (existing) {
|
|
9354
|
-
existing.roles.push(role.name);
|
|
9355
|
-
} else {
|
|
9356
|
-
memberMap.set(key, { ...member, roles: [role.name] });
|
|
9357
|
-
}
|
|
9358
|
-
}
|
|
9359
|
-
}
|
|
9360
|
-
return {
|
|
9361
|
-
projectKey,
|
|
9362
|
-
roles: roleDetails.map((r) => ({
|
|
9363
|
-
name: r.name,
|
|
9364
|
-
id: r.id,
|
|
9365
|
-
members: r.members
|
|
9366
|
-
})),
|
|
9367
|
-
allMembers: Array.from(memberMap.values())
|
|
9510
|
+
async request(method, path, options = {}) {
|
|
9511
|
+
const url = this.buildUrl(path, options.query);
|
|
9512
|
+
const headers = {
|
|
9513
|
+
Authorization: this.config.authHeader,
|
|
9514
|
+
Accept: "application/json",
|
|
9515
|
+
"Content-Type": "application/json"
|
|
9368
9516
|
};
|
|
9517
|
+
const response = await fetch(url, {
|
|
9518
|
+
method,
|
|
9519
|
+
headers,
|
|
9520
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
9521
|
+
});
|
|
9522
|
+
if (!response.ok) {
|
|
9523
|
+
const errorBody = await response.text();
|
|
9524
|
+
throw new Error(`Jira Webhook API error (${response.status}): ${errorBody}`);
|
|
9525
|
+
}
|
|
9526
|
+
if (response.status === 204) {
|
|
9527
|
+
return {};
|
|
9528
|
+
}
|
|
9529
|
+
const data = await response.json();
|
|
9530
|
+
return data;
|
|
9369
9531
|
}
|
|
9370
9532
|
// ============================================================================
|
|
9371
|
-
//
|
|
9533
|
+
// Webhook CRUD Operations
|
|
9372
9534
|
// ============================================================================
|
|
9373
9535
|
/**
|
|
9374
|
-
* List all
|
|
9536
|
+
* List all webhooks registered by this OAuth app.
|
|
9537
|
+
*
|
|
9538
|
+
* @param startAt - Pagination start index (default: 0)
|
|
9539
|
+
* @param maxResults - Max results per page (default: 50, max: 100)
|
|
9375
9540
|
*/
|
|
9376
|
-
async
|
|
9377
|
-
const {
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
}
|
|
9381
|
-
const issue = await this.request("GET", `/issue/${issueKey}`, {
|
|
9382
|
-
query: { fields: "attachment" }
|
|
9541
|
+
async listWebhooks(params = {}) {
|
|
9542
|
+
const { startAt = 0, maxResults = 50 } = params;
|
|
9543
|
+
const response = await this.request("GET", "/webhook", {
|
|
9544
|
+
query: { startAt, maxResults }
|
|
9383
9545
|
});
|
|
9384
|
-
|
|
9385
|
-
return attachments.map((att) => ({
|
|
9386
|
-
id: att.id,
|
|
9387
|
-
self: att.self,
|
|
9388
|
-
filename: att.filename,
|
|
9389
|
-
author: att.author,
|
|
9390
|
-
created: att.created,
|
|
9391
|
-
size: att.size,
|
|
9392
|
-
mimeType: att.mimeType,
|
|
9393
|
-
content: att.content,
|
|
9394
|
-
thumbnail: att.thumbnail
|
|
9395
|
-
}));
|
|
9546
|
+
return formatWebhookList(response);
|
|
9396
9547
|
}
|
|
9397
9548
|
/**
|
|
9398
|
-
*
|
|
9399
|
-
* Jira requires multipart/form-data with X-Atlassian-Token: no-check header.
|
|
9549
|
+
* Register a new webhook.
|
|
9400
9550
|
*
|
|
9401
|
-
* @param
|
|
9402
|
-
* @param
|
|
9403
|
-
* @param
|
|
9404
|
-
* @
|
|
9551
|
+
* @param url - The URL where Jira will POST webhook events
|
|
9552
|
+
* @param events - Events to subscribe to
|
|
9553
|
+
* @param jqlFilter - Optional JQL filter (only matching issues trigger events)
|
|
9554
|
+
* @returns The created webhook ID
|
|
9405
9555
|
*/
|
|
9406
|
-
async
|
|
9407
|
-
const {
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
const response = await fetch(url, {
|
|
9421
|
-
method: "POST",
|
|
9422
|
-
headers: {
|
|
9423
|
-
Authorization: this.config.authHeader,
|
|
9424
|
-
Accept: "application/json",
|
|
9425
|
-
"X-Atlassian-Token": "no-check"
|
|
9426
|
-
// Required for attachment uploads
|
|
9427
|
-
},
|
|
9428
|
-
body: formData
|
|
9556
|
+
async registerWebhook(params) {
|
|
9557
|
+
const { url, events, jqlFilter } = params;
|
|
9558
|
+
const webhook = {
|
|
9559
|
+
// Jira webhook API requires jqlFilter but has limited operator support.
|
|
9560
|
+
// "IS NOT EMPTY" and empty strings are rejected. Use a universally-true expression.
|
|
9561
|
+
jqlFilter: jqlFilter || "project != null",
|
|
9562
|
+
events
|
|
9563
|
+
};
|
|
9564
|
+
const body = {
|
|
9565
|
+
url,
|
|
9566
|
+
webhooks: [webhook]
|
|
9567
|
+
};
|
|
9568
|
+
const response = await this.request("POST", "/webhook", {
|
|
9569
|
+
body
|
|
9429
9570
|
});
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
throw new Error(`File too large: The attachment exceeds Jira's maximum file size limit.`);
|
|
9434
|
-
}
|
|
9435
|
-
throw new Error(`Jira attachment upload error (${response.status}): ${errorBody}`);
|
|
9571
|
+
console.log("[JIRA-WEBHOOK-API] registerWebhook raw response:", JSON.stringify(response, null, 2));
|
|
9572
|
+
if (!response.webhookRegistrationResult || response.webhookRegistrationResult.length === 0) {
|
|
9573
|
+
throw new Error("Failed to register webhook: No registration result returned");
|
|
9436
9574
|
}
|
|
9437
|
-
const result =
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
/**
|
|
9441
|
-
* Download an attachment by ID.
|
|
9442
|
-
* Returns the file content as base64-encoded string.
|
|
9443
|
-
*/
|
|
9444
|
-
async downloadAttachment(params) {
|
|
9445
|
-
const { attachmentId } = params;
|
|
9446
|
-
const metadata = await this.request("GET", `/attachment/${attachmentId}`);
|
|
9447
|
-
if (!metadata.content) {
|
|
9448
|
-
throw new Error(`Attachment ${attachmentId} has no download URL`);
|
|
9575
|
+
const result = response.webhookRegistrationResult[0];
|
|
9576
|
+
if (result.errors && result.errors.length > 0) {
|
|
9577
|
+
throw new Error(`Failed to register webhook: ${result.errors.join(", ")}`);
|
|
9449
9578
|
}
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
headers: {
|
|
9453
|
-
Authorization: this.config.authHeader
|
|
9454
|
-
}
|
|
9455
|
-
});
|
|
9456
|
-
if (!response.ok) {
|
|
9457
|
-
throw new Error(`Failed to download attachment (${response.status}): ${response.statusText}`);
|
|
9579
|
+
if (result.createdWebhookId === void 0 || result.createdWebhookId === null) {
|
|
9580
|
+
throw new Error(`Failed to register webhook: No webhook ID in response. Full result: ${JSON.stringify(result)}`);
|
|
9458
9581
|
}
|
|
9459
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
9460
|
-
const content = Buffer.from(arrayBuffer).toString("base64");
|
|
9461
9582
|
return {
|
|
9462
|
-
|
|
9463
|
-
mimeType: metadata.mimeType,
|
|
9464
|
-
size: metadata.size,
|
|
9465
|
-
content
|
|
9583
|
+
webhookId: result.createdWebhookId
|
|
9466
9584
|
};
|
|
9467
9585
|
}
|
|
9468
9586
|
/**
|
|
9469
|
-
*
|
|
9587
|
+
* Refresh webhook expiration dates.
|
|
9588
|
+
*
|
|
9589
|
+
* Jira webhooks expire after 30 days. Call this to extend expiration.
|
|
9590
|
+
*
|
|
9591
|
+
* @param webhookIds - IDs of webhooks to refresh
|
|
9592
|
+
* @returns New expiration date
|
|
9470
9593
|
*/
|
|
9471
|
-
async
|
|
9472
|
-
const {
|
|
9473
|
-
|
|
9594
|
+
async refreshWebhooks(params) {
|
|
9595
|
+
const { webhookIds } = params;
|
|
9596
|
+
const body = {
|
|
9597
|
+
webhookIds
|
|
9598
|
+
};
|
|
9599
|
+
const response = await this.request("PUT", "/webhook/refresh", {
|
|
9600
|
+
body
|
|
9601
|
+
});
|
|
9602
|
+
return {
|
|
9603
|
+
expirationDate: response.expirationDate
|
|
9604
|
+
};
|
|
9474
9605
|
}
|
|
9475
|
-
// ============================================================================
|
|
9476
|
-
// Agile API Access (Jira Software - Boards, Sprints)
|
|
9477
|
-
// ============================================================================
|
|
9478
|
-
_agileApi = null;
|
|
9479
9606
|
/**
|
|
9480
|
-
*
|
|
9481
|
-
*
|
|
9607
|
+
* Delete webhooks by ID.
|
|
9608
|
+
*
|
|
9609
|
+
* @param webhookIds - IDs of webhooks to delete
|
|
9482
9610
|
*/
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9611
|
+
async deleteWebhooks(params) {
|
|
9612
|
+
const { webhookIds } = params;
|
|
9613
|
+
const body = {
|
|
9614
|
+
webhookIds
|
|
9615
|
+
};
|
|
9616
|
+
await this.request("DELETE", "/webhook", {
|
|
9617
|
+
body
|
|
9618
|
+
});
|
|
9488
9619
|
}
|
|
9489
|
-
// ============================================================================
|
|
9490
|
-
// Webhook API Access (Webhook Management)
|
|
9491
|
-
// ============================================================================
|
|
9492
|
-
_webhookApi = null;
|
|
9493
9620
|
/**
|
|
9494
|
-
* Get
|
|
9495
|
-
*
|
|
9621
|
+
* Get a specific webhook by ID.
|
|
9622
|
+
*
|
|
9623
|
+
* Note: Jira API doesn't have a direct "get by ID" endpoint,
|
|
9624
|
+
* so we list and filter.
|
|
9496
9625
|
*/
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9626
|
+
async getWebhook(params) {
|
|
9627
|
+
const { webhookId } = params;
|
|
9628
|
+
const response = await this.request("GET", "/webhook", {
|
|
9629
|
+
query: { startAt: 0, maxResults: 100 }
|
|
9630
|
+
});
|
|
9631
|
+
const webhook = response.values.find((w) => w.id === webhookId);
|
|
9632
|
+
return webhook || null;
|
|
9502
9633
|
}
|
|
9503
9634
|
};
|
|
9504
9635
|
|
|
@@ -11399,6 +11530,12 @@ export {
|
|
|
11399
11530
|
CliCompletionDoneAction,
|
|
11400
11531
|
CliCompletionErrorAction,
|
|
11401
11532
|
CliToolResponseAction,
|
|
11533
|
+
KeepCommandRequestAction,
|
|
11534
|
+
KeepCommandAction,
|
|
11535
|
+
KeepCommandResponseAction,
|
|
11536
|
+
KeepCommandResultAction,
|
|
11537
|
+
TavernSceneCommandRequestAction,
|
|
11538
|
+
TavernSceneBroadcastAction,
|
|
11402
11539
|
SessionCreatedAction,
|
|
11403
11540
|
MessageDataToServer,
|
|
11404
11541
|
MessageDataToClient,
|
|
@@ -11646,6 +11783,13 @@ export {
|
|
|
11646
11783
|
formatProjectRoles,
|
|
11647
11784
|
formatProjectRoleMembers,
|
|
11648
11785
|
AgileApi,
|
|
11786
|
+
JIRA_MAX_ATTACHMENT_SIZE,
|
|
11787
|
+
JIRA_PROJECT_KEY_RE,
|
|
11788
|
+
isValidProjectKey,
|
|
11789
|
+
isValidIssueKey,
|
|
11790
|
+
wikiMarkupToAdf,
|
|
11791
|
+
containsWikiTable,
|
|
11792
|
+
JiraApi,
|
|
11649
11793
|
formatWebhook,
|
|
11650
11794
|
formatWebhookList,
|
|
11651
11795
|
extractAdfText,
|
|
@@ -11655,11 +11799,6 @@ export {
|
|
|
11655
11799
|
formatSprintEventForSlack,
|
|
11656
11800
|
formatGenericEventForSlack,
|
|
11657
11801
|
WebhookApi,
|
|
11658
|
-
JIRA_MAX_ATTACHMENT_SIZE,
|
|
11659
|
-
isValidIssueKey,
|
|
11660
|
-
wikiMarkupToAdf,
|
|
11661
|
-
containsWikiTable,
|
|
11662
|
-
JiraApi,
|
|
11663
11802
|
isIssueWebhookEvent,
|
|
11664
11803
|
isCommentWebhookEvent,
|
|
11665
11804
|
isSprintWebhookEvent,
|