azdo-cli 0.19.0-feature-037-api-error-surfacing.701 → 0.19.0-feature-038-pr-update.706
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 +11 -1
- package/dist/index.js +218 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
15
15
|
- Attach a local file to a work item, or remove a named attachment (`add-attachment`, `delete-attachment`)
|
|
16
16
|
- Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
|
|
17
17
|
- Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
|
|
18
|
-
- Check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
|
|
18
|
+
- Check branch pull request status, open PRs to `develop` (optionally pre-filled from a repository-defined template), update an existing PR's title or description (`pr update`), list PR comment threads for any PR (`--pr-number`), resolve/reopen threads, link/unlink work items, and add/remove required or optional reviewers — all from the CLI (`pr`)
|
|
19
|
+
- Feed long PR titles, descriptions and comment bodies from a file or a pipe instead of the shell (`--title-file`, `--description-file`, `--file`; `-` reads standard input)
|
|
19
20
|
- Persist org/project/default fields in local config (`config`)
|
|
20
21
|
- List all fields of a work item (`list-fields`)
|
|
21
22
|
- Authenticate per Azure DevOps organization with `azdo auth login` — OAuth (Microsoft Entra) by default, or a Personal Access Token via `--use-pat` (or the `AZDO_PAT` env var). Credentials are stored in the OS credential store. Inspect with `azdo auth status`, remove with `azdo auth logout`. Diagnose auth problems with `azdo auth diagnose`. See [docs/authentication.md](docs/authentication.md).
|
|
@@ -85,13 +86,22 @@ azdo pr comments edit 148 --file plan.md --pr-number 64 # rewrite it in p
|
|
|
85
86
|
azdo pr comments reply 148 "Great suggestion, I'll address it." # human-readable output
|
|
86
87
|
azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
|
|
87
88
|
azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
|
|
89
|
+
git log -1 --format=%B | azdo pr comments add --file - --pr-number 64 # "-" reads standard input
|
|
88
90
|
|
|
89
91
|
# Open a pull request — description from a repo template when you don't pass one
|
|
90
92
|
azdo pr open --title "Fix the thing" --description "Because X was broken"
|
|
91
93
|
azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
|
|
94
|
+
azdo pr open --title "Fix the thing" --description-file body.md # or --description-file - to pipe it in
|
|
92
95
|
# The description plus the template must stay within Azure DevOps' 4000-character cap; over it,
|
|
93
96
|
# the command says by how much and creates nothing. See docs/commands.md.
|
|
94
97
|
|
|
98
|
+
# Fix a title or description after the fact — a PR is no longer write-once
|
|
99
|
+
azdo pr update --pr-number 64 --title "A real title" # description untouched
|
|
100
|
+
azdo pr update --pr-number 64 --description-file body.md # replaces literally; no template is prepended
|
|
101
|
+
cat body.md | azdo pr edit --pr-number 64 --description-file - # "edit" is an alias for "update"
|
|
102
|
+
azdo pr update --pr-number 64 --title "A real title" --json # { pullRequestId, title, description, url, noop, updatedFields }
|
|
103
|
+
# Re-running with the values it already has writes nothing and reports "noop": true.
|
|
104
|
+
|
|
95
105
|
# Link/unlink a work item, add/remove reviewers
|
|
96
106
|
azdo pr work-items link 1234 --pr-number 64
|
|
97
107
|
azdo pr work-items unlink 1234 --pr-number 64
|
package/dist/index.js
CHANGED
|
@@ -3750,6 +3750,28 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3750
3750
|
pullRequest: mapPullRequest(context, repo, data)
|
|
3751
3751
|
};
|
|
3752
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
|
+
}
|
|
3753
3775
|
async function getPullRequestThreads(context, repo, cred, prId) {
|
|
3754
3776
|
const url = new URL(
|
|
3755
3777
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/threads`
|
|
@@ -4107,6 +4129,27 @@ function writeContractError(line) {
|
|
|
4107
4129
|
process.exitCode = 1;
|
|
4108
4130
|
}
|
|
4109
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
|
+
}
|
|
4110
4153
|
function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
4111
4154
|
if (inline !== void 0 && file !== void 0) {
|
|
4112
4155
|
writeError("Cannot specify both inline text and --file.");
|
|
@@ -4114,16 +4157,11 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4114
4157
|
}
|
|
4115
4158
|
let body;
|
|
4116
4159
|
if (file !== void 0) {
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
return null;
|
|
4120
|
-
}
|
|
4121
|
-
try {
|
|
4122
|
-
body = readFileSync5(file, "utf-8");
|
|
4123
|
-
} catch {
|
|
4124
|
-
writeError(`Cannot read file: ${file}`);
|
|
4160
|
+
const read = readTextSource(file);
|
|
4161
|
+
if (read === null) {
|
|
4125
4162
|
return null;
|
|
4126
4163
|
}
|
|
4164
|
+
body = read;
|
|
4127
4165
|
} else if (inline !== void 0) {
|
|
4128
4166
|
body = inline;
|
|
4129
4167
|
} else {
|
|
@@ -4137,6 +4175,33 @@ function resolveCommentBody(inline, file, emptyMessage = EMPTY_BODY_ERROR) {
|
|
|
4137
4175
|
}
|
|
4138
4176
|
return trimmed;
|
|
4139
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
|
+
}
|
|
4140
4205
|
function parseNonNegativeInt(raw) {
|
|
4141
4206
|
if (!/^\d+$/.test(raw)) {
|
|
4142
4207
|
return null;
|
|
@@ -4392,11 +4457,34 @@ function handlePrOpenError(err, context) {
|
|
|
4392
4457
|
}
|
|
4393
4458
|
handlePrCommandError(err, context, "write");
|
|
4394
4459
|
}
|
|
4460
|
+
function resolveOpenDescription(inline, file) {
|
|
4461
|
+
if (inline !== void 0 && file !== void 0) {
|
|
4462
|
+
writeError("Cannot specify both --description and --description-file.");
|
|
4463
|
+
return null;
|
|
4464
|
+
}
|
|
4465
|
+
if (file !== void 0) {
|
|
4466
|
+
const fromFile = readTextSource(file);
|
|
4467
|
+
if (fromFile === null) {
|
|
4468
|
+
return null;
|
|
4469
|
+
}
|
|
4470
|
+
const trimmed = fromFile.trim();
|
|
4471
|
+
if (trimmed === "") {
|
|
4472
|
+
writeError("Description must not be empty. Pass the text inline or use --description-file <path>.");
|
|
4473
|
+
return null;
|
|
4474
|
+
}
|
|
4475
|
+
return trimmed;
|
|
4476
|
+
}
|
|
4477
|
+
const trimmedInline = inline?.trim();
|
|
4478
|
+
return trimmedInline ? trimmedInline : void 0;
|
|
4479
|
+
}
|
|
4395
4480
|
function createPrOpenCommand() {
|
|
4396
4481
|
const command = new Command12("open");
|
|
4397
4482
|
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
4398
4483
|
"--description <description>",
|
|
4399
4484
|
"pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
|
|
4485
|
+
).option(
|
|
4486
|
+
"--description-file <path>",
|
|
4487
|
+
'read the description from a UTF-8 file instead of --description; "-" reads standard input'
|
|
4400
4488
|
).option("--json", "output JSON").action(async (options) => {
|
|
4401
4489
|
validateOrgProjectPair(options);
|
|
4402
4490
|
const title = options.title?.trim();
|
|
@@ -4404,8 +4492,10 @@ function createPrOpenCommand() {
|
|
|
4404
4492
|
writeError("--title is required for pull request creation.");
|
|
4405
4493
|
return;
|
|
4406
4494
|
}
|
|
4407
|
-
const
|
|
4408
|
-
|
|
4495
|
+
const description = resolveOpenDescription(options.description, options.descriptionFile);
|
|
4496
|
+
if (description === null) {
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4409
4499
|
let context;
|
|
4410
4500
|
try {
|
|
4411
4501
|
const resolved = await resolvePrCommandContext(options);
|
|
@@ -4445,6 +4535,123 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
4445
4535
|
});
|
|
4446
4536
|
return command;
|
|
4447
4537
|
}
|
|
4538
|
+
function handlePrUpdateError(err, context) {
|
|
4539
|
+
const message = err instanceof Error ? err.message : "";
|
|
4540
|
+
if (message.startsWith("DESCRIPTION_TOO_LONG: ")) {
|
|
4541
|
+
writeError(message.slice("DESCRIPTION_TOO_LONG: ".length));
|
|
4542
|
+
return;
|
|
4543
|
+
}
|
|
4544
|
+
handlePrCommandError(err, context, "write");
|
|
4545
|
+
}
|
|
4546
|
+
function joinFieldNames(fields) {
|
|
4547
|
+
return fields.join(" and ");
|
|
4548
|
+
}
|
|
4549
|
+
function resolveRequestedFields(options) {
|
|
4550
|
+
if (options.titleFile === STDIN_PATH && options.descriptionFile === STDIN_PATH) {
|
|
4551
|
+
writeError('Cannot read standard input twice; only one of --title-file and --description-file may be "-".');
|
|
4552
|
+
return null;
|
|
4553
|
+
}
|
|
4554
|
+
const title = resolveOptionalTextInput(options.title, options.titleFile, "title");
|
|
4555
|
+
if (title === null) {
|
|
4556
|
+
return null;
|
|
4557
|
+
}
|
|
4558
|
+
const description = resolveOptionalTextInput(options.description, options.descriptionFile, "description");
|
|
4559
|
+
if (description === null) {
|
|
4560
|
+
return null;
|
|
4561
|
+
}
|
|
4562
|
+
if (title === void 0 && description === void 0) {
|
|
4563
|
+
writeError("pr update requires at least one of --title, --title-file, --description or --description-file.");
|
|
4564
|
+
return null;
|
|
4565
|
+
}
|
|
4566
|
+
return { title, description };
|
|
4567
|
+
}
|
|
4568
|
+
function diffRequestedFields(requested, current) {
|
|
4569
|
+
const fields = {};
|
|
4570
|
+
const updatedFields = [];
|
|
4571
|
+
const requestedFields = [];
|
|
4572
|
+
if (requested.title !== void 0) {
|
|
4573
|
+
requestedFields.push("title");
|
|
4574
|
+
if (requested.title !== current.title) {
|
|
4575
|
+
fields.title = requested.title;
|
|
4576
|
+
updatedFields.push("title");
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
if (requested.description !== void 0) {
|
|
4580
|
+
requestedFields.push("description");
|
|
4581
|
+
if (requested.description !== (current.description ?? "")) {
|
|
4582
|
+
fields.description = requested.description;
|
|
4583
|
+
updatedFields.push("description");
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return { fields, updatedFields, requestedFields };
|
|
4587
|
+
}
|
|
4588
|
+
async function runPrUpdate(options) {
|
|
4589
|
+
const requested = resolveRequestedFields(options);
|
|
4590
|
+
if (requested === null) {
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
let context;
|
|
4594
|
+
try {
|
|
4595
|
+
const target = await resolvePullRequestTarget(options);
|
|
4596
|
+
if (target === null) {
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4599
|
+
context = target.context;
|
|
4600
|
+
const current = target.pullRequest;
|
|
4601
|
+
const { fields, updatedFields, requestedFields } = diffRequestedFields(requested, current);
|
|
4602
|
+
if (updatedFields.length === 0) {
|
|
4603
|
+
const noopResult = {
|
|
4604
|
+
pullRequestId: current.id,
|
|
4605
|
+
title: current.title,
|
|
4606
|
+
description: current.description ?? null,
|
|
4607
|
+
url: current.url,
|
|
4608
|
+
noop: true,
|
|
4609
|
+
updatedFields: []
|
|
4610
|
+
};
|
|
4611
|
+
if (options.json) {
|
|
4612
|
+
process.stdout.write(`${JSON.stringify(noopResult, null, 2)}
|
|
4613
|
+
`);
|
|
4614
|
+
return;
|
|
4615
|
+
}
|
|
4616
|
+
process.stdout.write(
|
|
4617
|
+
`Pull request #${current.id} already has the requested ${joinFieldNames(requestedFields)}; nothing to update.
|
|
4618
|
+
`
|
|
4619
|
+
);
|
|
4620
|
+
return;
|
|
4621
|
+
}
|
|
4622
|
+
const updated = await updatePullRequest(target.context, target.repo, target.pat, current.id, fields);
|
|
4623
|
+
const result = {
|
|
4624
|
+
pullRequestId: updated.id,
|
|
4625
|
+
title: updated.title,
|
|
4626
|
+
description: updated.description ?? null,
|
|
4627
|
+
url: updated.url,
|
|
4628
|
+
noop: false,
|
|
4629
|
+
updatedFields
|
|
4630
|
+
};
|
|
4631
|
+
if (options.json) {
|
|
4632
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4633
|
+
`);
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
process.stdout.write(
|
|
4637
|
+
`Updated pull request #${updated.id} (${updatedFields.join(", ")}).
|
|
4638
|
+
${updated.url ?? "\u2014"}
|
|
4639
|
+
`
|
|
4640
|
+
);
|
|
4641
|
+
} catch (err) {
|
|
4642
|
+
handlePrUpdateError(err, context);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
function createPrUpdateCommand() {
|
|
4646
|
+
const command = new Command12("update");
|
|
4647
|
+
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(
|
|
4648
|
+
"--description <description>",
|
|
4649
|
+
'new pull request description; REPLACES the existing description literally \u2014 no repository pull request template is looked up or prepended, unlike "azdo pr open"'
|
|
4650
|
+
).option("--description-file <path>", 'read the new description from a UTF-8 file; "-" reads standard input').option("--json", "output JSON").action(async (_options, command2) => {
|
|
4651
|
+
await runPrUpdate(mergedPrOptions(command2));
|
|
4652
|
+
});
|
|
4653
|
+
return command;
|
|
4654
|
+
}
|
|
4448
4655
|
function createPrCommentsCommand() {
|
|
4449
4656
|
const command = new Command12("comments");
|
|
4450
4657
|
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) => {
|
|
@@ -5224,6 +5431,7 @@ function createPrCommand() {
|
|
|
5224
5431
|
command.addCommand(createPrListCommand());
|
|
5225
5432
|
command.addCommand(createPrStatusCommand());
|
|
5226
5433
|
command.addCommand(createPrOpenCommand());
|
|
5434
|
+
command.addCommand(createPrUpdateCommand());
|
|
5227
5435
|
command.addCommand(createPrCommentsCommand());
|
|
5228
5436
|
command.addCommand(createPrCommentResolveCommand());
|
|
5229
5437
|
command.addCommand(createPrCommentReopenCommand());
|