azdo-cli 0.19.0-feature-037-api-error-surfacing.699 → 0.19.0-feature-038-pr-update.704
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +210 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -502,7 +502,9 @@ function describeFailureBody(body, contentType) {
|
|
|
502
502
|
return null;
|
|
503
503
|
}
|
|
504
504
|
const redacted = redactBody(trimmed) ?? trimmed;
|
|
505
|
-
|
|
505
|
+
const fields = renderErrorFields(redacted);
|
|
506
|
+
if (fields !== null) return truncateDetail(fields);
|
|
507
|
+
return truncateDetail(redactText(redacted).slice(0, MAX_RAW_BODY_CHARS));
|
|
506
508
|
}
|
|
507
509
|
function truncateDetail(detail) {
|
|
508
510
|
const collapsed = redactText(detail).replace(/\s+/g, " ").trim();
|
|
@@ -545,12 +547,12 @@ function writeTraceEntry(writer, url, init, response, responseBody) {
|
|
|
545
547
|
async function traceResponse(url, init, response) {
|
|
546
548
|
const writer = getActiveTraceWriter();
|
|
547
549
|
if (!writer) return null;
|
|
548
|
-
let responseBody =
|
|
550
|
+
let responseBody = null;
|
|
549
551
|
try {
|
|
550
552
|
responseBody = await response.clone().text();
|
|
551
553
|
} catch {
|
|
552
554
|
}
|
|
553
|
-
writeTraceEntry(writer, url, init, response, responseBody);
|
|
555
|
+
writeTraceEntry(writer, url, init, response, responseBody ?? "");
|
|
554
556
|
return responseBody;
|
|
555
557
|
}
|
|
556
558
|
async function fetchWithErrors(url, init) {
|
|
@@ -572,8 +574,9 @@ async function fetchWithErrors(url, init) {
|
|
|
572
574
|
if (response.status === 401) throw new Error(withDetail("AUTH_FAILED", failureDetail));
|
|
573
575
|
if (response.status === 403) throw new Error(withDetail("PERMISSION_DENIED", failureDetail));
|
|
574
576
|
if (response.status === 404) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
+
throw new Error(
|
|
578
|
+
failureDetail === null ? "NOT_FOUND" : `NOT_FOUND | url=${redactUrl(url)} | body=${failureDetail}`
|
|
579
|
+
);
|
|
577
580
|
}
|
|
578
581
|
if (contentType.toLowerCase().startsWith("text/html")) {
|
|
579
582
|
throw new Error("AUTH_FAILED");
|
|
@@ -3747,6 +3750,28 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3747
3750
|
pullRequest: mapPullRequest(context, repo, data)
|
|
3748
3751
|
};
|
|
3749
3752
|
}
|
|
3753
|
+
async function updatePullRequest(context, repo, cred, prId, fields) {
|
|
3754
|
+
if (fields.description !== void 0 && fields.description.length > MAX_PR_DESCRIPTION_CHARS) {
|
|
3755
|
+
const overflow = fields.description.length - MAX_PR_DESCRIPTION_CHARS;
|
|
3756
|
+
throw new Error(
|
|
3757
|
+
`DESCRIPTION_TOO_LONG: description is ${fields.description.length} characters, exceeding the Azure DevOps limit of ${MAX_PR_DESCRIPTION_CHARS} characters. Shorten the description by at least ${overflow} characters.`
|
|
3758
|
+
);
|
|
3759
|
+
}
|
|
3760
|
+
const url = new URL(
|
|
3761
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests/${prId}`
|
|
3762
|
+
);
|
|
3763
|
+
url.searchParams.set("api-version", "7.1");
|
|
3764
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3765
|
+
method: "PATCH",
|
|
3766
|
+
headers: {
|
|
3767
|
+
...authHeaders(cred),
|
|
3768
|
+
"Content-Type": "application/json"
|
|
3769
|
+
},
|
|
3770
|
+
body: JSON.stringify(fields)
|
|
3771
|
+
});
|
|
3772
|
+
const data = await readJsonResponse(response);
|
|
3773
|
+
return mapPullRequest(context, repo, data);
|
|
3774
|
+
}
|
|
3750
3775
|
async function getPullRequestThreads(context, repo, cred, prId) {
|
|
3751
3776
|
const url = new URL(
|
|
3752
3777
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
@@ -4104,6 +4129,27 @@ function writeContractError(line) {
|
|
|
4104
4129
|
process.exitCode = 1;
|
|
4105
4130
|
}
|
|
4106
4131
|
var EMPTY_BODY_ERROR = "Comment text must not be empty. Pass the text inline or use --file <path>.";
|
|
4132
|
+
var STDIN_PATH = "-";
|
|
4133
|
+
function readTextSource(file) {
|
|
4134
|
+
if (file === STDIN_PATH) {
|
|
4135
|
+
try {
|
|
4136
|
+
return readFileSync5(0, "utf-8");
|
|
4137
|
+
} catch {
|
|
4138
|
+
writeError("Cannot read standard input.");
|
|
4139
|
+
return null;
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
if (!existsSync5(file)) {
|
|
4143
|
+
writeError(`File not found: ${file}`);
|
|
4144
|
+
return null;
|
|
4145
|
+
}
|
|
4146
|
+
try {
|
|
4147
|
+
return readFileSync5(file, "utf-8");
|
|
4148
|
+
} catch {
|
|
4149
|
+
writeError(`Cannot read file: ${file}`);
|
|
4150
|
+
return null;
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4107
4153
|
function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
4108
4154
|
if (inline !== void 0 && file !== void 0) {
|
|
4109
4155
|
writeError("Cannot specify both inline text and --file.");
|
|
@@ -4111,16 +4157,11 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4111
4157
|
}
|
|
4112
4158
|
let body;
|
|
4113
4159
|
if (file !== void 0) {
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
return null;
|
|
4117
|
-
}
|
|
4118
|
-
try {
|
|
4119
|
-
body = readFileSync5(file, "utf-8");
|
|
4120
|
-
} catch {
|
|
4121
|
-
writeError(`Cannot read file: ${file}`);
|
|
4160
|
+
const read = readTextSource(file);
|
|
4161
|
+
if (read === null) {
|
|
4122
4162
|
return null;
|
|
4123
4163
|
}
|
|
4164
|
+
body = read;
|
|
4124
4165
|
} else if (inline !== void 0) {
|
|
4125
4166
|
body = inline;
|
|
4126
4167
|
} else {
|
|
@@ -4134,6 +4175,33 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4134
4175
|
}
|
|
4135
4176
|
return trimmed;
|
|
4136
4177
|
}
|
|
4178
|
+
function resolveOptionalTextInput(inline, file, flag) {
|
|
4179
|
+
if (inline !== void 0 && file !== void 0) {
|
|
4180
|
+
writeError(`Cannot specify both --${flag} and --${flag}-file.`);
|
|
4181
|
+
return null;
|
|
4182
|
+
}
|
|
4183
|
+
if (inline === void 0 && file === void 0) {
|
|
4184
|
+
return void 0;
|
|
4185
|
+
}
|
|
4186
|
+
const label = flag === "title" ? "Title" : "Description";
|
|
4187
|
+
const emptyMessage = `${label} must not be empty. Pass the text inline or use --${flag}-file <path>.`;
|
|
4188
|
+
let value;
|
|
4189
|
+
if (file !== void 0) {
|
|
4190
|
+
const read = readTextSource(file);
|
|
4191
|
+
if (read === null) {
|
|
4192
|
+
return null;
|
|
4193
|
+
}
|
|
4194
|
+
value = read;
|
|
4195
|
+
} else {
|
|
4196
|
+
value = inline;
|
|
4197
|
+
}
|
|
4198
|
+
const trimmed = value.trim();
|
|
4199
|
+
if (trimmed === "") {
|
|
4200
|
+
writeError(emptyMessage);
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4203
|
+
return trimmed;
|
|
4204
|
+
}
|
|
4137
4205
|
function parseNonNegativeInt(raw) {
|
|
4138
4206
|
if (!/^\d+$/.test(raw)) {
|
|
4139
4207
|
return null;
|
|
@@ -4394,6 +4462,9 @@ function createPrOpenCommand() {
|
|
|
4394
4462
|
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
4395
4463
|
"--description <description>",
|
|
4396
4464
|
"pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
|
|
4465
|
+
).option(
|
|
4466
|
+
"--description-file <path>",
|
|
4467
|
+
'read the description from a UTF-8 file instead of --description; "-" reads standard input'
|
|
4397
4468
|
).option("--json", "output JSON").action(async (options) => {
|
|
4398
4469
|
validateOrgProjectPair(options);
|
|
4399
4470
|
const title = options.title?.trim();
|
|
@@ -4401,8 +4472,25 @@ function createPrOpenCommand() {
|
|
|
4401
4472
|
writeError("--title is required for pull request creation.");
|
|
4402
4473
|
return;
|
|
4403
4474
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4475
|
+
if (options.description !== void 0 && options.descriptionFile !== void 0) {
|
|
4476
|
+
writeError("Cannot specify both --description and --description-file.");
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
let description;
|
|
4480
|
+
if (options.descriptionFile !== void 0) {
|
|
4481
|
+
const fromFile = readTextSource(options.descriptionFile);
|
|
4482
|
+
if (fromFile === null) {
|
|
4483
|
+
return;
|
|
4484
|
+
}
|
|
4485
|
+
description = fromFile.trim();
|
|
4486
|
+
if (description === "") {
|
|
4487
|
+
writeError("Description must not be empty. Pass the text inline or use --description-file <path>.");
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
} else {
|
|
4491
|
+
const trimmedDescription = options.description?.trim();
|
|
4492
|
+
description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
|
|
4493
|
+
}
|
|
4406
4494
|
let context;
|
|
4407
4495
|
try {
|
|
4408
4496
|
const resolved = await resolvePrCommandContext(options);
|
|
@@ -4442,6 +4530,112 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
4442
4530
|
});
|
|
4443
4531
|
return command;
|
|
4444
4532
|
}
|
|
4533
|
+
function handlePrUpdateError(err, context) {
|
|
4534
|
+
const message = err instanceof Error ? err.message : "";
|
|
4535
|
+
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
4536
|
+
writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
handlePrCommandError(err, context, "write");
|
|
4540
|
+
}
|
|
4541
|
+
function joinFieldNames(fields) {
|
|
4542
|
+
return fields.join(" and ");
|
|
4543
|
+
}
|
|
4544
|
+
async function runPrUpdate(options) {
|
|
4545
|
+
if (options.titleFile === STDIN_PATH && options.descriptionFile === STDIN_PATH) {
|
|
4546
|
+
writeError('Cannot read standard input twice; only one of --title-file and --description-file may be "-".');
|
|
4547
|
+
return;
|
|
4548
|
+
}
|
|
4549
|
+
const title = resolveOptionalTextInput(options.title, options.titleFile, "title");
|
|
4550
|
+
if (title === null) {
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
const description = resolveOptionalTextInput(options.description, options.descriptionFile, "description");
|
|
4554
|
+
if (description === null) {
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
if (title === void 0 && description === void 0) {
|
|
4558
|
+
writeError("pr update requires at least one of --title, --title-file, --description or --description-file.");
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
let context;
|
|
4562
|
+
try {
|
|
4563
|
+
const target = await resolvePullRequestTarget(options);
|
|
4564
|
+
if (target === null) {
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
context = target.context;
|
|
4568
|
+
const current = target.pullRequest;
|
|
4569
|
+
const fields = {};
|
|
4570
|
+
const updatedFields = [];
|
|
4571
|
+
const requestedFields = [];
|
|
4572
|
+
if (title !== void 0) {
|
|
4573
|
+
requestedFields.push("title");
|
|
4574
|
+
if (title !== current.title) {
|
|
4575
|
+
fields.title = title;
|
|
4576
|
+
updatedFields.push("title");
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
if (description !== void 0) {
|
|
4580
|
+
requestedFields.push("description");
|
|
4581
|
+
if (description !== (current.description ?? "")) {
|
|
4582
|
+
fields.description = description;
|
|
4583
|
+
updatedFields.push("description");
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
if (updatedFields.length === 0) {
|
|
4587
|
+
const noopResult = {
|
|
4588
|
+
pullRequestId: current.id,
|
|
4589
|
+
title: current.title,
|
|
4590
|
+
description: current.description ?? null,
|
|
4591
|
+
url: current.url,
|
|
4592
|
+
noop: true,
|
|
4593
|
+
updatedFields: []
|
|
4594
|
+
};
|
|
4595
|
+
if (options.json) {
|
|
4596
|
+
process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
|
|
4597
|
+
`);
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
process.stdout.write(
|
|
4601
|
+
`Pull request #${current.id} already has the requested ${joinFieldNames(requestedFields)}; nothing to update.
|
|
4602
|
+
`
|
|
4603
|
+
);
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
const updated = await updatePullRequest(target.context, target.repo, target.pat, current.id, fields);
|
|
4607
|
+
const result = {
|
|
4608
|
+
pullRequestId: updated.id,
|
|
4609
|
+
title: updated.title,
|
|
4610
|
+
description: updated.description ?? null,
|
|
4611
|
+
url: updated.url,
|
|
4612
|
+
noop: false,
|
|
4613
|
+
updatedFields
|
|
4614
|
+
};
|
|
4615
|
+
if (options.json) {
|
|
4616
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4617
|
+
`);
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
process.stdout.write(
|
|
4621
|
+
`Updated pull request #${updated.id} (${updatedFields.join(", ")}).
|
|
4622
|
+
${updated.url ?? "\u2014"}
|
|
4623
|
+
`
|
|
4624
|
+
);
|
|
4625
|
+
} catch (err) {
|
|
4626
|
+
handlePrUpdateError(err, context);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
function createPrUpdateCommand() {
|
|
4630
|
+
const command = new Command12("update");
|
|
4631
|
+
withCommonPrOptions(configureUnwrappedHelp(command)).alias("edit").description("Update a pull request's title and/or description").option("--pr-number <N>", PR_NUMBER_HELP).option("--title <title>", "new pull request title").option("--title-file <path>", 'read the new title from a UTF-8 file; "-" reads standard input').option(
|
|
4632
|
+
"--description <description>",
|
|
4633
|
+
'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
|
|
4634
|
+
).option("--description-file <path>", 'read the new description from a UTF-8 file; "-" reads standard input').option("--json", "output JSON").action(async (_options, command2) => {
|
|
4635
|
+
await runPrUpdate(mergedPrOptions(command2));
|
|
4636
|
+
});
|
|
4637
|
+
return command;
|
|
4638
|
+
}
|
|
4445
4639
|
function createPrCommentsCommand() {
|
|
4446
4640
|
const command = new Command12("comments");
|
|
4447
4641
|
withCommonPrOptions(configureUnwrappedHelp(command)).description("List pull request comment threads for the current branch").option("--pr-number <N>", PR_NUMBER_HELP).option("--hide-resolved", "hide threads whose status is resolved / won't fix / closed / by design").option("--exclude-resolved", "alias of --hide-resolved: exclude resolved / won't fix / closed / by design threads").option("--code-related-only", "show only threads anchored to a file/line; omit general discussion threads").option("--exclude-system", "omit Azure DevOps system comments (branch updates, reviewer votes, build events)").option("--max-chars <N>", "truncate each comment body to N characters (0 = no limit, the default)").option("--thread <id>", "show only the thread with this numeric id; fails when the pull request has no such thread").option("--contains <text>", "show only threads holding a comment that contains this literal, case-sensitive substring (matched before --max-chars truncates)").option("--json", "output JSON").action(async (options) => {
|
|
@@ -5221,6 +5415,7 @@ function createPrCommand() {
|
|
|
5221
5415
|
command.addCommand(createPrListCommand());
|
|
5222
5416
|
command.addCommand(createPrStatusCommand());
|
|
5223
5417
|
command.addCommand(createPrOpenCommand());
|
|
5418
|
+
command.addCommand(createPrUpdateCommand());
|
|
5224
5419
|
command.addCommand(createPrCommentsCommand());
|
|
5225
5420
|
command.addCommand(createPrCommentResolveCommand());
|
|
5226
5421
|
command.addCommand(createPrCommentReopenCommand());
|