azdo-cli 0.19.0-feature-037-api-error-surfacing.692 → 0.19.0-feature-037-api-error-surfacing.695
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 +2 -0
- package/dist/index.js +362 -205
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ azdo pr comment-reply 148 "Done." --pr-number 64 # flat
|
|
|
89
89
|
# Open a pull request — description from a repo template when you don't pass one
|
|
90
90
|
azdo pr open --title "Fix the thing" --description "Because X was broken"
|
|
91
91
|
azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
|
|
92
|
+
# The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
|
|
93
|
+
# the command says by how much and creates nothing. See docs/commands.md.
|
|
92
94
|
|
|
93
95
|
# Link/unlink a work item, add/remove reviewers
|
|
94
96
|
azdo pr work-items link 1234 --pr-number 64
|
package/dist/index.js
CHANGED
|
@@ -78,6 +78,22 @@ function redactUrl(url) {
|
|
|
78
78
|
return url;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
var SECRET_IN_TEXT = [
|
|
82
|
+
[/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
83
|
+
[
|
|
84
|
+
/(\b(?:access[_-]?token|refresh[_-]?token|token|pat|password|secret|api[_-]?key)\b["']?\s*[:=]\s*["']?)[^"'\s,;&}]+/gi,
|
|
85
|
+
`$1${REDACTED}`
|
|
86
|
+
],
|
|
87
|
+
[/\beyJ[A-Za-z0-9._~+/=-]{20,}/g, REDACTED],
|
|
88
|
+
[/\b(?=[A-Za-z0-9]*\d)(?=[A-Za-z0-9]*[A-Za-z])[A-Za-z0-9]{40,}\b/g, REDACTED]
|
|
89
|
+
];
|
|
90
|
+
function redactText(text) {
|
|
91
|
+
let out = text;
|
|
92
|
+
for (const [pattern, replacement] of SECRET_IN_TEXT) {
|
|
93
|
+
out = out.replace(pattern, replacement);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
81
97
|
function redactBody(body) {
|
|
82
98
|
if (body === null) return null;
|
|
83
99
|
try {
|
|
@@ -123,6 +139,131 @@ function getActiveTraceWriter() {
|
|
|
123
139
|
return activeWriter;
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
// src/services/command-helpers.ts
|
|
143
|
+
function isSentinel(message, sentinel) {
|
|
144
|
+
return message === sentinel || message.startsWith(`${sentinel}: `);
|
|
145
|
+
}
|
|
146
|
+
function withDetail(sentinel, detail) {
|
|
147
|
+
return detail === null || detail === "" ? sentinel : `${sentinel}: ${detail}`;
|
|
148
|
+
}
|
|
149
|
+
function sentinelDetail(message, sentinel) {
|
|
150
|
+
return message.startsWith(`${sentinel}: `) ? message.slice(sentinel.length + 2) : null;
|
|
151
|
+
}
|
|
152
|
+
function splitSentinel(message) {
|
|
153
|
+
const separator = message.indexOf(": ");
|
|
154
|
+
return separator === -1 ? { sentinel: message, detail: null } : { sentinel: message.slice(0, separator), detail: message.slice(separator + 2) };
|
|
155
|
+
}
|
|
156
|
+
function writeErrorDetail(message, sentinel) {
|
|
157
|
+
const detail = sentinelDetail(message, sentinel);
|
|
158
|
+
if (detail !== null) {
|
|
159
|
+
process.stderr.write(` ${detail}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function promptYesNo(prompt) {
|
|
164
|
+
if (!process.stdin.isTTY) return true;
|
|
165
|
+
process.stderr.write(prompt);
|
|
166
|
+
return await new Promise((resolve2) => {
|
|
167
|
+
process.stdin.setEncoding("utf8");
|
|
168
|
+
let answered = false;
|
|
169
|
+
const handler = (data) => {
|
|
170
|
+
if (answered) return;
|
|
171
|
+
answered = true;
|
|
172
|
+
process.stdin.removeListener("data", handler);
|
|
173
|
+
process.stdin.pause();
|
|
174
|
+
const trimmed = data.trim().toLowerCase();
|
|
175
|
+
resolve2(trimmed === "y" || trimmed === "yes");
|
|
176
|
+
};
|
|
177
|
+
process.stdin.resume();
|
|
178
|
+
process.stdin.on("data", handler);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function parseWorkItemId(idStr) {
|
|
182
|
+
const id = Number.parseInt(idStr, 10);
|
|
183
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
186
|
+
`
|
|
187
|
+
);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
return id;
|
|
191
|
+
}
|
|
192
|
+
function validateOrgProjectPair(options) {
|
|
193
|
+
const hasOrg = options.org !== void 0;
|
|
194
|
+
const hasProject = options.project !== void 0;
|
|
195
|
+
if (hasOrg !== hasProject) {
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
198
|
+
);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function validateSource(options) {
|
|
203
|
+
const hasContent = options.content !== void 0;
|
|
204
|
+
const hasFile = options.file !== void 0;
|
|
205
|
+
if (hasContent === hasFile) {
|
|
206
|
+
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function formatCreateError(err) {
|
|
211
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
212
|
+
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
213
|
+
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
214
|
+
if (requiredMatches.length > 0) {
|
|
215
|
+
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
216
|
+
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
217
|
+
}
|
|
218
|
+
return `Create rejected: ${message}`;
|
|
219
|
+
}
|
|
220
|
+
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
221
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
222
|
+
const msg = error.message;
|
|
223
|
+
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
224
|
+
if (isSentinel(msg, "AUTH_FAILED")) {
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
`Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
|
|
227
|
+
`
|
|
228
|
+
);
|
|
229
|
+
writeErrorDetail(msg, "AUTH_FAILED");
|
|
230
|
+
} else if (isSentinel(msg, "PERMISSION_DENIED")) {
|
|
231
|
+
process.stderr.write(
|
|
232
|
+
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
233
|
+
`
|
|
234
|
+
);
|
|
235
|
+
writeErrorDetail(msg, "PERMISSION_DENIED");
|
|
236
|
+
} else if (msg.startsWith("NOT_FOUND")) {
|
|
237
|
+
process.stderr.write(
|
|
238
|
+
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
239
|
+
`
|
|
240
|
+
);
|
|
241
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
244
|
+
);
|
|
245
|
+
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
246
|
+
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
247
|
+
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
248
|
+
`);
|
|
249
|
+
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
250
|
+
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
251
|
+
`);
|
|
252
|
+
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
253
|
+
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
254
|
+
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
255
|
+
`);
|
|
256
|
+
} else {
|
|
257
|
+
process.stderr.write(`Error: ${msg}
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
if (exit) {
|
|
261
|
+
process.exit(1);
|
|
262
|
+
} else {
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
126
267
|
// src/services/image-download.ts
|
|
127
268
|
import { Jimp } from "jimp";
|
|
128
269
|
import { writeFile } from "fs/promises";
|
|
@@ -326,65 +467,133 @@ async function fetchRaw(url, init) {
|
|
|
326
467
|
const body = await response.text();
|
|
327
468
|
return { status: response.status, body };
|
|
328
469
|
}
|
|
329
|
-
|
|
470
|
+
var MAX_DETAIL_CHARS = 500;
|
|
471
|
+
var MAX_RAW_BODY_CHARS = 200;
|
|
472
|
+
var failureDetails = /* @__PURE__ */ new WeakMap();
|
|
473
|
+
function renderErrorFields(body, requireMessage = false) {
|
|
474
|
+
let parsed;
|
|
475
|
+
try {
|
|
476
|
+
parsed = JSON.parse(body);
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
481
|
+
return renderErrorRecord(parsed, requireMessage);
|
|
482
|
+
}
|
|
483
|
+
function renderErrorRecord(record, requireMessage) {
|
|
484
|
+
const parts = [];
|
|
485
|
+
const message = typeof record.message === "string" ? record.message.trim() : "";
|
|
486
|
+
if (message !== "") {
|
|
487
|
+
parts.push(message);
|
|
488
|
+
} else if (requireMessage) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (typeof record.typeKey === "string" && record.typeKey.trim() !== "") {
|
|
492
|
+
parts.push(`[${record.typeKey.trim()}]`);
|
|
493
|
+
} else if (typeof record.errorCode === "number" || typeof record.errorCode === "string") {
|
|
494
|
+
parts.push(`[errorCode ${record.errorCode}]`);
|
|
495
|
+
}
|
|
496
|
+
return parts.length === 0 ? null : parts.join(" ");
|
|
497
|
+
}
|
|
498
|
+
function describeFailureBody(body, contentType) {
|
|
499
|
+
if (body === null) return null;
|
|
500
|
+
const trimmed = body.trim();
|
|
501
|
+
if (trimmed === "") return null;
|
|
502
|
+
if (contentType.toLowerCase().startsWith("text/html") || trimmed.startsWith("<")) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const redacted = redactBody(trimmed) ?? trimmed;
|
|
506
|
+
return truncateDetail(renderErrorFields(redacted) ?? redacted.slice(0, MAX_RAW_BODY_CHARS));
|
|
507
|
+
}
|
|
508
|
+
function truncateDetail(detail) {
|
|
509
|
+
const collapsed = redactText(detail).replace(/\s+/g, " ").trim();
|
|
510
|
+
if (collapsed === "") return null;
|
|
511
|
+
return collapsed.length > MAX_DETAIL_CHARS ? `${collapsed.slice(0, MAX_DETAIL_CHARS)}\u2026(truncated)` : collapsed;
|
|
512
|
+
}
|
|
513
|
+
function httpError(response) {
|
|
514
|
+
return new Error(withDetail(`HTTP_${response.status}`, failureDetails.get(response) ?? null));
|
|
515
|
+
}
|
|
516
|
+
var failureBodies = /* @__PURE__ */ new WeakMap();
|
|
517
|
+
async function readFailureBody(response, tracedBody) {
|
|
518
|
+
if (tracedBody !== null) return tracedBody;
|
|
519
|
+
try {
|
|
520
|
+
return await response.clone().text();
|
|
521
|
+
} catch {
|
|
522
|
+
try {
|
|
523
|
+
return await response.text();
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function writeTraceEntry(writer, url, init, response, responseBody) {
|
|
530
|
+
const respHeaders = {};
|
|
531
|
+
response.headers.forEach((v, k) => {
|
|
532
|
+
respHeaders[k] = v;
|
|
533
|
+
});
|
|
534
|
+
const entry = {
|
|
535
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
536
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
537
|
+
url: redactUrl(url),
|
|
538
|
+
requestHeaders: redactHeaders(init.headers ?? {}),
|
|
539
|
+
requestBody: typeof init.body === "string" ? redactBody(init.body) : null,
|
|
540
|
+
responseStatus: response.status,
|
|
541
|
+
responseHeaders: respHeaders,
|
|
542
|
+
responseBody
|
|
543
|
+
};
|
|
544
|
+
writer.append(entry);
|
|
545
|
+
}
|
|
546
|
+
async function traceResponse(url, init, response) {
|
|
330
547
|
const writer = getActiveTraceWriter();
|
|
548
|
+
if (!writer) return null;
|
|
549
|
+
let responseBody = "";
|
|
550
|
+
try {
|
|
551
|
+
responseBody = await response.clone().text();
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
writeTraceEntry(writer, url, init, response, responseBody);
|
|
555
|
+
return responseBody;
|
|
556
|
+
}
|
|
557
|
+
async function fetchWithErrors(url, init) {
|
|
331
558
|
let response;
|
|
332
559
|
try {
|
|
333
560
|
response = await fetch(url, init);
|
|
334
561
|
} catch (err) {
|
|
335
562
|
throw new Error("NETWORK_ERROR", { cause: err });
|
|
336
563
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
const respHeaders = {};
|
|
347
|
-
response.headers.forEach((v, k) => {
|
|
348
|
-
respHeaders[k] = v;
|
|
349
|
-
});
|
|
350
|
-
const entry = {
|
|
351
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
352
|
-
method: (init.method ?? "GET").toUpperCase(),
|
|
353
|
-
url: redactUrl(url),
|
|
354
|
-
requestHeaders: reqHeaders,
|
|
355
|
-
requestBody: reqBody ?? null,
|
|
356
|
-
responseStatus: response.status,
|
|
357
|
-
responseHeaders: respHeaders,
|
|
358
|
-
responseBody
|
|
359
|
-
};
|
|
360
|
-
writer.append(entry);
|
|
564
|
+
const tracedBody = await traceResponse(url, init, response);
|
|
565
|
+
const contentType = response.headers?.get("content-type") ?? "";
|
|
566
|
+
let failureDetail = null;
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
const body = await readFailureBody(response, tracedBody);
|
|
569
|
+
if (body !== null) failureBodies.set(response, body);
|
|
570
|
+
failureDetail = describeFailureBody(body, contentType);
|
|
571
|
+
if (failureDetail !== null) failureDetails.set(response, failureDetail);
|
|
361
572
|
}
|
|
362
|
-
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
363
|
-
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
573
|
+
if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
|
|
574
|
+
if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
|
|
364
575
|
if (response.status === 404) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const body = await response.text();
|
|
368
|
-
detail = ` | url=${url} | body=${body}`;
|
|
369
|
-
} catch {
|
|
370
|
-
}
|
|
371
|
-
throw new Error(`NOT_FOUND${detail}`);
|
|
576
|
+
const body = failureBodies.get(response);
|
|
577
|
+
throw new Error(body === void 0 ? "NOT_FOUND" : `NOT_FOUND | url=${url} | body=${body}`);
|
|
372
578
|
}
|
|
373
|
-
const contentType = response.headers?.get("content-type") ?? "";
|
|
374
579
|
if (contentType.toLowerCase().startsWith("text/html")) {
|
|
375
580
|
throw new Error("AUTH_FAILED");
|
|
376
581
|
}
|
|
377
582
|
return response;
|
|
378
583
|
}
|
|
379
584
|
async function readResponseMessage(response) {
|
|
585
|
+
const captured = failureBodies.get(response);
|
|
586
|
+
const rendered = captured === void 0 ? await renderErrorStream(response) : renderErrorFields(redactBody(captured.trim()) ?? captured.trim(), true);
|
|
587
|
+
return rendered === null ? null : truncateDetail(rendered);
|
|
588
|
+
}
|
|
589
|
+
async function renderErrorStream(response) {
|
|
380
590
|
try {
|
|
381
591
|
const body = await response.json();
|
|
382
|
-
if (typeof body
|
|
383
|
-
|
|
384
|
-
}
|
|
592
|
+
if (typeof body !== "object" || body === null) return null;
|
|
593
|
+
return renderErrorRecord(body, true);
|
|
385
594
|
} catch {
|
|
595
|
+
return null;
|
|
386
596
|
}
|
|
387
|
-
return null;
|
|
388
597
|
}
|
|
389
598
|
function normalizeFieldList(fields) {
|
|
390
599
|
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
@@ -464,7 +673,7 @@ async function readWriteResponse(response, errorCode) {
|
|
|
464
673
|
throw new Error(`${errorCode}: ${serverMessage}`);
|
|
465
674
|
}
|
|
466
675
|
if (!response.ok) {
|
|
467
|
-
throw
|
|
676
|
+
throw httpError(response);
|
|
468
677
|
}
|
|
469
678
|
const data = await response.json();
|
|
470
679
|
return {
|
|
@@ -487,7 +696,7 @@ async function getWorkItemFields(context, id, cred) {
|
|
|
487
696
|
}
|
|
488
697
|
}
|
|
489
698
|
if (!response.ok) {
|
|
490
|
-
throw
|
|
699
|
+
throw httpError(response);
|
|
491
700
|
}
|
|
492
701
|
const data = await response.json();
|
|
493
702
|
return data.fields;
|
|
@@ -539,7 +748,7 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
539
748
|
}
|
|
540
749
|
}
|
|
541
750
|
if (!response.ok) {
|
|
542
|
-
throw
|
|
751
|
+
throw httpError(response);
|
|
543
752
|
}
|
|
544
753
|
return await response.json();
|
|
545
754
|
}
|
|
@@ -550,7 +759,7 @@ async function getOrgFieldNames(context, cred) {
|
|
|
550
759
|
url.searchParams.set("api-version", "7.1");
|
|
551
760
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
552
761
|
if (!response.ok) {
|
|
553
|
-
throw
|
|
762
|
+
throw httpError(response);
|
|
554
763
|
}
|
|
555
764
|
const data = await response.json();
|
|
556
765
|
return (data.value ?? []).map((f) => f.referenceName);
|
|
@@ -632,7 +841,7 @@ async function getWorkItemFieldValue(context, id, cred, fieldName) {
|
|
|
632
841
|
}
|
|
633
842
|
}
|
|
634
843
|
if (!response.ok) {
|
|
635
|
-
throw
|
|
844
|
+
throw httpError(response);
|
|
636
845
|
}
|
|
637
846
|
const data = await response.json();
|
|
638
847
|
const value = data.fields[fieldName];
|
|
@@ -650,7 +859,7 @@ async function listWorkItemComments(context, id, cred) {
|
|
|
650
859
|
{ headers: authHeaders(cred) }
|
|
651
860
|
);
|
|
652
861
|
if (!response.ok) {
|
|
653
|
-
throw
|
|
862
|
+
throw httpError(response);
|
|
654
863
|
}
|
|
655
864
|
const data = await response.json();
|
|
656
865
|
comments.push(
|
|
@@ -680,7 +889,7 @@ async function addWorkItemComment(context, id, cred, text, format = "html") {
|
|
|
680
889
|
throw new Error(`BAD_REQUEST: ${serverMessage}`);
|
|
681
890
|
}
|
|
682
891
|
if (!response.ok) {
|
|
683
|
-
throw
|
|
892
|
+
throw httpError(response);
|
|
684
893
|
}
|
|
685
894
|
const data = await response.json();
|
|
686
895
|
return {
|
|
@@ -732,7 +941,7 @@ async function applyWorkItemPatch(context, id, cred, operations) {
|
|
|
732
941
|
async function downloadAttachment(url, cred) {
|
|
733
942
|
const response = await fetchWithErrors(url, { headers: authHeaders(cred) });
|
|
734
943
|
if (!response.ok) {
|
|
735
|
-
throw
|
|
944
|
+
throw httpError(response);
|
|
736
945
|
}
|
|
737
946
|
return response.arrayBuffer();
|
|
738
947
|
}
|
|
@@ -757,7 +966,7 @@ async function createAttachment(context, fileName, content, cred) {
|
|
|
757
966
|
}
|
|
758
967
|
}
|
|
759
968
|
if (!response.ok) {
|
|
760
|
-
throw
|
|
969
|
+
throw httpError(response);
|
|
761
970
|
}
|
|
762
971
|
return await response.json();
|
|
763
972
|
}
|
|
@@ -1546,109 +1755,6 @@ function toMarkdown(content) {
|
|
|
1546
1755
|
return decodeEntitiesInMarkdownCodeSpans(decodeAdoEntitiesInMarkdown(content));
|
|
1547
1756
|
}
|
|
1548
1757
|
|
|
1549
|
-
// src/services/command-helpers.ts
|
|
1550
|
-
async function promptYesNo(prompt) {
|
|
1551
|
-
if (!process.stdin.isTTY) return true;
|
|
1552
|
-
process.stderr.write(prompt);
|
|
1553
|
-
return await new Promise((resolve2) => {
|
|
1554
|
-
process.stdin.setEncoding("utf8");
|
|
1555
|
-
let answered = false;
|
|
1556
|
-
const handler = (data) => {
|
|
1557
|
-
if (answered) return;
|
|
1558
|
-
answered = true;
|
|
1559
|
-
process.stdin.removeListener("data", handler);
|
|
1560
|
-
process.stdin.pause();
|
|
1561
|
-
const trimmed = data.trim().toLowerCase();
|
|
1562
|
-
resolve2(trimmed === "y" || trimmed === "yes");
|
|
1563
|
-
};
|
|
1564
|
-
process.stdin.resume();
|
|
1565
|
-
process.stdin.on("data", handler);
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
function parseWorkItemId(idStr) {
|
|
1569
|
-
const id = Number.parseInt(idStr, 10);
|
|
1570
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
1571
|
-
process.stderr.write(
|
|
1572
|
-
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
1573
|
-
`
|
|
1574
|
-
);
|
|
1575
|
-
process.exit(1);
|
|
1576
|
-
}
|
|
1577
|
-
return id;
|
|
1578
|
-
}
|
|
1579
|
-
function validateOrgProjectPair(options) {
|
|
1580
|
-
const hasOrg = options.org !== void 0;
|
|
1581
|
-
const hasProject = options.project !== void 0;
|
|
1582
|
-
if (hasOrg !== hasProject) {
|
|
1583
|
-
process.stderr.write(
|
|
1584
|
-
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
1585
|
-
);
|
|
1586
|
-
process.exit(1);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function validateSource(options) {
|
|
1590
|
-
const hasContent = options.content !== void 0;
|
|
1591
|
-
const hasFile = options.file !== void 0;
|
|
1592
|
-
if (hasContent === hasFile) {
|
|
1593
|
-
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
1594
|
-
process.exit(1);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
function formatCreateError(err) {
|
|
1598
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1599
|
-
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
1600
|
-
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
1601
|
-
if (requiredMatches.length > 0) {
|
|
1602
|
-
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
1603
|
-
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
1604
|
-
}
|
|
1605
|
-
return `Create rejected: ${message}`;
|
|
1606
|
-
}
|
|
1607
|
-
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
1608
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1609
|
-
const msg = error.message;
|
|
1610
|
-
const scopeLabel = scope === "read" ? "Work Items (read)" : "Work Items (Read & Write)";
|
|
1611
|
-
if (msg === "AUTH_FAILED") {
|
|
1612
|
-
process.stderr.write(
|
|
1613
|
-
`Error: Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.
|
|
1614
|
-
`
|
|
1615
|
-
);
|
|
1616
|
-
} else if (msg === "PERMISSION_DENIED") {
|
|
1617
|
-
process.stderr.write(
|
|
1618
|
-
`Error: Access denied. Your PAT may lack ${scope} permissions for project "${context?.project}".
|
|
1619
|
-
`
|
|
1620
|
-
);
|
|
1621
|
-
} else if (msg.startsWith("NOT_FOUND")) {
|
|
1622
|
-
process.stderr.write(
|
|
1623
|
-
`Error: Work item ${id} not found in ${context?.org}/${context?.project}.
|
|
1624
|
-
`
|
|
1625
|
-
);
|
|
1626
|
-
} else if (msg === "NETWORK_ERROR") {
|
|
1627
|
-
process.stderr.write(
|
|
1628
|
-
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
1629
|
-
);
|
|
1630
|
-
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
1631
|
-
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
1632
|
-
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
1633
|
-
`);
|
|
1634
|
-
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
1635
|
-
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
1636
|
-
`);
|
|
1637
|
-
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
1638
|
-
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
1639
|
-
process.stderr.write(`Error: Update rejected: ${serverMsg}
|
|
1640
|
-
`);
|
|
1641
|
-
} else {
|
|
1642
|
-
process.stderr.write(`Error: ${msg}
|
|
1643
|
-
`);
|
|
1644
|
-
}
|
|
1645
|
-
if (exit) {
|
|
1646
|
-
process.exit(1);
|
|
1647
|
-
} else {
|
|
1648
|
-
process.exitCode = 1;
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
1758
|
// src/commands/get-item.ts
|
|
1653
1759
|
function parseRequestedFields(raw) {
|
|
1654
1760
|
if (raw === void 0) return void 0;
|
|
@@ -3132,10 +3238,10 @@ function buildUpsertResult(action, writeResult, fields, fallbackWorkItemType) {
|
|
|
3132
3238
|
};
|
|
3133
3239
|
}
|
|
3134
3240
|
function isUpdateWriteError(err) {
|
|
3135
|
-
return err.message
|
|
3241
|
+
return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message.startsWith("NOT_FOUND") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
|
|
3136
3242
|
}
|
|
3137
3243
|
function isCreateWriteError(err) {
|
|
3138
|
-
return err.message
|
|
3244
|
+
return isSentinel(err.message, "AUTH_FAILED") || isSentinel(err.message, "PERMISSION_DENIED") || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
3139
3245
|
}
|
|
3140
3246
|
function handleUpsertError(err, id, context) {
|
|
3141
3247
|
if (!(err instanceof Error)) {
|
|
@@ -3465,7 +3571,7 @@ function isThreadResolved(status2) {
|
|
|
3465
3571
|
}
|
|
3466
3572
|
async function readJsonResponse(response) {
|
|
3467
3573
|
if (!response.ok) {
|
|
3468
|
-
throw
|
|
3574
|
+
throw httpError(response);
|
|
3469
3575
|
}
|
|
3470
3576
|
return response.json();
|
|
3471
3577
|
}
|
|
@@ -3552,19 +3658,35 @@ async function getPullRequestBuilds(context, cred, prId) {
|
|
|
3552
3658
|
isBlocking: null
|
|
3553
3659
|
}));
|
|
3554
3660
|
}
|
|
3661
|
+
var MAX_PR_DESCRIPTION_CHARS = 4e3;
|
|
3662
|
+
var DESCRIPTION_SEPARATOR = "\n\n";
|
|
3555
3663
|
function composeDescription(description, template) {
|
|
3556
|
-
if (description
|
|
3557
|
-
return
|
|
3558
|
-
|
|
3559
|
-
${template.content}`;
|
|
3560
|
-
}
|
|
3561
|
-
if (description !== void 0) {
|
|
3562
|
-
return description;
|
|
3563
|
-
}
|
|
3564
|
-
if (template !== null) {
|
|
3565
|
-
return template.content;
|
|
3664
|
+
if (description === void 0 && template === null) {
|
|
3665
|
+
return null;
|
|
3566
3666
|
}
|
|
3567
|
-
|
|
3667
|
+
const provided = description ?? "";
|
|
3668
|
+
const templateContent = template?.content ?? "";
|
|
3669
|
+
const separator = description !== void 0 && template !== null ? DESCRIPTION_SEPARATOR : "";
|
|
3670
|
+
const text = `${provided}${separator}${templateContent}`;
|
|
3671
|
+
return {
|
|
3672
|
+
text,
|
|
3673
|
+
providedChars: provided.length,
|
|
3674
|
+
separatorChars: separator.length,
|
|
3675
|
+
templateChars: templateContent.length,
|
|
3676
|
+
templatePath: template?.path ?? null,
|
|
3677
|
+
totalChars: text.length
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
function formatDescriptionOverflow(composed) {
|
|
3681
|
+
const overflow = composed.totalChars - MAX_PR_DESCRIPTION_CHARS;
|
|
3682
|
+
return `description is ${composed.totalChars} characters${formatContributions(composed)}, exceeding the Azure DevOps limit of ${MAX_PR_DESCRIPTION_CHARS} characters. Shorten the description by at least ${overflow} characters.`;
|
|
3683
|
+
}
|
|
3684
|
+
function formatContributions(composed) {
|
|
3685
|
+
if (composed.templatePath === null) return "";
|
|
3686
|
+
return ` (${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} from the repository pull request template ${composed.templatePath})`;
|
|
3687
|
+
}
|
|
3688
|
+
function describeDescriptionBudget(composed) {
|
|
3689
|
+
return `description: ${composed.providedChars} provided + ${composed.separatorChars} separator + ${composed.templateChars} template = ${composed.totalChars} characters (client limit ${MAX_PR_DESCRIPTION_CHARS})`;
|
|
3568
3690
|
}
|
|
3569
3691
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
3570
3692
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
@@ -3585,29 +3707,40 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3585
3707
|
const repository = await getRepository(context, repo, cred);
|
|
3586
3708
|
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
3587
3709
|
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3710
|
+
const composed = composeDescription(description, template);
|
|
3711
|
+
if (composed === null) {
|
|
3590
3712
|
throw new Error("DESCRIPTION_REQUIRED");
|
|
3591
3713
|
}
|
|
3714
|
+
if (composed.totalChars > MAX_PR_DESCRIPTION_CHARS) {
|
|
3715
|
+
throw new Error(`DESCRIPTION_TOO_LONG: ${formatDescriptionOverflow(composed)}`);
|
|
3716
|
+
}
|
|
3592
3717
|
const payload = {
|
|
3593
3718
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
3594
3719
|
targetRefName: "refs/heads/develop",
|
|
3595
3720
|
title,
|
|
3596
|
-
description:
|
|
3721
|
+
description: composed.text
|
|
3597
3722
|
};
|
|
3598
3723
|
const url = new URL(
|
|
3599
3724
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
3600
3725
|
);
|
|
3601
3726
|
url.searchParams.set("api-version", "7.1");
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3727
|
+
let data;
|
|
3728
|
+
try {
|
|
3729
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3730
|
+
method: "POST",
|
|
3731
|
+
headers: {
|
|
3732
|
+
...authHeaders(cred),
|
|
3733
|
+
"Content-Type": "application/json"
|
|
3734
|
+
},
|
|
3735
|
+
body: JSON.stringify(payload)
|
|
3736
|
+
});
|
|
3737
|
+
data = await readJsonResponse(response);
|
|
3738
|
+
} catch (err) {
|
|
3739
|
+
if (err instanceof Error && err.message.startsWith("HTTP_400")) {
|
|
3740
|
+
throw new Error(`${err.message} | ${describeDescriptionBudget(composed)}`, { cause: err });
|
|
3741
|
+
}
|
|
3742
|
+
throw err;
|
|
3743
|
+
}
|
|
3611
3744
|
return {
|
|
3612
3745
|
branch: sourceBranch,
|
|
3613
3746
|
targetBranch: "develop",
|
|
@@ -3745,7 +3878,7 @@ async function patchWorkItemRelations(context, cred, workItemId, operation) {
|
|
|
3745
3878
|
body: JSON.stringify([operation])
|
|
3746
3879
|
});
|
|
3747
3880
|
if (!response.ok) {
|
|
3748
|
-
throw
|
|
3881
|
+
throw httpError(response);
|
|
3749
3882
|
}
|
|
3750
3883
|
}
|
|
3751
3884
|
async function linkWorkItemToPullRequest(context, repo, cred, prId, workItemId) {
|
|
@@ -3794,8 +3927,8 @@ async function resolveReviewerIdentity(org, cred, input) {
|
|
|
3794
3927
|
headers: authHeaders(cred)
|
|
3795
3928
|
});
|
|
3796
3929
|
} catch (err) {
|
|
3797
|
-
if (err instanceof Error && err.message
|
|
3798
|
-
throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
|
|
3930
|
+
if (err instanceof Error && isSentinel(err.message, "AUTH_FAILED")) {
|
|
3931
|
+
throw new Error(withDetail("IDENTITY_SCOPE_MISSING", sentinelDetail(err.message, "AUTH_FAILED")), { cause: err });
|
|
3799
3932
|
}
|
|
3800
3933
|
throw err;
|
|
3801
3934
|
}
|
|
@@ -3853,7 +3986,7 @@ async function removePullRequestReviewer(context, repo, cred, prId, reviewerId)
|
|
|
3853
3986
|
headers: authHeaders(cred)
|
|
3854
3987
|
});
|
|
3855
3988
|
if (!response.ok) {
|
|
3856
|
-
throw
|
|
3989
|
+
throw httpError(response);
|
|
3857
3990
|
}
|
|
3858
3991
|
return { reviewer: existing, noop: false };
|
|
3859
3992
|
}
|
|
@@ -3892,7 +4025,7 @@ async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
|
|
|
3892
4025
|
throw err;
|
|
3893
4026
|
}
|
|
3894
4027
|
if (!response.ok) {
|
|
3895
|
-
throw
|
|
4028
|
+
throw httpError(response);
|
|
3896
4029
|
}
|
|
3897
4030
|
return response.text();
|
|
3898
4031
|
}
|
|
@@ -4027,7 +4160,7 @@ function writeError(message, exitCode = 1) {
|
|
|
4027
4160
|
}
|
|
4028
4161
|
function handlePrCommandError(err, context, mode = "read") {
|
|
4029
4162
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
4030
|
-
if (error.message
|
|
4163
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
4031
4164
|
const scopeLabel = mode === "write" ? "Code (Read & Write)" : "Code (Read)";
|
|
4032
4165
|
writeError(`Authentication failed. Check that your PAT is valid and has the "${scopeLabel}" scope.`, EXIT_NOT_PERMITTED);
|
|
4033
4166
|
const credentialHint = describeResolvedCredential();
|
|
@@ -4035,17 +4168,20 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4035
4168
|
process.stderr.write(` ${credentialHint}
|
|
4036
4169
|
`);
|
|
4037
4170
|
}
|
|
4171
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
4038
4172
|
return;
|
|
4039
4173
|
}
|
|
4040
|
-
if (error.message
|
|
4174
|
+
if (isSentinel(error.message, "IDENTITY_SCOPE_MISSING")) {
|
|
4041
4175
|
writeError(
|
|
4042
4176
|
'Could not resolve reviewer identity: your PAT is missing the "Identity (Read)" scope required by the Azure DevOps identities API (separate from Code scope).',
|
|
4043
4177
|
EXIT_NOT_PERMITTED
|
|
4044
4178
|
);
|
|
4179
|
+
writeErrorDetail(error.message, "IDENTITY_SCOPE_MISSING");
|
|
4045
4180
|
return;
|
|
4046
4181
|
}
|
|
4047
|
-
if (error.message
|
|
4182
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
4048
4183
|
writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
|
|
4184
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
4049
4185
|
return;
|
|
4050
4186
|
}
|
|
4051
4187
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -4057,7 +4193,12 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
4057
4193
|
return;
|
|
4058
4194
|
}
|
|
4059
4195
|
if (error.message.startsWith("HTTP_")) {
|
|
4060
|
-
|
|
4196
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
4197
|
+
writeError(`Azure DevOps request failed with ${sentinel}.`);
|
|
4198
|
+
if (detail !== null) {
|
|
4199
|
+
process.stderr.write(` ${detail}
|
|
4200
|
+
`);
|
|
4201
|
+
}
|
|
4061
4202
|
return;
|
|
4062
4203
|
}
|
|
4063
4204
|
writeError(error.message);
|
|
@@ -4232,6 +4373,23 @@ function createPrStatusCommand() {
|
|
|
4232
4373
|
});
|
|
4233
4374
|
return command;
|
|
4234
4375
|
}
|
|
4376
|
+
function handlePrOpenError(err, context) {
|
|
4377
|
+
const message = err instanceof Error ? err.message : "";
|
|
4378
|
+
if (message.startsWith("AMBIGUOUS_PRS:")) {
|
|
4379
|
+
const ids = message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4380
|
+
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
if (message === "DESCRIPTION_REQUIRED") {
|
|
4384
|
+
writeError("--description is required for pull request creation.");
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
4388
|
+
writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
|
|
4389
|
+
return;
|
|
4390
|
+
}
|
|
4391
|
+
handlePrCommandError(err, context, "write");
|
|
4392
|
+
}
|
|
4235
4393
|
function createPrOpenCommand() {
|
|
4236
4394
|
const command = new Command12("open");
|
|
4237
4395
|
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
@@ -4280,16 +4438,7 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
4280
4438
|
`
|
|
4281
4439
|
);
|
|
4282
4440
|
} catch (err) {
|
|
4283
|
-
|
|
4284
|
-
const ids = err.message.replace("AMBIGUOUS_PRS:", "").split(",").map((id) => `#${id}`).join(", ");
|
|
4285
|
-
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
4286
|
-
return;
|
|
4287
|
-
}
|
|
4288
|
-
if (err instanceof Error && err.message === "DESCRIPTION_REQUIRED") {
|
|
4289
|
-
writeError("--description is required for pull request creation.");
|
|
4290
|
-
return;
|
|
4291
|
-
}
|
|
4292
|
-
handlePrCommandError(err, context, "write");
|
|
4441
|
+
handlePrOpenError(err, context);
|
|
4293
4442
|
}
|
|
4294
4443
|
});
|
|
4295
4444
|
return command;
|
|
@@ -5098,7 +5247,7 @@ function withApiVersion(url) {
|
|
|
5098
5247
|
}
|
|
5099
5248
|
async function readJsonResponse2(response) {
|
|
5100
5249
|
if (!response.ok) {
|
|
5101
|
-
throw
|
|
5250
|
+
throw httpError(response);
|
|
5102
5251
|
}
|
|
5103
5252
|
return await response.json();
|
|
5104
5253
|
}
|
|
@@ -5374,7 +5523,7 @@ async function getRunLog(context, cred, buildId, logId) {
|
|
|
5374
5523
|
);
|
|
5375
5524
|
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
5376
5525
|
if (!response.ok) {
|
|
5377
|
-
throw
|
|
5526
|
+
throw httpError(response);
|
|
5378
5527
|
}
|
|
5379
5528
|
return response.text();
|
|
5380
5529
|
}
|
|
@@ -5390,12 +5539,14 @@ function writeError2(message) {
|
|
|
5390
5539
|
}
|
|
5391
5540
|
function handlePipelineError(err, context) {
|
|
5392
5541
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
5393
|
-
if (error.message
|
|
5542
|
+
if (isSentinel(error.message, "AUTH_FAILED")) {
|
|
5394
5543
|
writeError2('Authentication failed. Check that your credential is valid and has the "Build (Read)" scope.');
|
|
5544
|
+
writeErrorDetail(error.message, "AUTH_FAILED");
|
|
5395
5545
|
return;
|
|
5396
5546
|
}
|
|
5397
|
-
if (error.message
|
|
5547
|
+
if (isSentinel(error.message, "PERMISSION_DENIED")) {
|
|
5398
5548
|
writeError2(`Access denied. Your credential may lack pipeline permissions for project "${context?.project}".`);
|
|
5549
|
+
writeErrorDetail(error.message, "PERMISSION_DENIED");
|
|
5399
5550
|
return;
|
|
5400
5551
|
}
|
|
5401
5552
|
if (error.message === "NETWORK_ERROR") {
|
|
@@ -5407,7 +5558,12 @@ function handlePipelineError(err, context) {
|
|
|
5407
5558
|
return;
|
|
5408
5559
|
}
|
|
5409
5560
|
if (error.message.startsWith("HTTP_")) {
|
|
5410
|
-
|
|
5561
|
+
const { sentinel, detail } = splitSentinel(error.message);
|
|
5562
|
+
writeError2(`Azure DevOps request failed with ${sentinel}.`);
|
|
5563
|
+
if (detail !== null) {
|
|
5564
|
+
process.stderr.write(` ${detail}
|
|
5565
|
+
`);
|
|
5566
|
+
}
|
|
5411
5567
|
return;
|
|
5412
5568
|
}
|
|
5413
5569
|
writeError2(error.message);
|
|
@@ -6299,7 +6455,7 @@ function mapRelationType(raw) {
|
|
|
6299
6455
|
};
|
|
6300
6456
|
}
|
|
6301
6457
|
async function readJsonResponse3(response) {
|
|
6302
|
-
if (!response.ok) throw
|
|
6458
|
+
if (!response.ok) throw httpError(response);
|
|
6303
6459
|
return await response.json();
|
|
6304
6460
|
}
|
|
6305
6461
|
function parseTargetId(url) {
|
|
@@ -6347,7 +6503,7 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6347
6503
|
{ op: "add", path: "/relations/-", value: { rel: relType.referenceName, url: targetUrl } }
|
|
6348
6504
|
])
|
|
6349
6505
|
});
|
|
6350
|
-
if (!response.ok) throw
|
|
6506
|
+
if (!response.ok) throw httpError(response);
|
|
6351
6507
|
return { status: "added", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6352
6508
|
}
|
|
6353
6509
|
async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
@@ -6367,7 +6523,7 @@ async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
6367
6523
|
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
6368
6524
|
body: JSON.stringify([{ op: "remove", path: `/relations/${index}` }])
|
|
6369
6525
|
});
|
|
6370
|
-
if (!response.ok) throw
|
|
6526
|
+
if (!response.ok) throw httpError(response);
|
|
6371
6527
|
return { status: "removed", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
6372
6528
|
}
|
|
6373
6529
|
async function listWorkItemRelations(context, cred, id) {
|
|
@@ -6455,11 +6611,12 @@ function handleRelationError(err, id1) {
|
|
|
6455
6611
|
const target = id1 !== void 0 ? id1 : "unknown";
|
|
6456
6612
|
process.stderr.write(`Error: work item #${target} not found.
|
|
6457
6613
|
`);
|
|
6458
|
-
} else if (msg
|
|
6614
|
+
} else if (isSentinel(msg, "AUTH_FAILED")) {
|
|
6459
6615
|
process.stderr.write(
|
|
6460
6616
|
`Error: authentication failed. Check your PAT has Work Items \u2192 Read & Write scope.
|
|
6461
6617
|
`
|
|
6462
6618
|
);
|
|
6619
|
+
writeErrorDetail(msg, "AUTH_FAILED");
|
|
6463
6620
|
} else {
|
|
6464
6621
|
process.stderr.write(`Error: ${msg}
|
|
6465
6622
|
`);
|