chief-clancy 0.5.12 → 0.7.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 +15 -8
- package/dist/bundle/clancy-afk.js +6 -2
- package/dist/bundle/clancy-once.js +71 -48
- package/dist/installer/hook-installer/hook-installer.d.ts +2 -0
- package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -1
- package/dist/installer/hook-installer/hook-installer.js +36 -1
- package/dist/installer/hook-installer/hook-installer.js.map +1 -1
- package/dist/installer/install.js +16 -1
- package/dist/installer/install.js.map +1 -1
- package/dist/schemas/env.d.ts +36 -0
- package/dist/schemas/env.d.ts.map +1 -1
- package/dist/schemas/env.js +11 -0
- package/dist/schemas/env.js.map +1 -1
- package/dist/schemas/github-issues.d.ts +6 -0
- package/dist/schemas/github-issues.d.ts.map +1 -1
- package/dist/schemas/github-issues.js +3 -0
- package/dist/schemas/github-issues.js.map +1 -1
- package/dist/schemas/jira.d.ts +21 -0
- package/dist/schemas/jira.d.ts.map +1 -1
- package/dist/schemas/jira.js +18 -0
- package/dist/schemas/jira.js.map +1 -1
- package/dist/schemas/linear.d.ts +41 -0
- package/dist/schemas/linear.d.ts.map +1 -1
- package/dist/schemas/linear.js +34 -0
- package/dist/schemas/linear.js.map +1 -1
- package/dist/scripts/afk/afk.d.ts.map +1 -1
- package/dist/scripts/afk/afk.js +28 -0
- package/dist/scripts/afk/afk.js.map +1 -1
- package/dist/scripts/afk/report/report.d.ts +47 -0
- package/dist/scripts/afk/report/report.d.ts.map +1 -0
- package/dist/scripts/afk/report/report.js +194 -0
- package/dist/scripts/afk/report/report.js.map +1 -0
- package/dist/scripts/board/github/github.d.ts +33 -3
- package/dist/scripts/board/github/github.d.ts.map +1 -1
- package/dist/scripts/board/github/github.js +112 -27
- package/dist/scripts/board/github/github.js.map +1 -1
- package/dist/scripts/board/jira/jira.d.ts +36 -4
- package/dist/scripts/board/jira/jira.d.ts.map +1 -1
- package/dist/scripts/board/jira/jira.js +138 -65
- package/dist/scripts/board/jira/jira.js.map +1 -1
- package/dist/scripts/board/linear/linear.d.ts +32 -5
- package/dist/scripts/board/linear/linear.d.ts.map +1 -1
- package/dist/scripts/board/linear/linear.js +135 -17
- package/dist/scripts/board/linear/linear.js.map +1 -1
- package/dist/scripts/once/board-ops/board-ops.d.ts.map +1 -1
- package/dist/scripts/once/board-ops/board-ops.js +1 -1
- package/dist/scripts/once/board-ops/board-ops.js.map +1 -1
- package/dist/scripts/once/cost/cost.d.ts +10 -0
- package/dist/scripts/once/cost/cost.d.ts.map +1 -0
- package/dist/scripts/once/cost/cost.js +23 -0
- package/dist/scripts/once/cost/cost.js.map +1 -0
- package/dist/scripts/once/deliver/deliver.d.ts.map +1 -1
- package/dist/scripts/once/deliver/deliver.js +18 -1
- package/dist/scripts/once/deliver/deliver.js.map +1 -1
- package/dist/scripts/once/fetch-ticket/fetch-ticket.d.ts +19 -1
- package/dist/scripts/once/fetch-ticket/fetch-ticket.d.ts.map +1 -1
- package/dist/scripts/once/fetch-ticket/fetch-ticket.js +93 -29
- package/dist/scripts/once/fetch-ticket/fetch-ticket.js.map +1 -1
- package/dist/scripts/once/lock/lock.d.ts +17 -0
- package/dist/scripts/once/lock/lock.d.ts.map +1 -0
- package/dist/scripts/once/lock/lock.js +70 -0
- package/dist/scripts/once/lock/lock.js.map +1 -0
- package/dist/scripts/once/once.d.ts.map +1 -1
- package/dist/scripts/once/once.js +100 -4
- package/dist/scripts/once/once.js.map +1 -1
- package/dist/scripts/once/resume/resume.d.ts +24 -0
- package/dist/scripts/once/resume/resume.d.ts.map +1 -0
- package/dist/scripts/once/resume/resume.js +159 -0
- package/dist/scripts/once/resume/resume.js.map +1 -0
- package/dist/scripts/shared/format/format.d.ts.map +1 -1
- package/dist/scripts/shared/format/format.js +6 -1
- package/dist/scripts/shared/format/format.js.map +1 -1
- package/dist/scripts/shared/progress/progress.d.ts +18 -2
- package/dist/scripts/shared/progress/progress.d.ts.map +1 -1
- package/dist/scripts/shared/progress/progress.js +31 -4
- package/dist/scripts/shared/progress/progress.js.map +1 -1
- package/dist/scripts/shared/pull-request/pr-body/pr-body.d.ts +2 -1
- package/dist/scripts/shared/pull-request/pr-body/pr-body.d.ts.map +1 -1
- package/dist/scripts/shared/pull-request/pr-body/pr-body.js +10 -1
- package/dist/scripts/shared/pull-request/pr-body/pr-body.js.map +1 -1
- package/dist/types/remote.d.ts +1 -1
- package/dist/types/remote.d.ts.map +1 -1
- package/hooks/clancy-branch-guard.js +129 -0
- package/hooks/clancy-check-update.js +43 -0
- package/hooks/clancy-context-monitor.js +134 -46
- package/hooks/clancy-post-compact.js +53 -0
- package/hooks/package.json +3 -0
- package/package.json +3 -2
- package/src/agents/devils-advocate.md +53 -0
- package/src/agents/verification-gate.md +128 -0
- package/src/roles/planner/workflows/approve-plan.md +2 -2
- package/src/roles/reviewer/workflows/logs.md +9 -6
- package/src/roles/setup/commands/help.md +7 -0
- package/src/roles/setup/workflows/init.md +111 -6
- package/src/roles/setup/workflows/scaffold.md +57 -0
- package/src/roles/setup/workflows/settings.md +145 -0
- package/src/roles/setup/workflows/update.md +18 -0
- package/src/roles/strategist/commands/approve-brief.md +20 -0
- package/src/roles/strategist/commands/brief.md +27 -0
- package/src/roles/strategist/workflows/approve-brief.md +763 -0
- package/src/roles/strategist/workflows/brief.md +732 -0
- package/src/templates/CLAUDE.md +8 -1
|
@@ -39,7 +39,7 @@ export declare function pingJira(baseUrl: string, projectKey: string, auth: stri
|
|
|
39
39
|
* @param label - If set, adds a label filter.
|
|
40
40
|
* @returns The JQL query string.
|
|
41
41
|
*/
|
|
42
|
-
export declare function buildJql(projectKey: string, status: string, sprint?: string, label?: string): string;
|
|
42
|
+
export declare function buildJql(projectKey: string, status: string, sprint?: string, label?: string, excludeHitl?: boolean): string;
|
|
43
43
|
/**
|
|
44
44
|
* Extract all text strings from a Jira ADF (Atlassian Document Format) description.
|
|
45
45
|
*
|
|
@@ -64,16 +64,48 @@ export declare function fetchTicket(baseUrl: string, auth: string, projectKey: s
|
|
|
64
64
|
epicKey?: string;
|
|
65
65
|
blockers: string[];
|
|
66
66
|
}) | undefined>;
|
|
67
|
+
/** Jira ticket with epic and blocker info. */
|
|
68
|
+
export type JiraTicket = Ticket & {
|
|
69
|
+
epicKey?: string;
|
|
70
|
+
blockers: string[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Fetch multiple candidate tickets from Jira.
|
|
74
|
+
*
|
|
75
|
+
* @param baseUrl - The Jira Cloud base URL.
|
|
76
|
+
* @param auth - The Base64-encoded Basic auth string.
|
|
77
|
+
* @param projectKey - The Jira project key.
|
|
78
|
+
* @param status - The JQL status to filter by.
|
|
79
|
+
* @param sprint - Optional sprint filter.
|
|
80
|
+
* @param label - Optional label filter.
|
|
81
|
+
* @param excludeHitl - If `true`, excludes tickets with the `clancy:hitl` label.
|
|
82
|
+
* @param limit - Maximum number of results to return (default: 5).
|
|
83
|
+
* @returns Array of fetched tickets (may be empty).
|
|
84
|
+
*/
|
|
85
|
+
export declare function fetchTickets(baseUrl: string, auth: string, projectKey: string, status: string, sprint?: string, label?: string, excludeHitl?: boolean, limit?: number): Promise<JiraTicket[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Check whether a Jira issue is blocked by unresolved blockers.
|
|
88
|
+
*
|
|
89
|
+
* Fetches the issue's links and checks for inward "Blocks" relationships
|
|
90
|
+
* where the blocking issue's statusCategory is not "done".
|
|
91
|
+
*
|
|
92
|
+
* @param baseUrl - The Jira Cloud base URL.
|
|
93
|
+
* @param auth - The Base64-encoded Basic auth string.
|
|
94
|
+
* @param key - The Jira issue key (e.g., `'PROJ-123'`).
|
|
95
|
+
* @returns `true` if any blocker is unresolved, `false` otherwise.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fetchBlockerStatus(baseUrl: string, auth: string, key: string): Promise<boolean>;
|
|
67
98
|
/** Result of checking children status for an epic. */
|
|
68
99
|
export type ChildrenStatus = {
|
|
69
100
|
total: number;
|
|
70
101
|
incomplete: number;
|
|
71
102
|
};
|
|
72
103
|
/**
|
|
73
|
-
* Fetch the children status of a Jira epic.
|
|
104
|
+
* Fetch the children status of a Jira epic (dual-mode).
|
|
74
105
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
106
|
+
* Tries the `Epic: {key}` text convention first (children with "Epic: PROJ-100"
|
|
107
|
+
* in their description). If no results, falls back to the native `parent = {key}`
|
|
108
|
+
* JQL query for backward compatibility with pre-v0.6.0 children.
|
|
77
109
|
*
|
|
78
110
|
* @param baseUrl - The Jira Cloud base URL.
|
|
79
111
|
* @param auth - The Base64-encoded Basic auth string.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jira.d.ts","sourceRoot":"","sources":["../../../../src/scripts/board/jira/jira.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jira.d.ts","sourceRoot":"","sources":["../../../../src/scripts/board/jira/jira.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK/C;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,CAWrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,OAAO,GACpB,MAAM,CAWR;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CA0BnD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CACN,CAAC,MAAM,GAAG;IACR,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC,GACF,SAAS,CACZ,CAYA;AAED,8CAA8C;AAC9C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,OAAO,EACrB,KAAK,SAAI,GACR,OAAO,CAAC,UAAU,EAAE,CAAC,CAyEvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,CAiClB;AAED,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAmBrC;AAuDD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAgClB"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Uses the new POST `/rest/api/3/search/jql` endpoint (old GET `/search`
|
|
8
8
|
* was removed by Atlassian in August 2025).
|
|
9
9
|
*/
|
|
10
|
-
import { jiraSearchResponseSchema, jiraTransitionsResponseSchema, } from '../../../schemas/jira.js';
|
|
10
|
+
import { jiraIssueLinksResponseSchema, jiraSearchResponseSchema, jiraTransitionsResponseSchema, } from '../../../schemas/jira.js';
|
|
11
11
|
import { jiraHeaders, pingEndpoint } from '../../../scripts/shared/http/http.js';
|
|
12
12
|
const SAFE_VALUE_PATTERN = /^[a-zA-Z0-9 _\-'.]+$/;
|
|
13
13
|
const ISSUE_KEY_PATTERN = /^[A-Z][A-Z0-9]+-\d+$/;
|
|
@@ -60,12 +60,14 @@ export async function pingJira(baseUrl, projectKey, auth) {
|
|
|
60
60
|
* @param label - If set, adds a label filter.
|
|
61
61
|
* @returns The JQL query string.
|
|
62
62
|
*/
|
|
63
|
-
export function buildJql(projectKey, status, sprint, label) {
|
|
63
|
+
export function buildJql(projectKey, status, sprint, label, excludeHitl) {
|
|
64
64
|
const parts = [`project="${projectKey}"`];
|
|
65
65
|
if (sprint)
|
|
66
66
|
parts.push('sprint in openSprints()');
|
|
67
67
|
if (label)
|
|
68
68
|
parts.push(`labels = "${label}"`);
|
|
69
|
+
if (excludeHitl)
|
|
70
|
+
parts.push('labels != "clancy:hitl"');
|
|
69
71
|
parts.push(`assignee=currentUser()`);
|
|
70
72
|
parts.push(`status="${status}"`);
|
|
71
73
|
return parts.join(' AND ') + ' ORDER BY priority ASC';
|
|
@@ -113,7 +115,24 @@ export function extractAdfText(adf) {
|
|
|
113
115
|
* @returns The fetched ticket, or `undefined` if no tickets are available.
|
|
114
116
|
*/
|
|
115
117
|
export async function fetchTicket(baseUrl, auth, projectKey, status, sprint, label) {
|
|
116
|
-
const
|
|
118
|
+
const results = await fetchTickets(baseUrl, auth, projectKey, status, sprint, label, false, 1);
|
|
119
|
+
return results[0];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fetch multiple candidate tickets from Jira.
|
|
123
|
+
*
|
|
124
|
+
* @param baseUrl - The Jira Cloud base URL.
|
|
125
|
+
* @param auth - The Base64-encoded Basic auth string.
|
|
126
|
+
* @param projectKey - The Jira project key.
|
|
127
|
+
* @param status - The JQL status to filter by.
|
|
128
|
+
* @param sprint - Optional sprint filter.
|
|
129
|
+
* @param label - Optional label filter.
|
|
130
|
+
* @param excludeHitl - If `true`, excludes tickets with the `clancy:hitl` label.
|
|
131
|
+
* @param limit - Maximum number of results to return (default: 5).
|
|
132
|
+
* @returns Array of fetched tickets (may be empty).
|
|
133
|
+
*/
|
|
134
|
+
export async function fetchTickets(baseUrl, auth, projectKey, status, sprint, label, excludeHitl, limit = 5) {
|
|
135
|
+
const jql = buildJql(projectKey, status, sprint, label, excludeHitl);
|
|
117
136
|
let response;
|
|
118
137
|
try {
|
|
119
138
|
response = await fetch(`${baseUrl}/rest/api/3/search/jql`, {
|
|
@@ -124,7 +143,7 @@ export async function fetchTicket(baseUrl, auth, projectKey, status, sprint, lab
|
|
|
124
143
|
},
|
|
125
144
|
body: JSON.stringify({
|
|
126
145
|
jql,
|
|
127
|
-
maxResults:
|
|
146
|
+
maxResults: limit,
|
|
128
147
|
fields: [
|
|
129
148
|
'summary',
|
|
130
149
|
'description',
|
|
@@ -137,11 +156,11 @@ export async function fetchTicket(baseUrl, auth, projectKey, status, sprint, lab
|
|
|
137
156
|
}
|
|
138
157
|
catch (err) {
|
|
139
158
|
console.warn(`⚠ Jira API request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
-
return
|
|
159
|
+
return [];
|
|
141
160
|
}
|
|
142
161
|
if (!response.ok) {
|
|
143
162
|
console.warn(`⚠ Jira API returned HTTP ${response.status}`);
|
|
144
|
-
return
|
|
163
|
+
return [];
|
|
145
164
|
}
|
|
146
165
|
let json;
|
|
147
166
|
try {
|
|
@@ -149,38 +168,78 @@ export async function fetchTicket(baseUrl, auth, projectKey, status, sprint, lab
|
|
|
149
168
|
}
|
|
150
169
|
catch {
|
|
151
170
|
console.warn('⚠ Jira API returned invalid JSON');
|
|
152
|
-
return
|
|
171
|
+
return [];
|
|
153
172
|
}
|
|
154
173
|
const parsed = jiraSearchResponseSchema.safeParse(json);
|
|
155
174
|
if (!parsed.success) {
|
|
156
175
|
console.warn(`⚠ Unexpected Jira response shape: ${parsed.error.message}`);
|
|
157
|
-
return
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
return parsed.data.issues.map((issue) => {
|
|
179
|
+
const fields = issue.fields;
|
|
180
|
+
// Extract blockers
|
|
181
|
+
const blockers = (fields.issuelinks ?? [])
|
|
182
|
+
.filter((link) => link.type?.name === 'Blocks' && link.inwardIssue?.key)
|
|
183
|
+
.map((link) => link.inwardIssue?.key)
|
|
184
|
+
.filter((key) => Boolean(key));
|
|
185
|
+
// Extract epic (next-gen parent OR classic customfield)
|
|
186
|
+
const epicKey = fields.parent?.key ?? fields.customfield_10014 ?? undefined;
|
|
187
|
+
return {
|
|
188
|
+
key: issue.key,
|
|
189
|
+
title: fields.summary,
|
|
190
|
+
description: extractAdfText(fields.description),
|
|
191
|
+
provider: 'jira',
|
|
192
|
+
epicKey,
|
|
193
|
+
blockers,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Check whether a Jira issue is blocked by unresolved blockers.
|
|
199
|
+
*
|
|
200
|
+
* Fetches the issue's links and checks for inward "Blocks" relationships
|
|
201
|
+
* where the blocking issue's statusCategory is not "done".
|
|
202
|
+
*
|
|
203
|
+
* @param baseUrl - The Jira Cloud base URL.
|
|
204
|
+
* @param auth - The Base64-encoded Basic auth string.
|
|
205
|
+
* @param key - The Jira issue key (e.g., `'PROJ-123'`).
|
|
206
|
+
* @returns `true` if any blocker is unresolved, `false` otherwise.
|
|
207
|
+
*/
|
|
208
|
+
export async function fetchBlockerStatus(baseUrl, auth, key) {
|
|
209
|
+
if (!ISSUE_KEY_PATTERN.test(key))
|
|
210
|
+
return false;
|
|
211
|
+
try {
|
|
212
|
+
const response = await fetch(`${baseUrl}/rest/api/3/issue/${key}?fields=issuelinks`, { headers: jiraHeaders(auth) });
|
|
213
|
+
if (!response.ok)
|
|
214
|
+
return false;
|
|
215
|
+
const json = await response.json();
|
|
216
|
+
const parsed = jiraIssueLinksResponseSchema.safeParse(json);
|
|
217
|
+
if (!parsed.success)
|
|
218
|
+
return false;
|
|
219
|
+
const links = parsed.data.fields?.issuelinks ?? [];
|
|
220
|
+
// Check for inward "Blocks" links with unresolved status
|
|
221
|
+
return links.some((link) => {
|
|
222
|
+
if (link.type?.name !== 'Blocks')
|
|
223
|
+
return false;
|
|
224
|
+
if (!link.inwardIssue?.key)
|
|
225
|
+
return false;
|
|
226
|
+
const categoryKey = link.inwardIssue.fields?.status?.statusCategory?.key;
|
|
227
|
+
// If status info is missing, assume not blocked
|
|
228
|
+
if (!categoryKey)
|
|
229
|
+
return false;
|
|
230
|
+
return categoryKey !== 'done';
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return false;
|
|
158
235
|
}
|
|
159
|
-
if (!parsed.data.issues.length)
|
|
160
|
-
return undefined;
|
|
161
|
-
const issue = parsed.data.issues[0];
|
|
162
|
-
const fields = issue.fields;
|
|
163
|
-
// Extract blockers
|
|
164
|
-
const blockers = (fields.issuelinks ?? [])
|
|
165
|
-
.filter((link) => link.type?.name === 'Blocks' && link.inwardIssue?.key)
|
|
166
|
-
.map((link) => link.inwardIssue?.key)
|
|
167
|
-
.filter((key) => Boolean(key));
|
|
168
|
-
// Extract epic (next-gen parent OR classic customfield)
|
|
169
|
-
const epicKey = fields.parent?.key ?? fields.customfield_10014 ?? undefined;
|
|
170
|
-
return {
|
|
171
|
-
key: issue.key,
|
|
172
|
-
title: fields.summary,
|
|
173
|
-
description: extractAdfText(fields.description),
|
|
174
|
-
provider: 'jira',
|
|
175
|
-
epicKey,
|
|
176
|
-
blockers,
|
|
177
|
-
};
|
|
178
236
|
}
|
|
179
237
|
/**
|
|
180
|
-
* Fetch the children status of a Jira epic.
|
|
238
|
+
* Fetch the children status of a Jira epic (dual-mode).
|
|
181
239
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
240
|
+
* Tries the `Epic: {key}` text convention first (children with "Epic: PROJ-100"
|
|
241
|
+
* in their description). If no results, falls back to the native `parent = {key}`
|
|
242
|
+
* JQL query for backward compatibility with pre-v0.6.0 children.
|
|
184
243
|
*
|
|
185
244
|
* @param baseUrl - The Jira Cloud base URL.
|
|
186
245
|
* @param auth - The Base64-encoded Basic auth string.
|
|
@@ -191,46 +250,60 @@ export async function fetchChildrenStatus(baseUrl, auth, parentKey) {
|
|
|
191
250
|
if (!ISSUE_KEY_PATTERN.test(parentKey))
|
|
192
251
|
return undefined;
|
|
193
252
|
try {
|
|
194
|
-
//
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
body: JSON.stringify({
|
|
202
|
-
jql: `parent = ${parentKey}`,
|
|
203
|
-
maxResults: 0,
|
|
204
|
-
}),
|
|
205
|
-
});
|
|
206
|
-
if (!totalResponse.ok)
|
|
207
|
-
return undefined;
|
|
208
|
-
const totalJson = (await totalResponse.json());
|
|
209
|
-
const total = totalJson.total ?? 0;
|
|
210
|
-
if (total === 0)
|
|
211
|
-
return { total: 0, incomplete: 0 };
|
|
212
|
-
// Fetch incomplete children
|
|
213
|
-
const incompleteResponse = await fetch(`${baseUrl}/rest/api/3/search/jql`, {
|
|
214
|
-
method: 'POST',
|
|
215
|
-
headers: {
|
|
216
|
-
...jiraHeaders(auth),
|
|
217
|
-
'Content-Type': 'application/json',
|
|
218
|
-
},
|
|
219
|
-
body: JSON.stringify({
|
|
220
|
-
jql: `parent = ${parentKey} AND statusCategory != "done"`,
|
|
221
|
-
maxResults: 0,
|
|
222
|
-
}),
|
|
223
|
-
});
|
|
224
|
-
if (!incompleteResponse.ok)
|
|
225
|
-
return undefined;
|
|
226
|
-
const incompleteJson = (await incompleteResponse.json());
|
|
227
|
-
const incomplete = incompleteJson.total ?? 0;
|
|
228
|
-
return { total, incomplete };
|
|
253
|
+
// Mode 1: Try Epic: text convention (scoped to project to avoid cross-project matches)
|
|
254
|
+
const projectPrefix = parentKey.split('-')[0];
|
|
255
|
+
const epicTextResult = await fetchChildrenByJql(baseUrl, auth, `project = "${projectPrefix}" AND description ~ "Epic: ${parentKey}"`);
|
|
256
|
+
if (epicTextResult && epicTextResult.total > 0)
|
|
257
|
+
return epicTextResult;
|
|
258
|
+
// Mode 2: Fall back to native parent API
|
|
259
|
+
return await fetchChildrenByJql(baseUrl, auth, `parent = ${parentKey}`);
|
|
229
260
|
}
|
|
230
261
|
catch {
|
|
231
262
|
return undefined;
|
|
232
263
|
}
|
|
233
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Fetch children status using a JQL query (total and incomplete counts).
|
|
267
|
+
*
|
|
268
|
+
* @param baseUrl - The Jira Cloud base URL.
|
|
269
|
+
* @param auth - The Base64-encoded Basic auth string.
|
|
270
|
+
* @param jql - The JQL query to find children.
|
|
271
|
+
* @returns The children status, or `undefined` on failure.
|
|
272
|
+
*/
|
|
273
|
+
async function fetchChildrenByJql(baseUrl, auth, jql) {
|
|
274
|
+
// Fetch total count
|
|
275
|
+
const totalResponse = await fetch(`${baseUrl}/rest/api/3/search/jql`, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: {
|
|
278
|
+
...jiraHeaders(auth),
|
|
279
|
+
'Content-Type': 'application/json',
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({ jql, maxResults: 0 }),
|
|
282
|
+
});
|
|
283
|
+
if (!totalResponse.ok)
|
|
284
|
+
return undefined;
|
|
285
|
+
const totalJson = (await totalResponse.json());
|
|
286
|
+
const total = totalJson.total ?? 0;
|
|
287
|
+
if (total === 0)
|
|
288
|
+
return { total: 0, incomplete: 0 };
|
|
289
|
+
// Fetch incomplete count
|
|
290
|
+
const incompleteResponse = await fetch(`${baseUrl}/rest/api/3/search/jql`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: {
|
|
293
|
+
...jiraHeaders(auth),
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
jql: `${jql} AND statusCategory != "done"`,
|
|
298
|
+
maxResults: 0,
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
if (!incompleteResponse.ok)
|
|
302
|
+
return undefined;
|
|
303
|
+
const incompleteJson = (await incompleteResponse.json());
|
|
304
|
+
const incomplete = incompleteJson.total ?? 0;
|
|
305
|
+
return { total, incomplete };
|
|
306
|
+
}
|
|
234
307
|
/**
|
|
235
308
|
* Look up a Jira transition ID by status name.
|
|
236
309
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jira.js","sourceRoot":"","sources":["../../../../src/scripts/board/jira/jira.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAI1E,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAClD,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa;IACzD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,OAAe,EACf,UAAkB,EAClB,IAAY;IAEZ,OAAO,YAAY,CACjB,GAAG,OAAO,uBAAuB,UAAU,EAAE,EAC7C,WAAW,CAAC,IAAI,CAAC,EACjB;QACE,GAAG,EAAE,wCAAwC;QAC7C,GAAG,EAAE,2CAA2C;QAChD,GAAG,EAAE,mBAAmB,UAAU,aAAa;KAChD,EACD,wCAAwC,CACzC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CACtB,UAAkB,EAClB,MAAc,EACd,MAAe,EACf,KAAc;
|
|
1
|
+
{"version":3,"file":"jira.js","sourceRoot":"","sources":["../../../../src/scripts/board/jira/jira.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,4BAA4B,EAC5B,wBAAwB,EACxB,6BAA6B,GAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAI1E,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAClD,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa;IACzD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,OAAe,EACf,UAAkB,EAClB,IAAY;IAEZ,OAAO,YAAY,CACjB,GAAG,OAAO,uBAAuB,UAAU,EAAE,EAC7C,WAAW,CAAC,IAAI,CAAC,EACjB;QACE,GAAG,EAAE,wCAAwC;QAC7C,GAAG,EAAE,2CAA2C;QAChD,GAAG,EAAE,mBAAmB,UAAU,aAAa;KAChD,EACD,wCAAwC,CACzC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CACtB,UAAkB,EAClB,MAAc,EACd,MAAe,EACf,KAAc,EACd,WAAqB;IAErB,MAAM,KAAK,GAAG,CAAC,YAAY,UAAU,GAAG,CAAC,CAAC;IAE1C,IAAI,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAClD,IAAI,KAAK;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,KAAK,GAAG,CAAC,CAAC;IAC7C,IAAI,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAEvD,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;IACrC,KAAK,CAAC,IAAI,CAAC,WAAW,MAAM,GAAG,CAAC,CAAC;IAEjC,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,wBAAwB,CAAC;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAE/C,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,SAAS,IAAI,CAAC,IAAa;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,KAAK,MAAM,IAAI,IAAI,IAAI;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QAED,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACrC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,IAA+B,CAAC,EAAE,CAAC;gBACnE,IAAI,CAAC,KAAK,CAAC,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAe,EACf,IAAY,EACZ,UAAkB,EAClB,MAAc,EACd,MAAe,EACf,KAAc;IAQd,MAAM,OAAO,GAAG,MAAM,YAAY,CAChC,OAAO,EACP,IAAI,EACJ,UAAU,EACV,MAAM,EACN,MAAM,EACN,KAAK,EACL,KAAK,EACL,CAAC,CACF,CAAC;IACF,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAQD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAe,EACf,IAAY,EACZ,UAAkB,EAClB,MAAc,EACd,MAAe,EACf,KAAc,EACd,WAAqB,EACrB,KAAK,GAAG,CAAC;IAET,MAAM,GAAG,GAAG,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IAErE,IAAI,QAAkB,CAAC;IAEvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,wBAAwB,EAAE;YACzD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,GAAG,WAAW,CAAC,IAAI,CAAC;gBACpB,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,GAAG;gBACH,UAAU,EAAE,KAAK;gBACjB,MAAM,EAAE;oBACN,SAAS;oBACT,aAAa;oBACb,YAAY;oBACZ,QAAQ;oBACR,mBAAmB;iBACpB;aACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CACV,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACjF,CAAC;QACF,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,4BAA4B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAC5D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,IAAa,CAAC;IAElB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,wBAAwB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAExD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,qCAAqC,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1E,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACtC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAE5B,mBAAmB;QACnB,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;aACvC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC;aACvE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC;aACpC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAEhD,wDAAwD;QACxD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,iBAAiB,IAAI,SAAS,CAAC;QAE5E,OAAO;YACL,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,KAAK,EAAE,MAAM,CAAC,OAAO;YACrB,WAAW,EAAE,cAAc,CAAC,MAAM,CAAC,WAAW,CAAC;YAC/C,QAAQ,EAAE,MAAe;YACzB,OAAO;YACP,QAAQ;SACT,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAe,EACf,IAAY,EACZ,GAAW;IAEX,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAC1B,GAAG,OAAO,qBAAqB,GAAG,oBAAoB,EACtD,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAC/B,CAAC;QAEF,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAE/B,MAAM,IAAI,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,4BAA4B,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAE5D,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAElC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,IAAI,EAAE,CAAC;QAEnD,yDAAyD;QACzD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAC;YAC/C,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG;gBAAE,OAAO,KAAK,CAAC;YAEzC,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC;YAEzE,gDAAgD;YAChD,IAAI,CAAC,WAAW;gBAAE,OAAO,KAAK,CAAC;YAE/B,OAAO,WAAW,KAAK,MAAM,CAAC;QAChC,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAKD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAe,EACf,IAAY,EACZ,SAAiB;IAEjB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzD,IAAI,CAAC;QACH,uFAAuF;QACvF,MAAM,aAAa,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,MAAM,cAAc,GAAG,MAAM,kBAAkB,CAC7C,OAAO,EACP,IAAI,EACJ,cAAc,aAAa,8BAA8B,SAAS,GAAG,CACtE,CAAC;QAEF,IAAI,cAAc,IAAI,cAAc,CAAC,KAAK,GAAG,CAAC;YAAE,OAAO,cAAc,CAAC;QAEtE,yCAAyC;QACzC,OAAO,MAAM,kBAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,SAAS,EAAE,CAAC,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,kBAAkB,CAC/B,OAAe,EACf,IAAY,EACZ,GAAW;IAEX,oBAAoB;IACpB,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,wBAAwB,EAAE;QACpE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,GAAG,WAAW,CAAC,IAAI,CAAC;YACpB,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;KAC7C,CAAC,CAAC;IAEH,IAAI,CAAC,aAAa,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,SAAS,GAAG,CAAC,MAAM,aAAa,CAAC,IAAI,EAAE,CAAuB,CAAC;IACrE,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC;IAEnC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAEpD,yBAAyB;IACzB,MAAM,kBAAkB,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,wBAAwB,EAAE;QACzE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,GAAG,WAAW,CAAC,IAAI,CAAC;YACpB,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,GAAG,EAAE,GAAG,GAAG,+BAA+B;YAC1C,UAAU,EAAE,CAAC;SACd,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,kBAAkB,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAE7C,MAAM,cAAc,GAAG,CAAC,MAAM,kBAAkB,CAAC,IAAI,EAAE,CAEtD,CAAC;IACF,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,IAAI,CAAC,CAAC;IAE7C,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAe,EACf,IAAY,EACZ,QAAgB,EAChB,UAAkB;IAElB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAC;IAExD,IAAI,QAAkB,CAAC;IAEvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CACpB,GAAG,OAAO,qBAAqB,QAAQ,cAAc,EACrD,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAC/B,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CACV,sCAAsC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACzF,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAEnC,IAAI,IAAa,CAAC;IAElB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;QACzD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,6BAA6B,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAE7D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CACV,2CAA2C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAClE,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAE9E,OAAO,UAAU,EAAE,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAe,EACf,IAAY,EACZ,QAAgB,EAChB,UAAkB;IAElB,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,UAAU,CACX,CAAC;QAEF,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,CAAC,IAAI,CACV,sBAAsB,UAAU,mBAAmB,QAAQ,EAAE,CAC9D,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAC1B,GAAG,OAAO,qBAAqB,QAAQ,cAAc,EACrD;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,GAAG,WAAW,CAAC,IAAI,CAAC;gBACpB,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,CAAC;SAC3D,CACF,CAAC;QAEF,OAAO,QAAQ,CAAC,EAAE,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -50,23 +50,50 @@ export declare function fetchIssue(env: LinearEnv): Promise<(Ticket & {
|
|
|
50
50
|
issueId: string;
|
|
51
51
|
parentIdentifier?: string;
|
|
52
52
|
}) | undefined>;
|
|
53
|
+
/** Linear ticket with issue ID and optional parent info. */
|
|
54
|
+
export type LinearTicket = Ticket & {
|
|
55
|
+
issueId: string;
|
|
56
|
+
parentIdentifier?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Fetch multiple candidate issues from Linear.
|
|
60
|
+
*
|
|
61
|
+
* @param env - The Linear environment variables.
|
|
62
|
+
* @param excludeHitl - If `true`, excludes issues with the `clancy:hitl` label.
|
|
63
|
+
* @param limit - Maximum number of results to return (default: 5).
|
|
64
|
+
* @returns Array of fetched tickets (may be empty).
|
|
65
|
+
*/
|
|
66
|
+
export declare function fetchIssues(env: LinearEnv, excludeHitl?: boolean, limit?: number): Promise<LinearTicket[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Check whether a Linear issue is blocked by unresolved blockers.
|
|
69
|
+
*
|
|
70
|
+
* Queries the issue's relations for `blockedBy` type relationships and checks
|
|
71
|
+
* if any blocking issues have an unresolved state (not "completed" or "canceled").
|
|
72
|
+
*
|
|
73
|
+
* @param apiKey - The Linear personal API key.
|
|
74
|
+
* @param issueId - The Linear issue UUID.
|
|
75
|
+
* @returns `true` if any blocker is unresolved, `false` otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export declare function fetchBlockerStatus(apiKey: string, issueId: string): Promise<boolean>;
|
|
53
78
|
/** Result of checking children status for a parent issue. */
|
|
54
79
|
export type ChildrenStatus = {
|
|
55
80
|
total: number;
|
|
56
81
|
incomplete: number;
|
|
57
82
|
};
|
|
58
83
|
/**
|
|
59
|
-
* Fetch the children status of a Linear parent issue.
|
|
84
|
+
* Fetch the children status of a Linear parent issue (dual-mode).
|
|
60
85
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
86
|
+
* Tries the `Epic: {identifier}` text convention first (searches for issues
|
|
87
|
+
* with "Epic: {parentIdentifier}" in their description). If no results, falls
|
|
88
|
+
* back to the native `children` API for backward compatibility.
|
|
64
89
|
*
|
|
65
90
|
* @param apiKey - The Linear personal API key.
|
|
66
91
|
* @param parentId - The Linear parent issue UUID.
|
|
92
|
+
* @param parentIdentifier - The Linear parent identifier (e.g., `'ENG-42'`).
|
|
93
|
+
* Required for Epic: text convention search. Falls back to native API only if not provided.
|
|
67
94
|
* @returns The children status, or `undefined` on failure.
|
|
68
95
|
*/
|
|
69
|
-
export declare function fetchChildrenStatus(apiKey: string, parentId: string): Promise<ChildrenStatus | undefined>;
|
|
96
|
+
export declare function fetchChildrenStatus(apiKey: string, parentId: string, parentIdentifier?: string): Promise<ChildrenStatus | undefined>;
|
|
70
97
|
/**
|
|
71
98
|
* Look up a Linear workflow state ID by name and team.
|
|
72
99
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../../../src/scripts/board/linear/linear.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../../../src/scripts/board/linear/linear.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK/C,KAAK,SAAS,GAAG;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,OAAO,CAAC,CA8BlB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsC1C;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CACrD,CAAC,MAAM,GAAG;IACR,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,GACF,SAAS,CACZ,CAGA;AAED,4DAA4D;AAC5D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,SAAS,EACd,WAAW,CAAC,EAAE,OAAO,EACrB,KAAK,SAAI,GACR,OAAO,CAAC,YAAY,EAAE,CAAC,CA8EzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CA8BlB;AAED,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAiBrC;AAuFD;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB7B;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAelB"}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Important: Linear personal API keys do NOT use "Bearer" prefix.
|
|
8
8
|
* Only OAuth tokens use "Bearer". This is intentional per Linear docs.
|
|
9
9
|
*/
|
|
10
|
-
import { linearIssueUpdateResponseSchema, linearIssuesResponseSchema, linearViewerResponseSchema, linearWorkflowStatesResponseSchema, } from '../../../schemas/linear.js';
|
|
10
|
+
import { linearIssueRelationsResponseSchema, linearIssueSearchResponseSchema, linearIssueUpdateResponseSchema, linearIssuesResponseSchema, linearViewerResponseSchema, linearWorkflowStatesResponseSchema, } from '../../../schemas/linear.js';
|
|
11
11
|
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
12
12
|
const LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
13
13
|
/**
|
|
@@ -111,19 +111,40 @@ export async function pingLinear(apiKey) {
|
|
|
111
111
|
* @returns The fetched ticket with optional parent info, or `undefined` if none available.
|
|
112
112
|
*/
|
|
113
113
|
export async function fetchIssue(env) {
|
|
114
|
+
const results = await fetchIssues(env, false, 1);
|
|
115
|
+
return results[0];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fetch multiple candidate issues from Linear.
|
|
119
|
+
*
|
|
120
|
+
* @param env - The Linear environment variables.
|
|
121
|
+
* @param excludeHitl - If `true`, excludes issues with the `clancy:hitl` label.
|
|
122
|
+
* @param limit - Maximum number of results to return (default: 5).
|
|
123
|
+
* @returns Array of fetched tickets (may be empty).
|
|
124
|
+
*/
|
|
125
|
+
export async function fetchIssues(env, excludeHitl, limit = 5) {
|
|
114
126
|
const label = env.CLANCY_LABEL?.trim();
|
|
115
127
|
const hasLabel = Boolean(label);
|
|
116
128
|
const labelFilter = hasLabel ? 'labels: { name: { eq: $label } }' : '';
|
|
129
|
+
// Build variable declarations for the query
|
|
130
|
+
const varDecls = [
|
|
131
|
+
'$teamId: String!',
|
|
132
|
+
...(hasLabel ? ['$label: String!'] : []),
|
|
133
|
+
];
|
|
134
|
+
// Build filter parts
|
|
135
|
+
const filterParts = [
|
|
136
|
+
'state: { type: { eq: "unstarted" } }',
|
|
137
|
+
'team: { id: { eq: $teamId } }',
|
|
138
|
+
labelFilter,
|
|
139
|
+
].filter(Boolean);
|
|
117
140
|
const query = `
|
|
118
|
-
query($
|
|
141
|
+
query(${varDecls.join(', ')}) {
|
|
119
142
|
viewer {
|
|
120
143
|
assignedIssues(
|
|
121
144
|
filter: {
|
|
122
|
-
|
|
123
|
-
team: { id: { eq: $teamId } }
|
|
124
|
-
${labelFilter}
|
|
145
|
+
${filterParts.join('\n ')}
|
|
125
146
|
}
|
|
126
|
-
first:
|
|
147
|
+
first: ${excludeHitl ? limit * 3 : limit}
|
|
127
148
|
orderBy: priority
|
|
128
149
|
) {
|
|
129
150
|
nodes {
|
|
@@ -132,6 +153,7 @@ export async function fetchIssue(env) {
|
|
|
132
153
|
title
|
|
133
154
|
description
|
|
134
155
|
parent { identifier title }
|
|
156
|
+
labels { nodes { name } }
|
|
135
157
|
}
|
|
136
158
|
}
|
|
137
159
|
}
|
|
@@ -146,33 +168,129 @@ export async function fetchIssue(env) {
|
|
|
146
168
|
const parsed = linearIssuesResponseSchema.safeParse(raw);
|
|
147
169
|
if (!parsed.success) {
|
|
148
170
|
console.warn(`⚠ Unexpected Linear response shape: ${parsed.error.message}`);
|
|
149
|
-
return
|
|
171
|
+
return [];
|
|
150
172
|
}
|
|
151
|
-
|
|
173
|
+
let nodes = parsed.data.data?.viewer?.assignedIssues?.nodes;
|
|
152
174
|
if (!nodes?.length)
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
175
|
+
return [];
|
|
176
|
+
// HITL/AFK filtering: exclude issues with clancy:hitl label (using parsed schema data)
|
|
177
|
+
if (excludeHitl) {
|
|
178
|
+
nodes = nodes.filter((n) => !n.labels?.nodes?.some((l) => l.name === 'clancy:hitl'));
|
|
179
|
+
}
|
|
180
|
+
// Trim to requested limit after filtering
|
|
181
|
+
nodes = nodes.slice(0, limit);
|
|
182
|
+
return nodes.map((issue) => ({
|
|
156
183
|
key: issue.identifier,
|
|
157
184
|
title: issue.title,
|
|
158
185
|
description: issue.description ?? '',
|
|
159
186
|
provider: 'linear',
|
|
160
187
|
issueId: issue.id,
|
|
161
188
|
parentIdentifier: issue.parent?.identifier,
|
|
162
|
-
};
|
|
189
|
+
}));
|
|
163
190
|
}
|
|
164
191
|
/**
|
|
165
|
-
*
|
|
192
|
+
* Check whether a Linear issue is blocked by unresolved blockers.
|
|
193
|
+
*
|
|
194
|
+
* Queries the issue's relations for `blockedBy` type relationships and checks
|
|
195
|
+
* if any blocking issues have an unresolved state (not "completed" or "canceled").
|
|
196
|
+
*
|
|
197
|
+
* @param apiKey - The Linear personal API key.
|
|
198
|
+
* @param issueId - The Linear issue UUID.
|
|
199
|
+
* @returns `true` if any blocker is unresolved, `false` otherwise.
|
|
200
|
+
*/
|
|
201
|
+
export async function fetchBlockerStatus(apiKey, issueId) {
|
|
202
|
+
const query = `
|
|
203
|
+
query($issueId: String!) {
|
|
204
|
+
issue(id: $issueId) {
|
|
205
|
+
relations {
|
|
206
|
+
nodes {
|
|
207
|
+
type
|
|
208
|
+
relatedIssue {
|
|
209
|
+
state { type }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
const raw = await linearGraphql(apiKey, query, { issueId });
|
|
217
|
+
const parsed = linearIssueRelationsResponseSchema.safeParse(raw);
|
|
218
|
+
if (!parsed.success)
|
|
219
|
+
return false;
|
|
220
|
+
const relations = parsed.data.data?.issue?.relations?.nodes ?? [];
|
|
221
|
+
const doneTypes = new Set(['completed', 'canceled']);
|
|
222
|
+
return relations.some((rel) => {
|
|
223
|
+
if (rel.type !== 'blockedBy')
|
|
224
|
+
return false;
|
|
225
|
+
const stateType = rel.relatedIssue?.state?.type;
|
|
226
|
+
if (!stateType)
|
|
227
|
+
return false;
|
|
228
|
+
return !doneTypes.has(stateType);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Fetch the children status of a Linear parent issue (dual-mode).
|
|
233
|
+
*
|
|
234
|
+
* Tries the `Epic: {identifier}` text convention first (searches for issues
|
|
235
|
+
* with "Epic: {parentIdentifier}" in their description). If no results, falls
|
|
236
|
+
* back to the native `children` API for backward compatibility.
|
|
166
237
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
238
|
+
* @param apiKey - The Linear personal API key.
|
|
239
|
+
* @param parentId - The Linear parent issue UUID.
|
|
240
|
+
* @param parentIdentifier - The Linear parent identifier (e.g., `'ENG-42'`).
|
|
241
|
+
* Required for Epic: text convention search. Falls back to native API only if not provided.
|
|
242
|
+
* @returns The children status, or `undefined` on failure.
|
|
243
|
+
*/
|
|
244
|
+
export async function fetchChildrenStatus(apiKey, parentId, parentIdentifier) {
|
|
245
|
+
try {
|
|
246
|
+
// Mode 1: Try Epic: text convention (only if identifier is available)
|
|
247
|
+
if (parentIdentifier) {
|
|
248
|
+
const epicTextResult = await fetchChildrenByDescription(apiKey, `Epic: ${parentIdentifier}`);
|
|
249
|
+
if (epicTextResult && epicTextResult.total > 0)
|
|
250
|
+
return epicTextResult;
|
|
251
|
+
}
|
|
252
|
+
// Mode 2: Fall back to native children API
|
|
253
|
+
return await fetchChildrenByNativeApi(apiKey, parentId);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Fetch children status by searching for a description substring.
|
|
261
|
+
*
|
|
262
|
+
* @param apiKey - The Linear personal API key.
|
|
263
|
+
* @param descriptionRef - The description substring to search for.
|
|
264
|
+
* @returns The children status, or `undefined` on failure.
|
|
265
|
+
*/
|
|
266
|
+
async function fetchChildrenByDescription(apiKey, descriptionRef) {
|
|
267
|
+
const query = `
|
|
268
|
+
query($filter: String!) {
|
|
269
|
+
issueSearch(query: $filter) {
|
|
270
|
+
nodes {
|
|
271
|
+
state { type }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
const raw = await linearGraphql(apiKey, query, { filter: descriptionRef });
|
|
277
|
+
const parsed = linearIssueSearchResponseSchema.safeParse(raw);
|
|
278
|
+
if (!parsed.success)
|
|
279
|
+
return undefined;
|
|
280
|
+
const nodes = parsed.data.data?.issueSearch?.nodes ?? [];
|
|
281
|
+
const total = nodes.length;
|
|
282
|
+
const doneTypes = new Set(['completed', 'canceled']);
|
|
283
|
+
const incomplete = nodes.filter((n) => !n.state?.type || !doneTypes.has(n.state.type)).length;
|
|
284
|
+
return { total, incomplete };
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Fetch children status using the native parent-child API.
|
|
170
288
|
*
|
|
171
289
|
* @param apiKey - The Linear personal API key.
|
|
172
290
|
* @param parentId - The Linear parent issue UUID.
|
|
173
291
|
* @returns The children status, or `undefined` on failure.
|
|
174
292
|
*/
|
|
175
|
-
|
|
293
|
+
async function fetchChildrenByNativeApi(apiKey, parentId) {
|
|
176
294
|
const query = `
|
|
177
295
|
query($issueId: String!) {
|
|
178
296
|
issue(id: $issueId) {
|