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.
Files changed (2) hide show
  1. package/dist/index.js +210 -15
  2. 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
- return truncateDetail(renderErrorFields(redacted) ?? redacted.slice(0, MAX_RAW_BODY_CHARS));
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
- const body = failureBodies.get(response);
576
- throw new Error(body === void 0 ? "NOT_FOUND" : `NOT_FOUND | url=${url} | body=${body}`);
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
- if (!existsSync5(file)) {
4115
- writeError(`File not found: ${file}`);
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
- const trimmedDescription = options.description?.trim();
4405
- const description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azdo-cli",
3
- "version": "0.19.0-feature-037-api-error-surfacing.699",
3
+ "version": "0.19.0-feature-038-pr-update.704",
4
4
  "description": "Azure DevOps CLI tool",
5
5
  "type": "module",
6
6
  "bin": {