jirallm 0.1.1 → 0.3.0
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/README.md +75 -0
- package/dist/cli/commands/assign.d.ts +9 -0
- package/dist/cli/commands/assign.d.ts.map +1 -0
- package/dist/cli/commands/assign.js +37 -0
- package/dist/cli/commands/assign.js.map +1 -0
- package/dist/cli/commands/assign.test.d.ts +2 -0
- package/dist/cli/commands/assign.test.d.ts.map +1 -0
- package/dist/cli/commands/assign.test.js +69 -0
- package/dist/cli/commands/assign.test.js.map +1 -0
- package/dist/cli/commands/attach.d.ts +16 -0
- package/dist/cli/commands/attach.d.ts.map +1 -0
- package/dist/cli/commands/attach.js +52 -0
- package/dist/cli/commands/attach.js.map +1 -0
- package/dist/cli/commands/attach.test.d.ts +2 -0
- package/dist/cli/commands/attach.test.d.ts.map +1 -0
- package/dist/cli/commands/attach.test.js +92 -0
- package/dist/cli/commands/attach.test.js.map +1 -0
- package/dist/cli/commands/board.d.ts +10 -0
- package/dist/cli/commands/board.d.ts.map +1 -0
- package/dist/cli/commands/board.js +54 -0
- package/dist/cli/commands/board.js.map +1 -0
- package/dist/cli/commands/boards.d.ts +11 -0
- package/dist/cli/commands/boards.d.ts.map +1 -0
- package/dist/cli/commands/boards.js +27 -0
- package/dist/cli/commands/boards.js.map +1 -0
- package/dist/cli/commands/boards.test.d.ts +2 -0
- package/dist/cli/commands/boards.test.d.ts.map +1 -0
- package/dist/cli/commands/boards.test.js +80 -0
- package/dist/cli/commands/boards.test.js.map +1 -0
- package/dist/cli/commands/comment.d.ts +15 -0
- package/dist/cli/commands/comment.d.ts.map +1 -0
- package/dist/cli/commands/comment.js +61 -0
- package/dist/cli/commands/comment.js.map +1 -0
- package/dist/cli/commands/components.d.ts +7 -0
- package/dist/cli/commands/components.d.ts.map +1 -0
- package/dist/cli/commands/components.js +23 -0
- package/dist/cli/commands/components.js.map +1 -0
- package/dist/cli/commands/components.test.d.ts +2 -0
- package/dist/cli/commands/components.test.d.ts.map +1 -0
- package/dist/cli/commands/components.test.js +62 -0
- package/dist/cli/commands/components.test.js.map +1 -0
- package/dist/cli/commands/create.d.ts +18 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +49 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/create.test.d.ts +2 -0
- package/dist/cli/commands/create.test.d.ts.map +1 -0
- package/dist/cli/commands/create.test.js +133 -0
- package/dist/cli/commands/create.test.js.map +1 -0
- package/dist/cli/commands/edit.d.ts +17 -0
- package/dist/cli/commands/edit.d.ts.map +1 -0
- package/dist/cli/commands/edit.js +52 -0
- package/dist/cli/commands/edit.js.map +1 -0
- package/dist/cli/commands/edit.test.d.ts +2 -0
- package/dist/cli/commands/edit.test.d.ts.map +1 -0
- package/dist/cli/commands/edit.test.js +96 -0
- package/dist/cli/commands/edit.test.js.map +1 -0
- package/dist/cli/commands/fetch.d.ts +7 -0
- package/dist/cli/commands/fetch.d.ts.map +1 -0
- package/dist/cli/commands/fetch.js +32 -0
- package/dist/cli/commands/fetch.js.map +1 -0
- package/dist/cli/commands/fetch.test.d.ts +2 -0
- package/dist/cli/commands/fetch.test.d.ts.map +1 -0
- package/dist/cli/commands/fetch.test.js +93 -0
- package/dist/cli/commands/fetch.test.js.map +1 -0
- package/dist/cli/commands/fields.d.ts +8 -0
- package/dist/cli/commands/fields.d.ts.map +1 -0
- package/dist/cli/commands/fields.js +47 -0
- package/dist/cli/commands/fields.js.map +1 -0
- package/dist/cli/commands/fields.test.d.ts +2 -0
- package/dist/cli/commands/fields.test.d.ts.map +1 -0
- package/dist/cli/commands/fields.test.js +78 -0
- package/dist/cli/commands/fields.test.js.map +1 -0
- package/dist/cli/commands/issuetypes.d.ts +7 -0
- package/dist/cli/commands/issuetypes.d.ts.map +1 -0
- package/dist/cli/commands/issuetypes.js +23 -0
- package/dist/cli/commands/issuetypes.js.map +1 -0
- package/dist/cli/commands/issuetypes.test.d.ts +2 -0
- package/dist/cli/commands/issuetypes.test.d.ts.map +1 -0
- package/dist/cli/commands/issuetypes.test.js +62 -0
- package/dist/cli/commands/issuetypes.test.js.map +1 -0
- package/dist/cli/commands/link.d.ts +18 -0
- package/dist/cli/commands/link.d.ts.map +1 -0
- package/dist/cli/commands/link.js +51 -0
- package/dist/cli/commands/link.js.map +1 -0
- package/dist/cli/commands/link.test.d.ts +2 -0
- package/dist/cli/commands/link.test.d.ts.map +1 -0
- package/dist/cli/commands/link.test.js +96 -0
- package/dist/cli/commands/link.test.js.map +1 -0
- package/dist/cli/commands/linktypes.d.ts +6 -0
- package/dist/cli/commands/linktypes.d.ts.map +1 -0
- package/dist/cli/commands/linktypes.js +21 -0
- package/dist/cli/commands/linktypes.js.map +1 -0
- package/dist/cli/commands/linktypes.test.d.ts +2 -0
- package/dist/cli/commands/linktypes.test.d.ts.map +1 -0
- package/dist/cli/commands/linktypes.test.js +55 -0
- package/dist/cli/commands/linktypes.test.js.map +1 -0
- package/dist/cli/commands/me.d.ts +6 -0
- package/dist/cli/commands/me.d.ts.map +1 -0
- package/dist/cli/commands/me.js +17 -0
- package/dist/cli/commands/me.js.map +1 -0
- package/dist/cli/commands/me.test.d.ts +2 -0
- package/dist/cli/commands/me.test.d.ts.map +1 -0
- package/dist/cli/commands/me.test.js +58 -0
- package/dist/cli/commands/me.test.js.map +1 -0
- package/dist/cli/commands/projects.d.ts +9 -0
- package/dist/cli/commands/projects.d.ts.map +1 -0
- package/dist/cli/commands/projects.js +25 -0
- package/dist/cli/commands/projects.js.map +1 -0
- package/dist/cli/commands/projects.test.d.ts +2 -0
- package/dist/cli/commands/projects.test.d.ts.map +1 -0
- package/dist/cli/commands/projects.test.js +73 -0
- package/dist/cli/commands/projects.test.js.map +1 -0
- package/dist/cli/commands/search.d.ts +11 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +41 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/search.test.d.ts +2 -0
- package/dist/cli/commands/search.test.d.ts.map +1 -0
- package/dist/cli/commands/search.test.js +106 -0
- package/dist/cli/commands/search.test.js.map +1 -0
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +5 -13
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/sprints.d.ts +10 -0
- package/dist/cli/commands/sprints.d.ts.map +1 -0
- package/dist/cli/commands/sprints.js +28 -0
- package/dist/cli/commands/sprints.js.map +1 -0
- package/dist/cli/commands/sprints.test.d.ts +2 -0
- package/dist/cli/commands/sprints.test.d.ts.map +1 -0
- package/dist/cli/commands/sprints.test.js +68 -0
- package/dist/cli/commands/sprints.test.js.map +1 -0
- package/dist/cli/commands/transition.d.ts +8 -0
- package/dist/cli/commands/transition.d.ts.map +1 -0
- package/dist/cli/commands/transition.js +46 -0
- package/dist/cli/commands/transition.js.map +1 -0
- package/dist/cli/commands/upgrade.d.ts +16 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +83 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/commands/upgrade.test.d.ts +2 -0
- package/dist/cli/commands/upgrade.test.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.test.js +146 -0
- package/dist/cli/commands/upgrade.test.js.map +1 -0
- package/dist/cli/commands/watchers.d.ts +10 -0
- package/dist/cli/commands/watchers.d.ts.map +1 -0
- package/dist/cli/commands/watchers.js +43 -0
- package/dist/cli/commands/watchers.js.map +1 -0
- package/dist/cli/commands/watchers.test.d.ts +2 -0
- package/dist/cli/commands/watchers.test.d.ts.map +1 -0
- package/dist/cli/commands/watchers.test.js +90 -0
- package/dist/cli/commands/watchers.test.js.map +1 -0
- package/dist/cli/commands/worklog.d.ts +8 -0
- package/dist/cli/commands/worklog.d.ts.map +1 -0
- package/dist/cli/commands/worklog.js +100 -0
- package/dist/cli/commands/worklog.js.map +1 -0
- package/dist/cli/commands/worklog.test.d.ts +2 -0
- package/dist/cli/commands/worklog.test.d.ts.map +1 -0
- package/dist/cli/commands/worklog.test.js +195 -0
- package/dist/cli/commands/worklog.test.js.map +1 -0
- package/dist/cli/index.js +712 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/issueKey.d.ts.map +1 -1
- package/dist/cli/issueKey.js +6 -2
- package/dist/cli/issueKey.js.map +1 -1
- package/dist/cli/issueKey.test.js +35 -0
- package/dist/cli/issueKey.test.js.map +1 -1
- package/dist/cli/jsonOutput.d.ts +6 -0
- package/dist/cli/jsonOutput.d.ts.map +1 -0
- package/dist/cli/jsonOutput.js +7 -0
- package/dist/cli/jsonOutput.js.map +1 -0
- package/dist/cli/jsonOutput.test.d.ts +2 -0
- package/dist/cli/jsonOutput.test.d.ts.map +1 -0
- package/dist/cli/jsonOutput.test.js +44 -0
- package/dist/cli/jsonOutput.test.js.map +1 -0
- package/dist/cli/resolveOrg.d.ts +2 -0
- package/dist/cli/resolveOrg.d.ts.map +1 -0
- package/dist/cli/resolveOrg.js +15 -0
- package/dist/cli/resolveOrg.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/chunkMarkdown.d.ts +6 -0
- package/dist/lib/chunkMarkdown.d.ts.map +1 -0
- package/dist/lib/chunkMarkdown.js +73 -0
- package/dist/lib/chunkMarkdown.js.map +1 -0
- package/dist/lib/chunkMarkdown.test.d.ts +2 -0
- package/dist/lib/chunkMarkdown.test.d.ts.map +1 -0
- package/dist/lib/chunkMarkdown.test.js +171 -0
- package/dist/lib/chunkMarkdown.test.js.map +1 -0
- package/dist/lib/config.d.ts +6 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +25 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/config.test.js +67 -0
- package/dist/lib/config.test.js.map +1 -1
- package/dist/lib/configSchema.d.ts +44 -0
- package/dist/lib/configSchema.d.ts.map +1 -1
- package/dist/lib/configSchema.js +20 -0
- package/dist/lib/configSchema.js.map +1 -1
- package/dist/lib/customFieldWrite.d.ts +21 -0
- package/dist/lib/customFieldWrite.d.ts.map +1 -0
- package/dist/lib/customFieldWrite.js +83 -0
- package/dist/lib/customFieldWrite.js.map +1 -0
- package/dist/lib/customFieldWrite.test.d.ts +2 -0
- package/dist/lib/customFieldWrite.test.d.ts.map +1 -0
- package/dist/lib/customFieldWrite.test.js +87 -0
- package/dist/lib/customFieldWrite.test.js.map +1 -0
- package/dist/lib/exportFields.d.ts +30 -0
- package/dist/lib/exportFields.d.ts.map +1 -0
- package/dist/lib/exportFields.js +169 -0
- package/dist/lib/exportFields.js.map +1 -0
- package/dist/lib/exportFields.test.d.ts +2 -0
- package/dist/lib/exportFields.test.d.ts.map +1 -0
- package/dist/lib/exportFields.test.js +145 -0
- package/dist/lib/exportFields.test.js.map +1 -0
- package/dist/lib/exporter.d.ts +25 -3
- package/dist/lib/exporter.d.ts.map +1 -1
- package/dist/lib/exporter.js +195 -18
- package/dist/lib/exporter.js.map +1 -1
- package/dist/lib/exporter.test.d.ts +2 -0
- package/dist/lib/exporter.test.d.ts.map +1 -0
- package/dist/lib/exporter.test.js +227 -0
- package/dist/lib/exporter.test.js.map +1 -0
- package/dist/lib/jiraClient.d.ts +272 -1
- package/dist/lib/jiraClient.d.ts.map +1 -1
- package/dist/lib/jiraClient.discovery.test.d.ts +2 -0
- package/dist/lib/jiraClient.discovery.test.d.ts.map +1 -0
- package/dist/lib/jiraClient.discovery.test.js +209 -0
- package/dist/lib/jiraClient.discovery.test.js.map +1 -0
- package/dist/lib/jiraClient.js +656 -16
- package/dist/lib/jiraClient.js.map +1 -1
- package/dist/lib/jiraClient.test.d.ts +2 -0
- package/dist/lib/jiraClient.test.d.ts.map +1 -0
- package/dist/lib/jiraClient.test.js +383 -0
- package/dist/lib/jiraClient.test.js.map +1 -0
- package/dist/lib/jiraClient.write.test.d.ts +2 -0
- package/dist/lib/jiraClient.write.test.d.ts.map +1 -0
- package/dist/lib/jiraClient.write.test.js +358 -0
- package/dist/lib/jiraClient.write.test.js.map +1 -0
- package/dist/lib/markdownToWiki.d.ts +2 -0
- package/dist/lib/markdownToWiki.d.ts.map +1 -0
- package/dist/lib/markdownToWiki.js +110 -0
- package/dist/lib/markdownToWiki.js.map +1 -0
- package/dist/lib/markdownToWiki.test.d.ts +2 -0
- package/dist/lib/markdownToWiki.test.d.ts.map +1 -0
- package/dist/lib/markdownToWiki.test.js +122 -0
- package/dist/lib/markdownToWiki.test.js.map +1 -0
- package/dist/lib/platform.d.ts +2 -0
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +8 -0
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/runCommand.d.ts +1 -0
- package/dist/lib/runCommand.d.ts.map +1 -1
- package/dist/lib/runCommand.js +8 -1
- package/dist/lib/runCommand.js.map +1 -1
- package/dist/lib/worklog.d.ts +36 -0
- package/dist/lib/worklog.d.ts.map +1 -0
- package/dist/lib/worklog.js +188 -0
- package/dist/lib/worklog.js.map +1 -0
- package/dist/lib/worklog.test.d.ts +2 -0
- package/dist/lib/worklog.test.d.ts.map +1 -0
- package/dist/lib/worklog.test.js +118 -0
- package/dist/lib/worklog.test.js.map +1 -0
- package/package.json +3 -1
package/dist/lib/jiraClient.js
CHANGED
|
@@ -1,7 +1,72 @@
|
|
|
1
1
|
import { createWriteStream } from 'fs';
|
|
2
|
-
import { mkdir } from 'fs/promises';
|
|
3
|
-
import { dirname } from 'path';
|
|
2
|
+
import { mkdir, readFile, stat } from 'fs/promises';
|
|
3
|
+
import { basename, dirname } from 'path';
|
|
4
4
|
import { pipeline } from 'stream/promises';
|
|
5
|
+
import { markdownToWiki } from './markdownToWiki.js';
|
|
6
|
+
function extractActiveSprintName(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return undefined;
|
|
9
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
10
|
+
const items = arr
|
|
11
|
+
.map((entry) => {
|
|
12
|
+
if (entry && typeof entry === 'object')
|
|
13
|
+
return entry;
|
|
14
|
+
if (typeof entry === 'string') {
|
|
15
|
+
const nameMatch = entry.match(/name=([^,\]]+)/);
|
|
16
|
+
const stateMatch = entry.match(/state=([^,\]]+)/);
|
|
17
|
+
return {
|
|
18
|
+
name: nameMatch?.[1],
|
|
19
|
+
state: stateMatch?.[1],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
})
|
|
24
|
+
.filter((x) => Boolean(x));
|
|
25
|
+
const active = items.find((s) => s.state?.toLowerCase() === 'active');
|
|
26
|
+
return (active ?? items[items.length - 1])?.name;
|
|
27
|
+
}
|
|
28
|
+
function extractCustomFieldValue(raw, type) {
|
|
29
|
+
if (raw === null || raw === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
switch (type) {
|
|
32
|
+
case 'scalar':
|
|
33
|
+
return typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean'
|
|
34
|
+
? raw
|
|
35
|
+
: undefined;
|
|
36
|
+
case 'number':
|
|
37
|
+
return typeof raw === 'number' ? raw : undefined;
|
|
38
|
+
case 'select':
|
|
39
|
+
if (typeof raw === 'object' && raw !== null && 'value' in raw) {
|
|
40
|
+
return raw.value;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
case 'user':
|
|
44
|
+
if (typeof raw === 'object' && raw !== null && 'displayName' in raw) {
|
|
45
|
+
return raw.displayName;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
case 'sprint':
|
|
49
|
+
return extractActiveSprintName(raw);
|
|
50
|
+
case 'array':
|
|
51
|
+
if (!Array.isArray(raw))
|
|
52
|
+
return undefined;
|
|
53
|
+
return raw
|
|
54
|
+
.map((item) => {
|
|
55
|
+
if (typeof item === 'string')
|
|
56
|
+
return item;
|
|
57
|
+
if (item && typeof item === 'object' && 'value' in item) {
|
|
58
|
+
return item.value;
|
|
59
|
+
}
|
|
60
|
+
if (item && typeof item === 'object' && 'name' in item) {
|
|
61
|
+
return item.name;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
})
|
|
65
|
+
.filter((x) => x !== undefined);
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
5
70
|
const COMMON_EPIC_FIELDS = [
|
|
6
71
|
'customfield_10014',
|
|
7
72
|
'customfield_10008',
|
|
@@ -11,10 +76,50 @@ const COMMON_EPIC_FIELDS = [
|
|
|
11
76
|
export class JiraClient {
|
|
12
77
|
config;
|
|
13
78
|
authHeader;
|
|
79
|
+
fieldsCache;
|
|
80
|
+
sprintFieldIdCache;
|
|
81
|
+
storyPointsFieldIdCache;
|
|
14
82
|
constructor(config, apiToken) {
|
|
15
83
|
this.config = config;
|
|
16
84
|
this.authHeader = `Basic ${Buffer.from(`${config.userEmail}:${apiToken}`).toString('base64')}`;
|
|
17
85
|
}
|
|
86
|
+
async listFields() {
|
|
87
|
+
if (this.fieldsCache)
|
|
88
|
+
return this.fieldsCache;
|
|
89
|
+
const all = await this.makeRequest('/field');
|
|
90
|
+
this.fieldsCache = all;
|
|
91
|
+
return all;
|
|
92
|
+
}
|
|
93
|
+
async detectSprintFieldId() {
|
|
94
|
+
if (this.sprintFieldIdCache !== undefined)
|
|
95
|
+
return this.sprintFieldIdCache ?? undefined;
|
|
96
|
+
try {
|
|
97
|
+
const fields = await this.listFields();
|
|
98
|
+
const match = fields.find((f) => f.schema?.custom === 'com.pyxis.greenhopper.jira:gh-sprint' ||
|
|
99
|
+
f.name?.toLowerCase() === 'sprint');
|
|
100
|
+
this.sprintFieldIdCache = match?.id ?? null;
|
|
101
|
+
return match?.id;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.sprintFieldIdCache = null;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async detectStoryPointsFieldId() {
|
|
109
|
+
if (this.storyPointsFieldIdCache !== undefined)
|
|
110
|
+
return this.storyPointsFieldIdCache ?? undefined;
|
|
111
|
+
try {
|
|
112
|
+
const fields = await this.listFields();
|
|
113
|
+
const match = fields.find((f) => f.schema?.custom === 'com.atlassian.jira.plugin.system.customfieldtypes:float' &&
|
|
114
|
+
/story\s*points?/i.test(f.name ?? '')) ?? fields.find((f) => /^story\s*points?$/i.test(f.name ?? ''));
|
|
115
|
+
this.storyPointsFieldIdCache = match?.id ?? null;
|
|
116
|
+
return match?.id;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
this.storyPointsFieldIdCache = null;
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
18
123
|
extractEpicFromFields(fields) {
|
|
19
124
|
for (const fieldId of COMMON_EPIC_FIELDS) {
|
|
20
125
|
const epicField = fields[fieldId];
|
|
@@ -213,8 +318,8 @@ export class JiraClient {
|
|
|
213
318
|
async getCurrentUser() {
|
|
214
319
|
return this.makeRequest('/myself');
|
|
215
320
|
}
|
|
216
|
-
async fetchIssueDetails(issueKey) {
|
|
217
|
-
const
|
|
321
|
+
async fetchIssueDetails(issueKey, options = {}) {
|
|
322
|
+
const baseFields = [
|
|
218
323
|
'summary',
|
|
219
324
|
'description',
|
|
220
325
|
'status',
|
|
@@ -222,7 +327,16 @@ export class JiraClient {
|
|
|
222
327
|
'attachment',
|
|
223
328
|
'issuetype',
|
|
224
329
|
...COMMON_EPIC_FIELDS,
|
|
225
|
-
]
|
|
330
|
+
];
|
|
331
|
+
const sprintFieldId = options.customFieldDefs?.sprint?.id ?? (await this.detectSprintFieldId());
|
|
332
|
+
const storyPointsFieldId = options.customFieldDefs?.storyPoints?.id ?? (await this.detectStoryPointsFieldId());
|
|
333
|
+
const extraFields = options.jiraFieldIds ?? [];
|
|
334
|
+
const fieldSet = new Set([...baseFields, ...extraFields]);
|
|
335
|
+
if (sprintFieldId)
|
|
336
|
+
fieldSet.add(sprintFieldId);
|
|
337
|
+
if (storyPointsFieldId)
|
|
338
|
+
fieldSet.add(storyPointsFieldId);
|
|
339
|
+
const fields = [...fieldSet].join(',');
|
|
226
340
|
const response = await this.makeRequest(`/issue/${issueKey}?fields=${fields}`);
|
|
227
341
|
const comments = await this.fetchIssueComments(issueKey);
|
|
228
342
|
const changelog = await this.fetchIssueChangelog(issueKey);
|
|
@@ -232,21 +346,29 @@ export class JiraClient {
|
|
|
232
346
|
url: att.content,
|
|
233
347
|
})) || [];
|
|
234
348
|
const history = this.mergeHistory(comments, changelog, attachmentMetadata);
|
|
235
|
-
return {
|
|
349
|
+
return this.buildTaskData(response, attachmentMetadata, history, {
|
|
350
|
+
sprintFieldId,
|
|
351
|
+
storyPointsFieldId,
|
|
352
|
+
customFieldDefs: options.customFieldDefs ?? {},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
buildTaskData(response, attachmentMetadata, history, ctx) {
|
|
356
|
+
const f = response.fields;
|
|
357
|
+
const data = {
|
|
236
358
|
key: response.key,
|
|
237
|
-
title:
|
|
238
|
-
status:
|
|
239
|
-
description: this.convertADFToMarkdown(
|
|
240
|
-
issueType:
|
|
241
|
-
parent:
|
|
359
|
+
title: f.summary,
|
|
360
|
+
status: f.status?.name || 'Unknown',
|
|
361
|
+
description: this.convertADFToMarkdown(f.description, attachmentMetadata),
|
|
362
|
+
issueType: f.issuetype?.name,
|
|
363
|
+
parent: f.parent
|
|
242
364
|
? {
|
|
243
|
-
key:
|
|
244
|
-
title:
|
|
245
|
-
status:
|
|
365
|
+
key: f.parent.key,
|
|
366
|
+
title: f.parent.fields.summary,
|
|
367
|
+
status: f.parent.fields.status?.name,
|
|
246
368
|
}
|
|
247
369
|
: undefined,
|
|
248
|
-
epic: this.extractEpicFromFields(
|
|
249
|
-
attachments:
|
|
370
|
+
epic: this.extractEpicFromFields(f),
|
|
371
|
+
attachments: f.attachment?.map((att) => ({
|
|
250
372
|
id: att.id,
|
|
251
373
|
filename: att.filename,
|
|
252
374
|
url: att.content,
|
|
@@ -254,6 +376,100 @@ export class JiraClient {
|
|
|
254
376
|
})) || [],
|
|
255
377
|
history,
|
|
256
378
|
};
|
|
379
|
+
const priority = f.priority;
|
|
380
|
+
if (priority?.name)
|
|
381
|
+
data.priority = priority.name;
|
|
382
|
+
const resolution = f.resolution;
|
|
383
|
+
if (resolution?.name)
|
|
384
|
+
data.resolution = resolution.name;
|
|
385
|
+
const assignee = f.assignee;
|
|
386
|
+
if (assignee?.displayName)
|
|
387
|
+
data.assignee = assignee.displayName;
|
|
388
|
+
const reporter = f.reporter;
|
|
389
|
+
if (reporter?.displayName)
|
|
390
|
+
data.reporter = reporter.displayName;
|
|
391
|
+
const creator = f.creator;
|
|
392
|
+
if (creator?.displayName)
|
|
393
|
+
data.creator = creator.displayName;
|
|
394
|
+
if (typeof f.created === 'string')
|
|
395
|
+
data.createdAt = f.created;
|
|
396
|
+
if (typeof f.updated === 'string')
|
|
397
|
+
data.updatedAt = f.updated;
|
|
398
|
+
if (typeof f.duedate === 'string' && f.duedate)
|
|
399
|
+
data.dueDate = f.duedate;
|
|
400
|
+
if (typeof f.resolutiondate === 'string' && f.resolutiondate) {
|
|
401
|
+
data.resolutionDate = f.resolutiondate;
|
|
402
|
+
}
|
|
403
|
+
const components = f.components;
|
|
404
|
+
if (components?.length)
|
|
405
|
+
data.components = components.map((c) => c.name);
|
|
406
|
+
const labels = f.labels;
|
|
407
|
+
if (labels?.length)
|
|
408
|
+
data.labels = labels;
|
|
409
|
+
const fixVersions = f.fixVersions;
|
|
410
|
+
if (fixVersions?.length)
|
|
411
|
+
data.fixVersions = fixVersions.map((v) => v.name);
|
|
412
|
+
const versions = f.versions;
|
|
413
|
+
if (versions?.length)
|
|
414
|
+
data.versions = versions.map((v) => v.name);
|
|
415
|
+
if (ctx.sprintFieldId) {
|
|
416
|
+
const sprintField = f[ctx.sprintFieldId];
|
|
417
|
+
const sprintName = extractActiveSprintName(sprintField);
|
|
418
|
+
if (sprintName)
|
|
419
|
+
data.sprint = sprintName;
|
|
420
|
+
}
|
|
421
|
+
if (ctx.storyPointsFieldId) {
|
|
422
|
+
const sp = f[ctx.storyPointsFieldId];
|
|
423
|
+
if (typeof sp === 'number')
|
|
424
|
+
data.storyPoints = sp;
|
|
425
|
+
}
|
|
426
|
+
const tt = f.timetracking;
|
|
427
|
+
if (tt && (tt.originalEstimate || tt.remainingEstimate || tt.timeSpent)) {
|
|
428
|
+
data.timetracking = {
|
|
429
|
+
originalEstimate: tt.originalEstimate,
|
|
430
|
+
remainingEstimate: tt.remainingEstimate,
|
|
431
|
+
timeSpent: tt.timeSpent,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const issueLinks = f.issuelinks;
|
|
435
|
+
if (issueLinks?.length) {
|
|
436
|
+
const mapped = [];
|
|
437
|
+
for (const link of issueLinks) {
|
|
438
|
+
if (link.inwardIssue) {
|
|
439
|
+
mapped.push({
|
|
440
|
+
type: link.type.inward ?? link.type.name ?? 'relates to',
|
|
441
|
+
key: link.inwardIssue.key,
|
|
442
|
+
title: link.inwardIssue.fields.summary,
|
|
443
|
+
status: link.inwardIssue.fields.status?.name,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else if (link.outwardIssue) {
|
|
447
|
+
mapped.push({
|
|
448
|
+
type: link.type.outward ?? link.type.name ?? 'relates to',
|
|
449
|
+
key: link.outwardIssue.key,
|
|
450
|
+
title: link.outwardIssue.fields.summary,
|
|
451
|
+
status: link.outwardIssue.fields.status?.name,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (mapped.length)
|
|
456
|
+
data.issueLinks = mapped;
|
|
457
|
+
}
|
|
458
|
+
const customFieldDefs = ctx.customFieldDefs;
|
|
459
|
+
if (Object.keys(customFieldDefs).length > 0) {
|
|
460
|
+
const cf = {};
|
|
461
|
+
for (const [friendly, def] of Object.entries(customFieldDefs)) {
|
|
462
|
+
if (friendly === 'sprint' || friendly === 'storyPoints')
|
|
463
|
+
continue;
|
|
464
|
+
const raw = f[def.id];
|
|
465
|
+
const value = extractCustomFieldValue(raw, def.type);
|
|
466
|
+
if (value !== undefined && value !== null && value !== '')
|
|
467
|
+
cf[friendly] = value;
|
|
468
|
+
}
|
|
469
|
+
if (Object.keys(cf).length > 0)
|
|
470
|
+
data.customFields = cf;
|
|
471
|
+
}
|
|
472
|
+
return data;
|
|
257
473
|
}
|
|
258
474
|
async fetchIssueComments(issueKey) {
|
|
259
475
|
const allComments = [];
|
|
@@ -291,6 +507,430 @@ export class JiraClient {
|
|
|
291
507
|
return [];
|
|
292
508
|
}
|
|
293
509
|
}
|
|
510
|
+
async addComment(issueKey, wikiBody, parentCommentId) {
|
|
511
|
+
const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/comment`;
|
|
512
|
+
const payload = { body: wikiBody };
|
|
513
|
+
if (parentCommentId)
|
|
514
|
+
payload.parentId = parentCommentId;
|
|
515
|
+
const response = await fetch(url, {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers: {
|
|
518
|
+
Authorization: this.authHeader,
|
|
519
|
+
Accept: 'application/json',
|
|
520
|
+
'Content-Type': 'application/json',
|
|
521
|
+
},
|
|
522
|
+
body: JSON.stringify(payload),
|
|
523
|
+
});
|
|
524
|
+
if (!response.ok) {
|
|
525
|
+
const errorText = await response.text();
|
|
526
|
+
throw new Error(`Jira addComment failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
527
|
+
}
|
|
528
|
+
const json = (await response.json());
|
|
529
|
+
return { id: json.id };
|
|
530
|
+
}
|
|
531
|
+
async addWorklog(issueKey, payload) {
|
|
532
|
+
const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/worklog`;
|
|
533
|
+
const response = await fetch(url, {
|
|
534
|
+
method: 'POST',
|
|
535
|
+
headers: {
|
|
536
|
+
Authorization: this.authHeader,
|
|
537
|
+
Accept: 'application/json',
|
|
538
|
+
'Content-Type': 'application/json',
|
|
539
|
+
},
|
|
540
|
+
body: JSON.stringify(payload),
|
|
541
|
+
});
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
const errorText = await response.text();
|
|
544
|
+
throw new Error(`Jira addWorklog failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
545
|
+
}
|
|
546
|
+
const json = (await response.json());
|
|
547
|
+
return { id: json.id, issueId: json.issueId };
|
|
548
|
+
}
|
|
549
|
+
async deleteComment(issueKey, commentId) {
|
|
550
|
+
const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}/comment/${commentId}`;
|
|
551
|
+
const response = await fetch(url, {
|
|
552
|
+
method: 'DELETE',
|
|
553
|
+
headers: { Authorization: this.authHeader, Accept: 'application/json' },
|
|
554
|
+
});
|
|
555
|
+
if (!response.ok && response.status !== 204) {
|
|
556
|
+
const errorText = await response.text();
|
|
557
|
+
throw new Error(`Jira deleteComment failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async makeAgileRequest(endpoint, options = {}) {
|
|
561
|
+
const url = `${this.config.baseUrl}/rest/agile/1.0${endpoint}`;
|
|
562
|
+
const response = await fetch(url, {
|
|
563
|
+
...options,
|
|
564
|
+
headers: {
|
|
565
|
+
Authorization: this.authHeader,
|
|
566
|
+
Accept: 'application/json',
|
|
567
|
+
'Content-Type': 'application/json',
|
|
568
|
+
...options.headers,
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
const errorText = await response.text();
|
|
573
|
+
throw new Error(`Jira Agile API request failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
574
|
+
}
|
|
575
|
+
return response.json();
|
|
576
|
+
}
|
|
577
|
+
async findBoardByName(name) {
|
|
578
|
+
const response = await this.makeAgileRequest(`/board?name=${encodeURIComponent(name)}`);
|
|
579
|
+
const exact = response.values.find((b) => b.name === name);
|
|
580
|
+
if (exact)
|
|
581
|
+
return exact;
|
|
582
|
+
if (response.values.length === 1)
|
|
583
|
+
return response.values[0];
|
|
584
|
+
if (response.values.length === 0) {
|
|
585
|
+
throw new Error(`No board found matching "${name}".`);
|
|
586
|
+
}
|
|
587
|
+
throw new Error(`Multiple boards matched "${name}": ${response.values.map((b) => b.name).join(', ')}. Use the exact name.`);
|
|
588
|
+
}
|
|
589
|
+
async getBoardConfiguration(boardId) {
|
|
590
|
+
return this.makeAgileRequest(`/board/${boardId}/configuration`);
|
|
591
|
+
}
|
|
592
|
+
async getBoardColumnStatusIds(boardName, columnName) {
|
|
593
|
+
const board = await this.findBoardByName(boardName);
|
|
594
|
+
const config = await this.getBoardConfiguration(board.id);
|
|
595
|
+
const column = config.columnConfig.columns.find((c) => c.name === columnName);
|
|
596
|
+
if (!column) {
|
|
597
|
+
const available = config.columnConfig.columns.map((c) => c.name).join(', ');
|
|
598
|
+
throw new Error(`Column "${columnName}" not found on board "${boardName}". Available: ${available}`);
|
|
599
|
+
}
|
|
600
|
+
return column.statuses.map((s) => s.id);
|
|
601
|
+
}
|
|
602
|
+
async searchByJql(jql, fields = ['summary', 'status', 'assignee', 'issuetype']) {
|
|
603
|
+
const all = [];
|
|
604
|
+
let nextPageToken;
|
|
605
|
+
do {
|
|
606
|
+
const body = { jql, fields, maxResults: 100 };
|
|
607
|
+
if (nextPageToken)
|
|
608
|
+
body.nextPageToken = nextPageToken;
|
|
609
|
+
const response = await this.makeRequest('/search/jql', { method: 'POST', body: JSON.stringify(body) });
|
|
610
|
+
all.push(...response.issues);
|
|
611
|
+
nextPageToken = response.isLast === false ? response.nextPageToken : undefined;
|
|
612
|
+
} while (nextPageToken);
|
|
613
|
+
return all;
|
|
614
|
+
}
|
|
615
|
+
async searchIssues(jql, opts = {}) {
|
|
616
|
+
const body = {
|
|
617
|
+
jql,
|
|
618
|
+
fields: opts.fields ?? ['summary', 'status', 'assignee', 'issuetype'],
|
|
619
|
+
maxResults: opts.limit ?? 50,
|
|
620
|
+
};
|
|
621
|
+
if (opts.nextPageToken)
|
|
622
|
+
body.nextPageToken = opts.nextPageToken;
|
|
623
|
+
const response = await this.makeRequest('/search/jql', { method: 'POST', body: JSON.stringify(body) });
|
|
624
|
+
return {
|
|
625
|
+
issues: response.issues,
|
|
626
|
+
nextPageToken: response.nextPageToken,
|
|
627
|
+
isLast: response.isLast !== false,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
async listProjects(opts = {}) {
|
|
631
|
+
const params = new URLSearchParams();
|
|
632
|
+
if (opts.query)
|
|
633
|
+
params.set('query', opts.query);
|
|
634
|
+
if (opts.startAt !== undefined)
|
|
635
|
+
params.set('startAt', String(opts.startAt));
|
|
636
|
+
if (opts.limit !== undefined)
|
|
637
|
+
params.set('maxResults', String(opts.limit));
|
|
638
|
+
const qs = params.toString();
|
|
639
|
+
return this.makeRequest(`/project/search${qs ? `?${qs}` : ''}`);
|
|
640
|
+
}
|
|
641
|
+
async listBoards(opts = {}) {
|
|
642
|
+
const params = new URLSearchParams();
|
|
643
|
+
if (opts.projectKey)
|
|
644
|
+
params.set('projectKeyOrId', opts.projectKey);
|
|
645
|
+
if (opts.type)
|
|
646
|
+
params.set('type', opts.type);
|
|
647
|
+
if (opts.name)
|
|
648
|
+
params.set('name', opts.name);
|
|
649
|
+
if (opts.startAt !== undefined)
|
|
650
|
+
params.set('startAt', String(opts.startAt));
|
|
651
|
+
if (opts.limit !== undefined)
|
|
652
|
+
params.set('maxResults', String(opts.limit));
|
|
653
|
+
const qs = params.toString();
|
|
654
|
+
return this.makeAgileRequest(`/board${qs ? `?${qs}` : ''}`);
|
|
655
|
+
}
|
|
656
|
+
async listSprints(boardId, opts = {}) {
|
|
657
|
+
const params = new URLSearchParams();
|
|
658
|
+
if (opts.state)
|
|
659
|
+
params.set('state', opts.state);
|
|
660
|
+
if (opts.startAt !== undefined)
|
|
661
|
+
params.set('startAt', String(opts.startAt));
|
|
662
|
+
if (opts.limit !== undefined)
|
|
663
|
+
params.set('maxResults', String(opts.limit));
|
|
664
|
+
const qs = params.toString();
|
|
665
|
+
return this.makeAgileRequest(`/board/${boardId}/sprint${qs ? `?${qs}` : ''}`);
|
|
666
|
+
}
|
|
667
|
+
async listIssueTypes(projectKey) {
|
|
668
|
+
if (!projectKey)
|
|
669
|
+
return this.makeRequest('/issuetype');
|
|
670
|
+
const project = await this.makeRequest(`/project/${projectKey}`);
|
|
671
|
+
return this.makeRequest(`/issuetype/project?projectId=${project.id}`);
|
|
672
|
+
}
|
|
673
|
+
async listComponents(projectKey) {
|
|
674
|
+
return this.makeRequest(`/project/${projectKey}/components`);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Fetch the fields available on the create screen for a given issue type, including
|
|
678
|
+
* allowedValues for select fields — helps users discover valid custom-field option strings.
|
|
679
|
+
*/
|
|
680
|
+
async getCreateFields(projectKey, issueTypeName) {
|
|
681
|
+
const types = await this.listIssueTypes(projectKey);
|
|
682
|
+
const match = types.find((t) => t.name.toLowerCase() === issueTypeName.toLowerCase());
|
|
683
|
+
if (!match) {
|
|
684
|
+
throw new Error(`Issue type "${issueTypeName}" not found in project ${projectKey}. ` +
|
|
685
|
+
`Available: ${types.map((t) => t.name).join(', ')}.`);
|
|
686
|
+
}
|
|
687
|
+
const response = await this.makeRequest(`/issue/createmeta/${projectKey}/issuetypes/${match.id}`);
|
|
688
|
+
return response.fields.map((f) => ({
|
|
689
|
+
fieldId: f.fieldId,
|
|
690
|
+
name: f.name,
|
|
691
|
+
required: f.required,
|
|
692
|
+
schemaType: f.schema?.type,
|
|
693
|
+
allowedValues: f.allowedValues
|
|
694
|
+
?.map((v) => v.value ?? v.name)
|
|
695
|
+
.filter((v) => Boolean(v)),
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
async listLinkTypes() {
|
|
699
|
+
const response = await this.makeRequest('/issueLinkType');
|
|
700
|
+
return response.issueLinkTypes;
|
|
701
|
+
}
|
|
702
|
+
async listPriorities() {
|
|
703
|
+
return this.makeRequest('/priority');
|
|
704
|
+
}
|
|
705
|
+
async listStatuses(projectKey) {
|
|
706
|
+
if (!projectKey)
|
|
707
|
+
return this.makeRequest('/status');
|
|
708
|
+
const statuses = await this.makeRequest(`/project/${projectKey}/statuses`);
|
|
709
|
+
const seen = new Map();
|
|
710
|
+
for (const entry of statuses) {
|
|
711
|
+
for (const s of entry.statuses)
|
|
712
|
+
seen.set(s.id, s);
|
|
713
|
+
}
|
|
714
|
+
return [...seen.values()];
|
|
715
|
+
}
|
|
716
|
+
async getIssueTransitions(issueKey) {
|
|
717
|
+
const response = await this.makeRequest(`/issue/${issueKey}/transitions`);
|
|
718
|
+
return response.transitions;
|
|
719
|
+
}
|
|
720
|
+
async transitionIssue(issueKey, targetStatus) {
|
|
721
|
+
const transitions = await this.getIssueTransitions(issueKey);
|
|
722
|
+
const match = transitions.find((t) => t.to.name === targetStatus) ??
|
|
723
|
+
transitions.find((t) => t.name === targetStatus);
|
|
724
|
+
if (!match) {
|
|
725
|
+
const available = transitions.map((t) => `"${t.name}" → "${t.to.name}"`).join(', ');
|
|
726
|
+
throw new Error(`No transition to "${targetStatus}" available on ${issueKey}. Available: ${available}`);
|
|
727
|
+
}
|
|
728
|
+
const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/transitions`;
|
|
729
|
+
const response = await fetch(url, {
|
|
730
|
+
method: 'POST',
|
|
731
|
+
headers: {
|
|
732
|
+
Authorization: this.authHeader,
|
|
733
|
+
Accept: 'application/json',
|
|
734
|
+
'Content-Type': 'application/json',
|
|
735
|
+
},
|
|
736
|
+
body: JSON.stringify({ transition: { id: match.id } }),
|
|
737
|
+
});
|
|
738
|
+
if (!response.ok && response.status !== 204) {
|
|
739
|
+
const errorText = await response.text();
|
|
740
|
+
throw new Error(`Jira transition failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
741
|
+
}
|
|
742
|
+
return { name: match.name, id: match.id };
|
|
743
|
+
}
|
|
744
|
+
async createIssue(input) {
|
|
745
|
+
const fields = {
|
|
746
|
+
project: { key: input.projectKey },
|
|
747
|
+
issuetype: { name: input.issueType },
|
|
748
|
+
summary: input.summary,
|
|
749
|
+
};
|
|
750
|
+
if (input.descriptionMarkdown !== undefined) {
|
|
751
|
+
fields.description = markdownToWiki(input.descriptionMarkdown);
|
|
752
|
+
}
|
|
753
|
+
if (input.assigneeAccountId)
|
|
754
|
+
fields.assignee = { accountId: input.assigneeAccountId };
|
|
755
|
+
if (input.labels)
|
|
756
|
+
fields.labels = input.labels;
|
|
757
|
+
if (input.priority)
|
|
758
|
+
fields.priority = { name: input.priority };
|
|
759
|
+
if (input.parentKey)
|
|
760
|
+
fields.parent = { key: input.parentKey };
|
|
761
|
+
if (input.components)
|
|
762
|
+
fields.components = input.components.map((name) => ({ name }));
|
|
763
|
+
if (input.customFields)
|
|
764
|
+
Object.assign(fields, input.customFields);
|
|
765
|
+
const url = `${this.config.baseUrl}/rest/api/2/issue`;
|
|
766
|
+
const response = await fetch(url, {
|
|
767
|
+
method: 'POST',
|
|
768
|
+
headers: {
|
|
769
|
+
Authorization: this.authHeader,
|
|
770
|
+
Accept: 'application/json',
|
|
771
|
+
'Content-Type': 'application/json',
|
|
772
|
+
},
|
|
773
|
+
body: JSON.stringify({ fields }),
|
|
774
|
+
});
|
|
775
|
+
if (!response.ok) {
|
|
776
|
+
const errorText = await response.text();
|
|
777
|
+
throw new Error(`Jira createIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
778
|
+
}
|
|
779
|
+
return response.json();
|
|
780
|
+
}
|
|
781
|
+
async editIssue(issueKey, input) {
|
|
782
|
+
const fields = {};
|
|
783
|
+
if (input.summary !== undefined)
|
|
784
|
+
fields.summary = input.summary;
|
|
785
|
+
if (input.descriptionMarkdown !== undefined) {
|
|
786
|
+
fields.description = markdownToWiki(input.descriptionMarkdown);
|
|
787
|
+
}
|
|
788
|
+
if (input.assigneeAccountId !== undefined) {
|
|
789
|
+
fields.assignee = input.assigneeAccountId === null
|
|
790
|
+
? null
|
|
791
|
+
: { accountId: input.assigneeAccountId };
|
|
792
|
+
}
|
|
793
|
+
if (input.labels !== undefined)
|
|
794
|
+
fields.labels = input.labels;
|
|
795
|
+
if (input.priority !== undefined)
|
|
796
|
+
fields.priority = { name: input.priority };
|
|
797
|
+
if (input.components !== undefined)
|
|
798
|
+
fields.components = input.components.map((name) => ({ name }));
|
|
799
|
+
if (input.customFields !== undefined)
|
|
800
|
+
Object.assign(fields, input.customFields);
|
|
801
|
+
if (Object.keys(fields).length === 0) {
|
|
802
|
+
throw new Error('editIssue called with no fields to update.');
|
|
803
|
+
}
|
|
804
|
+
const url = `${this.config.baseUrl}/rest/api/2/issue/${issueKey}`;
|
|
805
|
+
const response = await fetch(url, {
|
|
806
|
+
method: 'PUT',
|
|
807
|
+
headers: {
|
|
808
|
+
Authorization: this.authHeader,
|
|
809
|
+
Accept: 'application/json',
|
|
810
|
+
'Content-Type': 'application/json',
|
|
811
|
+
},
|
|
812
|
+
body: JSON.stringify({ fields }),
|
|
813
|
+
});
|
|
814
|
+
if (!response.ok && response.status !== 204) {
|
|
815
|
+
const errorText = await response.text();
|
|
816
|
+
throw new Error(`Jira editIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async assignIssue(issueKey, accountIdOrNull) {
|
|
820
|
+
const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/assignee`;
|
|
821
|
+
const response = await fetch(url, {
|
|
822
|
+
method: 'PUT',
|
|
823
|
+
headers: {
|
|
824
|
+
Authorization: this.authHeader,
|
|
825
|
+
Accept: 'application/json',
|
|
826
|
+
'Content-Type': 'application/json',
|
|
827
|
+
},
|
|
828
|
+
body: JSON.stringify({ accountId: accountIdOrNull }),
|
|
829
|
+
});
|
|
830
|
+
if (!response.ok && response.status !== 204) {
|
|
831
|
+
const errorText = await response.text();
|
|
832
|
+
throw new Error(`Jira assignIssue failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async linkIssues(inwardKey, outwardKey, linkTypeName, comment) {
|
|
836
|
+
const payload = {
|
|
837
|
+
type: { name: linkTypeName },
|
|
838
|
+
inwardIssue: { key: inwardKey },
|
|
839
|
+
outwardIssue: { key: outwardKey },
|
|
840
|
+
};
|
|
841
|
+
if (comment)
|
|
842
|
+
payload.comment = { body: markdownToWiki(comment) };
|
|
843
|
+
const url = `${this.config.baseUrl}/rest/api/3/issueLink`;
|
|
844
|
+
const response = await fetch(url, {
|
|
845
|
+
method: 'POST',
|
|
846
|
+
headers: {
|
|
847
|
+
Authorization: this.authHeader,
|
|
848
|
+
Accept: 'application/json',
|
|
849
|
+
'Content-Type': 'application/json',
|
|
850
|
+
},
|
|
851
|
+
body: JSON.stringify(payload),
|
|
852
|
+
});
|
|
853
|
+
if (!response.ok && response.status !== 201) {
|
|
854
|
+
const errorText = await response.text();
|
|
855
|
+
throw new Error(`Jira linkIssues failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async removeIssueLink(linkId) {
|
|
859
|
+
const url = `${this.config.baseUrl}/rest/api/3/issueLink/${linkId}`;
|
|
860
|
+
const response = await fetch(url, {
|
|
861
|
+
method: 'DELETE',
|
|
862
|
+
headers: { Authorization: this.authHeader, Accept: 'application/json' },
|
|
863
|
+
});
|
|
864
|
+
if (!response.ok && response.status !== 204) {
|
|
865
|
+
const errorText = await response.text();
|
|
866
|
+
throw new Error(`Jira removeIssueLink failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async listWatchers(issueKey) {
|
|
870
|
+
const response = await this.makeRequest(`/issue/${issueKey}/watchers`);
|
|
871
|
+
return response.watchers;
|
|
872
|
+
}
|
|
873
|
+
async addWatcher(issueKey, accountId) {
|
|
874
|
+
const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/watchers`;
|
|
875
|
+
// Jira quirk: body is a JSON string (account id in quotes), not an object
|
|
876
|
+
const response = await fetch(url, {
|
|
877
|
+
method: 'POST',
|
|
878
|
+
headers: {
|
|
879
|
+
Authorization: this.authHeader,
|
|
880
|
+
Accept: 'application/json',
|
|
881
|
+
'Content-Type': 'application/json',
|
|
882
|
+
},
|
|
883
|
+
body: JSON.stringify(accountId),
|
|
884
|
+
});
|
|
885
|
+
if (!response.ok && response.status !== 204) {
|
|
886
|
+
const errorText = await response.text();
|
|
887
|
+
throw new Error(`Jira addWatcher failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async removeWatcher(issueKey, accountId) {
|
|
891
|
+
const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/watchers?accountId=${encodeURIComponent(accountId)}`;
|
|
892
|
+
const response = await fetch(url, {
|
|
893
|
+
method: 'DELETE',
|
|
894
|
+
headers: { Authorization: this.authHeader, Accept: 'application/json' },
|
|
895
|
+
});
|
|
896
|
+
if (!response.ok && response.status !== 204) {
|
|
897
|
+
const errorText = await response.text();
|
|
898
|
+
throw new Error(`Jira removeWatcher failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async uploadAttachment(issueKey, filePath) {
|
|
902
|
+
await stat(filePath);
|
|
903
|
+
const buf = await readFile(filePath);
|
|
904
|
+
const form = new FormData();
|
|
905
|
+
const arr = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
906
|
+
form.append('file', new Blob([arr]), basename(filePath));
|
|
907
|
+
const url = `${this.config.baseUrl}/rest/api/3/issue/${issueKey}/attachments`;
|
|
908
|
+
const response = await fetch(url, {
|
|
909
|
+
method: 'POST',
|
|
910
|
+
headers: {
|
|
911
|
+
Authorization: this.authHeader,
|
|
912
|
+
Accept: 'application/json',
|
|
913
|
+
'X-Atlassian-Token': 'no-check',
|
|
914
|
+
},
|
|
915
|
+
body: form,
|
|
916
|
+
});
|
|
917
|
+
if (!response.ok) {
|
|
918
|
+
const errorText = await response.text();
|
|
919
|
+
throw new Error(`Jira uploadAttachment failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
920
|
+
}
|
|
921
|
+
return response.json();
|
|
922
|
+
}
|
|
923
|
+
async deleteAttachment(attachmentId) {
|
|
924
|
+
const url = `${this.config.baseUrl}/rest/api/3/attachment/${attachmentId}`;
|
|
925
|
+
const response = await fetch(url, {
|
|
926
|
+
method: 'DELETE',
|
|
927
|
+
headers: { Authorization: this.authHeader, Accept: 'application/json' },
|
|
928
|
+
});
|
|
929
|
+
if (!response.ok && response.status !== 204) {
|
|
930
|
+
const errorText = await response.text();
|
|
931
|
+
throw new Error(`Jira deleteAttachment failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
294
934
|
async downloadAttachment(attachmentUrl, outputPath) {
|
|
295
935
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
296
936
|
const response = await fetch(attachmentUrl, {
|