azdo-cli 0.16.0-develop.606 → 0.16.0-develop.628
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 +12 -1
- package/dist/index.js +461 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
14
14
|
- Read and post work item comments (`comments`)
|
|
15
15
|
- Read/write rich-text fields as markdown (`get-md-field`, `set-md-field`)
|
|
16
16
|
- Download images embedded in rich-text fields, optionally resized for LLM use (`get-item`/`get-md-field` `--download-images`, `--resize-images`)
|
|
17
|
-
- Check branch pull request status, open PRs to `develop
|
|
17
|
+
- 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
18
|
- Persist org/project/default fields in local config (`config`)
|
|
19
19
|
- List all fields of a work item (`list-fields`)
|
|
20
20
|
- 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).
|
|
@@ -79,6 +79,17 @@ azdo pr comments reply 148 "Great suggestion, I'll address it." # human
|
|
|
79
79
|
azdo pr comments reply 148 "Done." --pr-number 64 --json # JSON: { pullRequestId, threadId, commentId, content }
|
|
80
80
|
azdo pr comment-reply 148 "Done." --pr-number 64 # flat alias, identical behaviour
|
|
81
81
|
|
|
82
|
+
# Open a pull request — description from a repo template when you don't pass one
|
|
83
|
+
azdo pr open --title "Fix the thing" --description "Because X was broken"
|
|
84
|
+
azdo pr open --title "Fix the thing" # uses docs/pull_request_template[/branches/<branch>].md if present
|
|
85
|
+
|
|
86
|
+
# Link/unlink a work item, add/remove reviewers
|
|
87
|
+
azdo pr work-items link 1234 --pr-number 64
|
|
88
|
+
azdo pr work-items unlink 1234 --pr-number 64
|
|
89
|
+
azdo pr reviewers add jane@example.com --pr-number 64 # optional by default
|
|
90
|
+
azdo pr reviewers add jane@example.com --pr-number 64 --required # required (or promotes in place)
|
|
91
|
+
azdo pr reviewers remove jane@example.com --pr-number 64
|
|
92
|
+
|
|
82
93
|
# Any pr subcommand can target another repository
|
|
83
94
|
azdo pr comments --repo other-repo --pr-number 12
|
|
84
95
|
|
package/dist/index.js
CHANGED
|
@@ -1798,7 +1798,7 @@ async function runConnectivityTest(org, cred) {
|
|
|
1798
1798
|
return { status: "failed", error };
|
|
1799
1799
|
}
|
|
1800
1800
|
async function resolveCredentialIdentity(org, cred) {
|
|
1801
|
-
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/connectionData?api-version=7.1`;
|
|
1801
|
+
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/connectionData?api-version=7.1-preview`;
|
|
1802
1802
|
try {
|
|
1803
1803
|
const result = await fetchRaw(url, { headers: authHeaders(cred) });
|
|
1804
1804
|
if (result.status < 200 || result.status >= 300) {
|
|
@@ -3511,6 +3511,20 @@ async function getPullRequestBuilds(context, cred, prId) {
|
|
|
3511
3511
|
isBlocking: null
|
|
3512
3512
|
}));
|
|
3513
3513
|
}
|
|
3514
|
+
function composeDescription(description, template) {
|
|
3515
|
+
if (description !== void 0 && template !== null) {
|
|
3516
|
+
return `${description}
|
|
3517
|
+
|
|
3518
|
+
${template.content}`;
|
|
3519
|
+
}
|
|
3520
|
+
if (description !== void 0) {
|
|
3521
|
+
return description;
|
|
3522
|
+
}
|
|
3523
|
+
if (template !== null) {
|
|
3524
|
+
return template.content;
|
|
3525
|
+
}
|
|
3526
|
+
return null;
|
|
3527
|
+
}
|
|
3514
3528
|
async function openPullRequest(context, repo, cred, sourceBranch, title, description) {
|
|
3515
3529
|
const existing = await listPullRequests(context, repo, cred, sourceBranch, {
|
|
3516
3530
|
status: "active",
|
|
@@ -3527,11 +3541,18 @@ async function openPullRequest(context, repo, cred, sourceBranch, title, descrip
|
|
|
3527
3541
|
if (existing.length > 1) {
|
|
3528
3542
|
throw new Error(`AMBIGUOUS_PRS:${existing.map((pullRequest) => pullRequest.id).join(",")}`);
|
|
3529
3543
|
}
|
|
3544
|
+
const repository = await getRepository(context, repo, cred);
|
|
3545
|
+
const defaultBranch = repository.defaultBranch ? repository.defaultBranch.replace(/^refs\/heads\//, "") : "develop";
|
|
3546
|
+
const template = await resolvePullRequestTemplate(context, repo, cred, defaultBranch, "develop");
|
|
3547
|
+
const finalDescription = composeDescription(description, template);
|
|
3548
|
+
if (finalDescription === null) {
|
|
3549
|
+
throw new Error("DESCRIPTION_REQUIRED");
|
|
3550
|
+
}
|
|
3530
3551
|
const payload = {
|
|
3531
3552
|
sourceRefName: `refs/heads/${sourceBranch}`,
|
|
3532
3553
|
targetRefName: "refs/heads/develop",
|
|
3533
3554
|
title,
|
|
3534
|
-
description
|
|
3555
|
+
description: finalDescription
|
|
3535
3556
|
};
|
|
3536
3557
|
const url = new URL(
|
|
3537
3558
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullrequests`
|
|
@@ -3638,6 +3659,245 @@ async function postThreadComment(context, repo, cred, prId, threadId, content) {
|
|
|
3638
3659
|
publishedAt: data.publishedDate ?? null
|
|
3639
3660
|
};
|
|
3640
3661
|
}
|
|
3662
|
+
function buildRepositoryUrl(context, repo) {
|
|
3663
|
+
const url = new URL(
|
|
3664
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}`
|
|
3665
|
+
);
|
|
3666
|
+
url.searchParams.set("api-version", "7.1");
|
|
3667
|
+
return url;
|
|
3668
|
+
}
|
|
3669
|
+
async function getRepository(context, repo, cred) {
|
|
3670
|
+
const response = await fetchWithErrors(buildRepositoryUrl(context, repo).toString(), {
|
|
3671
|
+
headers: authHeaders(cred)
|
|
3672
|
+
});
|
|
3673
|
+
return readJsonResponse(response);
|
|
3674
|
+
}
|
|
3675
|
+
async function resolveRepositoryId(context, repo, cred) {
|
|
3676
|
+
const repository = await getRepository(context, repo, cred);
|
|
3677
|
+
return repository.id;
|
|
3678
|
+
}
|
|
3679
|
+
function buildWorkItemArtifactUri(projectId, repositoryId, prId) {
|
|
3680
|
+
return `vstfs:///Git/PullRequestId/${projectId}/${repositoryId}/${prId}`;
|
|
3681
|
+
}
|
|
3682
|
+
function buildWorkItemUrl2(context, workItemId) {
|
|
3683
|
+
const url = new URL(
|
|
3684
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${workItemId}`
|
|
3685
|
+
);
|
|
3686
|
+
url.searchParams.set("api-version", "7.1");
|
|
3687
|
+
return url;
|
|
3688
|
+
}
|
|
3689
|
+
async function getWorkItemRelations(context, cred, workItemId) {
|
|
3690
|
+
const url = buildWorkItemUrl2(context, workItemId);
|
|
3691
|
+
url.searchParams.set("$expand", "relations");
|
|
3692
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3693
|
+
const data = await readJsonResponse(response);
|
|
3694
|
+
return data.relations ?? [];
|
|
3695
|
+
}
|
|
3696
|
+
async function patchWorkItemRelations(context, cred, workItemId, operation) {
|
|
3697
|
+
const url = buildWorkItemUrl2(context, workItemId);
|
|
3698
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
3699
|
+
method: "PATCH",
|
|
3700
|
+
headers: {
|
|
3701
|
+
...authHeaders(cred),
|
|
3702
|
+
"Content-Type": "application/json-patch+json"
|
|
3703
|
+
},
|
|
3704
|
+
body: JSON.stringify([operation])
|
|
3705
|
+
});
|
|
3706
|
+
if (!response.ok) {
|
|
3707
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
async function linkWorkItemToPullRequest(context, repo, cred, prId, workItemId) {
|
|
3711
|
+
const [projectId, repositoryId, relations] = await Promise.all([
|
|
3712
|
+
resolveProjectId(context, cred),
|
|
3713
|
+
resolveRepositoryId(context, repo, cred),
|
|
3714
|
+
getWorkItemRelations(context, cred, workItemId)
|
|
3715
|
+
]);
|
|
3716
|
+
const uri = buildWorkItemArtifactUri(projectId, repositoryId, prId);
|
|
3717
|
+
const alreadyLinked = relations.some((relation) => relation.rel === "ArtifactLink" && relation.url === uri);
|
|
3718
|
+
if (alreadyLinked) {
|
|
3719
|
+
return { pullRequestId: prId, workItemId, url: uri, noop: true };
|
|
3720
|
+
}
|
|
3721
|
+
await patchWorkItemRelations(context, cred, workItemId, {
|
|
3722
|
+
op: "add",
|
|
3723
|
+
path: "/relations/-",
|
|
3724
|
+
value: { rel: "ArtifactLink", url: uri, attributes: { name: "Pull Request" } }
|
|
3725
|
+
});
|
|
3726
|
+
return { pullRequestId: prId, workItemId, url: uri, noop: false };
|
|
3727
|
+
}
|
|
3728
|
+
async function unlinkWorkItemFromPullRequest(context, repo, cred, prId, workItemId) {
|
|
3729
|
+
const [projectId, repositoryId, relations] = await Promise.all([
|
|
3730
|
+
resolveProjectId(context, cred),
|
|
3731
|
+
resolveRepositoryId(context, repo, cred),
|
|
3732
|
+
getWorkItemRelations(context, cred, workItemId)
|
|
3733
|
+
]);
|
|
3734
|
+
const uri = buildWorkItemArtifactUri(projectId, repositoryId, prId);
|
|
3735
|
+
const index = relations.findIndex((relation) => relation.rel === "ArtifactLink" && relation.url === uri);
|
|
3736
|
+
if (index === -1) {
|
|
3737
|
+
return { pullRequestId: prId, workItemId, url: uri, noop: true };
|
|
3738
|
+
}
|
|
3739
|
+
await patchWorkItemRelations(context, cred, workItemId, { op: "remove", path: `/relations/${index}` });
|
|
3740
|
+
return { pullRequestId: prId, workItemId, url: uri, noop: false };
|
|
3741
|
+
}
|
|
3742
|
+
function buildIdentitiesUrl(org, filterValue) {
|
|
3743
|
+
const url = new URL(`https://vssps.dev.azure.com/${encodeURIComponent(org)}/_apis/identities`);
|
|
3744
|
+
url.searchParams.set("searchFilter", "General");
|
|
3745
|
+
url.searchParams.set("filterValue", filterValue);
|
|
3746
|
+
url.searchParams.set("api-version", "7.1");
|
|
3747
|
+
return url;
|
|
3748
|
+
}
|
|
3749
|
+
async function resolveReviewerIdentity(org, cred, input) {
|
|
3750
|
+
let response;
|
|
3751
|
+
try {
|
|
3752
|
+
response = await fetchWithErrors(buildIdentitiesUrl(org, input).toString(), {
|
|
3753
|
+
headers: authHeaders(cred)
|
|
3754
|
+
});
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
if (err instanceof Error && err.message === "AUTH_FAILED") {
|
|
3757
|
+
throw new Error("IDENTITY_SCOPE_MISSING", { cause: err });
|
|
3758
|
+
}
|
|
3759
|
+
throw err;
|
|
3760
|
+
}
|
|
3761
|
+
const data = await readJsonResponse(response);
|
|
3762
|
+
if (data.value.length !== 1) {
|
|
3763
|
+
throw new Error(`RESOLVE_FAILED:${input}`);
|
|
3764
|
+
}
|
|
3765
|
+
return data.value[0];
|
|
3766
|
+
}
|
|
3767
|
+
function buildPullRequestReviewerUrl(context, repo, prId, reviewerId) {
|
|
3768
|
+
const url = new URL(
|
|
3769
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/reviewers/${encodeURIComponent(reviewerId)}`
|
|
3770
|
+
);
|
|
3771
|
+
url.searchParams.set("api-version", "7.1");
|
|
3772
|
+
return url;
|
|
3773
|
+
}
|
|
3774
|
+
function mapReviewer(data) {
|
|
3775
|
+
return {
|
|
3776
|
+
id: data.id,
|
|
3777
|
+
displayName: data.displayName ?? null,
|
|
3778
|
+
uniqueName: data.uniqueName ?? null,
|
|
3779
|
+
isRequired: data.isRequired ?? false,
|
|
3780
|
+
vote: data.vote ?? 0
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
async function getPullRequestReviewers(context, repo, cred, prId) {
|
|
3784
|
+
const url = new URL(
|
|
3785
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/pullRequests/${prId}/reviewers`
|
|
3786
|
+
);
|
|
3787
|
+
url.searchParams.set("api-version", "7.1");
|
|
3788
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
3789
|
+
const data = await readJsonResponse(response);
|
|
3790
|
+
return data.value.map(mapReviewer);
|
|
3791
|
+
}
|
|
3792
|
+
async function addOrUpdatePullRequestReviewer(context, repo, cred, prId, reviewerId, isRequired) {
|
|
3793
|
+
const response = await fetchWithErrors(buildPullRequestReviewerUrl(context, repo, prId, reviewerId).toString(), {
|
|
3794
|
+
method: "PUT",
|
|
3795
|
+
headers: {
|
|
3796
|
+
...authHeaders(cred),
|
|
3797
|
+
"Content-Type": "application/json"
|
|
3798
|
+
},
|
|
3799
|
+
body: JSON.stringify({ vote: 0, isRequired })
|
|
3800
|
+
});
|
|
3801
|
+
const data = await readJsonResponse(response);
|
|
3802
|
+
return mapReviewer(data);
|
|
3803
|
+
}
|
|
3804
|
+
async function removePullRequestReviewer(context, repo, cred, prId, reviewerId) {
|
|
3805
|
+
const reviewers = await getPullRequestReviewers(context, repo, cred, prId);
|
|
3806
|
+
const existing = reviewers.find((reviewer) => reviewer.id === reviewerId);
|
|
3807
|
+
if (existing === void 0) {
|
|
3808
|
+
return { reviewer: null, noop: true };
|
|
3809
|
+
}
|
|
3810
|
+
const response = await fetchWithErrors(buildPullRequestReviewerUrl(context, repo, prId, reviewerId).toString(), {
|
|
3811
|
+
method: "DELETE",
|
|
3812
|
+
headers: authHeaders(cred)
|
|
3813
|
+
});
|
|
3814
|
+
if (!response.ok) {
|
|
3815
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3816
|
+
}
|
|
3817
|
+
return { reviewer: existing, noop: false };
|
|
3818
|
+
}
|
|
3819
|
+
var TEMPLATE_ROOTS = [".azuredevops", ".vsts", "docs", ""];
|
|
3820
|
+
var TEMPLATE_EXTENSIONS = [".md", ".txt"];
|
|
3821
|
+
function joinTemplatePath(root, relative) {
|
|
3822
|
+
return root === "" ? relative : `${root}/${relative}`;
|
|
3823
|
+
}
|
|
3824
|
+
function branchTemplateSegments(branch) {
|
|
3825
|
+
const parts = branch.split("/").filter((part) => part.length > 0).slice(0, 10);
|
|
3826
|
+
const segments = [];
|
|
3827
|
+
for (let i = parts.length; i > 0; i -= 1) {
|
|
3828
|
+
segments.push(parts.slice(0, i).join("/"));
|
|
3829
|
+
}
|
|
3830
|
+
return segments;
|
|
3831
|
+
}
|
|
3832
|
+
async function fetchRepositoryItemContent(context, repo, cred, path3, branch) {
|
|
3833
|
+
const url = new URL(
|
|
3834
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/git/repositories/${encodeURIComponent(repo)}/items`
|
|
3835
|
+
);
|
|
3836
|
+
url.searchParams.set("path", path3);
|
|
3837
|
+
url.searchParams.set("versionDescriptor.version", branch);
|
|
3838
|
+
url.searchParams.set("versionDescriptor.versionType", "branch");
|
|
3839
|
+
url.searchParams.set("includeContent", "true");
|
|
3840
|
+
url.searchParams.set("$format", "text");
|
|
3841
|
+
url.searchParams.set("api-version", "7.1");
|
|
3842
|
+
let response;
|
|
3843
|
+
try {
|
|
3844
|
+
response = await fetchWithErrors(url.toString(), {
|
|
3845
|
+
headers: { ...authHeaders(cred), Accept: "text/plain" }
|
|
3846
|
+
});
|
|
3847
|
+
} catch (err) {
|
|
3848
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
3849
|
+
return null;
|
|
3850
|
+
}
|
|
3851
|
+
throw err;
|
|
3852
|
+
}
|
|
3853
|
+
if (!response.ok) {
|
|
3854
|
+
throw new Error(`HTTP_${response.status}`);
|
|
3855
|
+
}
|
|
3856
|
+
return response.text();
|
|
3857
|
+
}
|
|
3858
|
+
function branchTemplateCandidates(targetBranch) {
|
|
3859
|
+
const paths = [];
|
|
3860
|
+
for (const segment of branchTemplateSegments(targetBranch)) {
|
|
3861
|
+
for (const root of TEMPLATE_ROOTS) {
|
|
3862
|
+
for (const ext of TEMPLATE_EXTENSIONS) {
|
|
3863
|
+
paths.push(joinTemplatePath(root, `pull_request_template/branches/${segment}${ext}`));
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
return paths;
|
|
3868
|
+
}
|
|
3869
|
+
function defaultTemplateCandidates() {
|
|
3870
|
+
const paths = [];
|
|
3871
|
+
for (const root of TEMPLATE_ROOTS) {
|
|
3872
|
+
for (const ext of TEMPLATE_EXTENSIONS) {
|
|
3873
|
+
paths.push(joinTemplatePath(root, `pull_request_template${ext}`));
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
return paths;
|
|
3877
|
+
}
|
|
3878
|
+
async function firstMatchingTemplate(context, repo, cred, defaultBranch, candidates, kind) {
|
|
3879
|
+
for (const path3 of candidates) {
|
|
3880
|
+
const content = await fetchRepositoryItemContent(context, repo, cred, path3, defaultBranch);
|
|
3881
|
+
if (content !== null) {
|
|
3882
|
+
return { path: path3, content, kind };
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
return null;
|
|
3886
|
+
}
|
|
3887
|
+
async function resolvePullRequestTemplate(context, repo, cred, defaultBranch, targetBranch) {
|
|
3888
|
+
const branchMatch = await firstMatchingTemplate(
|
|
3889
|
+
context,
|
|
3890
|
+
repo,
|
|
3891
|
+
cred,
|
|
3892
|
+
defaultBranch,
|
|
3893
|
+
branchTemplateCandidates(targetBranch),
|
|
3894
|
+
"branch"
|
|
3895
|
+
);
|
|
3896
|
+
if (branchMatch !== null) {
|
|
3897
|
+
return branchMatch;
|
|
3898
|
+
}
|
|
3899
|
+
return firstMatchingTemplate(context, repo, cred, defaultBranch, defaultTemplateCandidates(), "default");
|
|
3900
|
+
}
|
|
3641
3901
|
|
|
3642
3902
|
// src/commands/pr.ts
|
|
3643
3903
|
function parsePositivePrNumber(raw) {
|
|
@@ -3736,6 +3996,13 @@ function handlePrCommandError(err, context, mode = "read") {
|
|
|
3736
3996
|
}
|
|
3737
3997
|
return;
|
|
3738
3998
|
}
|
|
3999
|
+
if (error.message === "IDENTITY_SCOPE_MISSING") {
|
|
4000
|
+
writeError(
|
|
4001
|
+
'Could not resolve reviewer identity: your PAT is missing the "Identity (Read)" scope required by the Azure DevOps identities API (separate from Code scope).',
|
|
4002
|
+
EXIT_NOT_PERMITTED
|
|
4003
|
+
);
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
3739
4006
|
if (error.message === "PERMISSION_DENIED") {
|
|
3740
4007
|
writeError(`Access denied. Your PAT may lack ${mode} permissions for project "${context?.project}".`, EXIT_NOT_PERMITTED);
|
|
3741
4008
|
return;
|
|
@@ -3926,18 +4193,18 @@ function createPrStatusCommand() {
|
|
|
3926
4193
|
}
|
|
3927
4194
|
function createPrOpenCommand() {
|
|
3928
4195
|
const command = new Command12("open");
|
|
3929
|
-
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
4196
|
+
withCommonPrOptions(command).description("Open a pull request from the current branch to develop").option("--title <title>", "pull request title").option(
|
|
4197
|
+
"--description <description>",
|
|
4198
|
+
"pull request description; when omitted, a repository-defined pull request template is used if one exists (prepended by this text when both are present)"
|
|
4199
|
+
).option("--json", "output JSON").action(async (options) => {
|
|
3930
4200
|
validateOrgProjectPair(options);
|
|
3931
4201
|
const title = options.title?.trim();
|
|
3932
4202
|
if (!title) {
|
|
3933
4203
|
writeError("--title is required for pull request creation.");
|
|
3934
4204
|
return;
|
|
3935
4205
|
}
|
|
3936
|
-
const
|
|
3937
|
-
|
|
3938
|
-
writeError("--description is required for pull request creation.");
|
|
3939
|
-
return;
|
|
3940
|
-
}
|
|
4206
|
+
const trimmedDescription = options.description?.trim();
|
|
4207
|
+
const description = trimmedDescription && trimmedDescription.length > 0 ? trimmedDescription : void 0;
|
|
3941
4208
|
let context;
|
|
3942
4209
|
try {
|
|
3943
4210
|
const resolved = await resolvePrCommandContext(options);
|
|
@@ -3977,6 +4244,10 @@ ${result.pullRequest.url ?? "\u2014"}
|
|
|
3977
4244
|
writeError(`Multiple active pull requests already exist for this branch targeting develop: ${ids}. Use pr status to review them.`);
|
|
3978
4245
|
return;
|
|
3979
4246
|
}
|
|
4247
|
+
if (err instanceof Error && err.message === "DESCRIPTION_REQUIRED") {
|
|
4248
|
+
writeError("--description is required for pull request creation.");
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
3980
4251
|
handlePrCommandError(err, context, "write");
|
|
3981
4252
|
}
|
|
3982
4253
|
});
|
|
@@ -4579,6 +4850,182 @@ function createPrListCommand() {
|
|
|
4579
4850
|
});
|
|
4580
4851
|
return command;
|
|
4581
4852
|
}
|
|
4853
|
+
async function runWorkItemLinkChange(workItemIdRaw, options, direction) {
|
|
4854
|
+
let context;
|
|
4855
|
+
try {
|
|
4856
|
+
const workItemId = parsePositivePrNumber(workItemIdRaw);
|
|
4857
|
+
if (workItemId === null) {
|
|
4858
|
+
validateOrgProjectPair(options);
|
|
4859
|
+
writeError(`Invalid work item id "${workItemIdRaw}"; expected a positive integer.`);
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
const target = await resolvePullRequestTarget(options);
|
|
4863
|
+
if (target === null) {
|
|
4864
|
+
return;
|
|
4865
|
+
}
|
|
4866
|
+
context = target.context;
|
|
4867
|
+
let outcome;
|
|
4868
|
+
try {
|
|
4869
|
+
outcome = direction === "link" ? await linkWorkItemToPullRequest(target.context, target.repo, target.pat, target.pullRequest.id, workItemId) : await unlinkWorkItemFromPullRequest(target.context, target.repo, target.pat, target.pullRequest.id, workItemId);
|
|
4870
|
+
} catch (err) {
|
|
4871
|
+
if (err instanceof Error && err.message.startsWith("NOT_FOUND")) {
|
|
4872
|
+
writeError(`Work item #${workItemId} not found in ${target.context.org}/${target.context.project}.`, EXIT_NOT_FOUND);
|
|
4873
|
+
return;
|
|
4874
|
+
}
|
|
4875
|
+
throw err;
|
|
4876
|
+
}
|
|
4877
|
+
const result = {
|
|
4878
|
+
pullRequestId: outcome.pullRequestId,
|
|
4879
|
+
workItemId: outcome.workItemId,
|
|
4880
|
+
noop: outcome.noop
|
|
4881
|
+
};
|
|
4882
|
+
if (options.json) {
|
|
4883
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4884
|
+
`);
|
|
4885
|
+
return;
|
|
4886
|
+
}
|
|
4887
|
+
if (direction === "link") {
|
|
4888
|
+
process.stdout.write(
|
|
4889
|
+
outcome.noop ? `Work item #${workItemId} is already linked to pull request #${outcome.pullRequestId}.
|
|
4890
|
+
` : `Linked work item #${workItemId} to pull request #${outcome.pullRequestId}.
|
|
4891
|
+
`
|
|
4892
|
+
);
|
|
4893
|
+
} else {
|
|
4894
|
+
process.stdout.write(
|
|
4895
|
+
outcome.noop ? `Work item #${workItemId} was not linked to pull request #${outcome.pullRequestId}.
|
|
4896
|
+
` : `Unlinked work item #${workItemId} from pull request #${outcome.pullRequestId}.
|
|
4897
|
+
`
|
|
4898
|
+
);
|
|
4899
|
+
}
|
|
4900
|
+
} catch (err) {
|
|
4901
|
+
handlePrCommandError(err, context, "write");
|
|
4902
|
+
}
|
|
4903
|
+
}
|
|
4904
|
+
function buildWorkItemLinkCommand(name, description, direction) {
|
|
4905
|
+
const command = new Command12(name);
|
|
4906
|
+
withCommonPrOptions(configureUnwrappedHelp(command)).description(description).argument("<workItemId>", "numeric id of the work item").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (workItemIdRaw, _options, command2) => {
|
|
4907
|
+
await runWorkItemLinkChange(workItemIdRaw, mergedPrOptions(command2), direction);
|
|
4908
|
+
});
|
|
4909
|
+
return command;
|
|
4910
|
+
}
|
|
4911
|
+
function createPrWorkItemsCommand() {
|
|
4912
|
+
const command = new Command12("work-items");
|
|
4913
|
+
command.description("Manage work items linked to a pull request");
|
|
4914
|
+
command.addCommand(buildWorkItemLinkCommand("link", "Link a work item to the pull request", "link"));
|
|
4915
|
+
command.addCommand(buildWorkItemLinkCommand("unlink", "Unlink a work item from the pull request", "unlink"));
|
|
4916
|
+
return command;
|
|
4917
|
+
}
|
|
4918
|
+
async function runReviewerAdd(reviewer, options) {
|
|
4919
|
+
let context;
|
|
4920
|
+
try {
|
|
4921
|
+
const target = await resolvePullRequestTarget(options);
|
|
4922
|
+
if (target === null) {
|
|
4923
|
+
return;
|
|
4924
|
+
}
|
|
4925
|
+
context = target.context;
|
|
4926
|
+
let identity;
|
|
4927
|
+
try {
|
|
4928
|
+
identity = await resolveReviewerIdentity(target.context.org, target.pat, reviewer);
|
|
4929
|
+
} catch (err) {
|
|
4930
|
+
if (err instanceof Error && err.message.startsWith("RESOLVE_FAILED:")) {
|
|
4931
|
+
writeError(`Reviewer "${reviewer}" could not be resolved to an Azure DevOps identity.`);
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
throw err;
|
|
4935
|
+
}
|
|
4936
|
+
const isRequired = options.required === true;
|
|
4937
|
+
const existingReviewers = await getPullRequestReviewers(target.context, target.repo, target.pat, target.pullRequest.id);
|
|
4938
|
+
const existing = existingReviewers.find((reviewer2) => reviewer2.id === identity.id);
|
|
4939
|
+
const noop = existing?.isRequired === isRequired;
|
|
4940
|
+
const added = noop ? existing : await addOrUpdatePullRequestReviewer(
|
|
4941
|
+
target.context,
|
|
4942
|
+
target.repo,
|
|
4943
|
+
target.pat,
|
|
4944
|
+
target.pullRequest.id,
|
|
4945
|
+
identity.id,
|
|
4946
|
+
isRequired
|
|
4947
|
+
);
|
|
4948
|
+
const result = {
|
|
4949
|
+
pullRequestId: target.pullRequest.id,
|
|
4950
|
+
reviewer: { id: added.id, displayName: added.displayName, uniqueName: added.uniqueName, isRequired: added.isRequired },
|
|
4951
|
+
noop
|
|
4952
|
+
};
|
|
4953
|
+
if (options.json) {
|
|
4954
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4955
|
+
`);
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
if (noop) {
|
|
4959
|
+
const label2 = added.displayName ?? added.uniqueName ?? reviewer;
|
|
4960
|
+
const kind2 = added.isRequired ? "required" : "optional";
|
|
4961
|
+
process.stdout.write(`${label2} is already a ${kind2} reviewer on pull request #${target.pullRequest.id}.
|
|
4962
|
+
`);
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
const label = added.displayName ?? added.uniqueName ?? reviewer;
|
|
4966
|
+
const kind = added.isRequired ? "required" : "optional";
|
|
4967
|
+
process.stdout.write(`Added ${label} as a ${kind} reviewer on pull request #${target.pullRequest.id}.
|
|
4968
|
+
`);
|
|
4969
|
+
} catch (err) {
|
|
4970
|
+
handlePrCommandError(err, context, "write");
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
async function runReviewerRemove(reviewer, options) {
|
|
4974
|
+
let context;
|
|
4975
|
+
try {
|
|
4976
|
+
const target = await resolvePullRequestTarget(options);
|
|
4977
|
+
if (target === null) {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
context = target.context;
|
|
4981
|
+
let identity;
|
|
4982
|
+
try {
|
|
4983
|
+
identity = await resolveReviewerIdentity(target.context.org, target.pat, reviewer);
|
|
4984
|
+
} catch (err) {
|
|
4985
|
+
if (err instanceof Error && err.message.startsWith("RESOLVE_FAILED:")) {
|
|
4986
|
+
writeError(`Reviewer "${reviewer}" could not be resolved to an Azure DevOps identity.`);
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
throw err;
|
|
4990
|
+
}
|
|
4991
|
+
const outcome = await removePullRequestReviewer(target.context, target.repo, target.pat, target.pullRequest.id, identity.id);
|
|
4992
|
+
const result = {
|
|
4993
|
+
pullRequestId: target.pullRequest.id,
|
|
4994
|
+
reviewer: outcome.reviewer ? { id: outcome.reviewer.id, displayName: outcome.reviewer.displayName, uniqueName: outcome.reviewer.uniqueName, isRequired: outcome.reviewer.isRequired } : null,
|
|
4995
|
+
noop: outcome.noop
|
|
4996
|
+
};
|
|
4997
|
+
if (options.json) {
|
|
4998
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4999
|
+
`);
|
|
5000
|
+
return;
|
|
5001
|
+
}
|
|
5002
|
+
if (outcome.noop) {
|
|
5003
|
+
process.stdout.write(`${reviewer} is not a reviewer on pull request #${target.pullRequest.id}.
|
|
5004
|
+
`);
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
const label = outcome.reviewer?.displayName ?? outcome.reviewer?.uniqueName ?? reviewer;
|
|
5008
|
+
process.stdout.write(`Removed ${label} from pull request #${target.pullRequest.id}.
|
|
5009
|
+
`);
|
|
5010
|
+
} catch (err) {
|
|
5011
|
+
handlePrCommandError(err, context, "write");
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
function createPrReviewersCommand() {
|
|
5015
|
+
const command = new Command12("reviewers");
|
|
5016
|
+
command.description("Manage pull request reviewers");
|
|
5017
|
+
const add = new Command12("add");
|
|
5018
|
+
withCommonPrOptions(configureUnwrappedHelp(add)).description("Add a reviewer to the pull request (optional by default)").argument("<reviewer>", "reviewer email or unique name").option("--required", "mark the reviewer as required instead of optional").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
|
|
5019
|
+
await runReviewerAdd(reviewer, mergedPrOptions(command2));
|
|
5020
|
+
});
|
|
5021
|
+
command.addCommand(add);
|
|
5022
|
+
const remove = new Command12("remove");
|
|
5023
|
+
withCommonPrOptions(configureUnwrappedHelp(remove)).description("Remove a reviewer from the pull request").argument("<reviewer>", "reviewer email or unique name").option("--pr-number <N>", PR_NUMBER_HELP).option("--json", "output JSON").action(async (reviewer, _options, command2) => {
|
|
5024
|
+
await runReviewerRemove(reviewer, mergedPrOptions(command2));
|
|
5025
|
+
});
|
|
5026
|
+
command.addCommand(remove);
|
|
5027
|
+
return command;
|
|
5028
|
+
}
|
|
4582
5029
|
function createPrCommand() {
|
|
4583
5030
|
const command = new Command12("pr");
|
|
4584
5031
|
command.description("Manage Azure DevOps pull requests");
|
|
@@ -4590,6 +5037,8 @@ function createPrCommand() {
|
|
|
4590
5037
|
command.addCommand(createPrCommentReopenCommand());
|
|
4591
5038
|
command.addCommand(createPrCommentReplyCommand());
|
|
4592
5039
|
command.addCommand(createPrCommentAddCommand());
|
|
5040
|
+
command.addCommand(createPrWorkItemsCommand());
|
|
5041
|
+
command.addCommand(createPrReviewersCommand());
|
|
4593
5042
|
command.addCommand(createPrCommentEditCommand());
|
|
4594
5043
|
return command;
|
|
4595
5044
|
}
|
|
@@ -5648,7 +6097,7 @@ function buildRelationTypesUrl(context) {
|
|
|
5648
6097
|
url.searchParams.set("api-version", API_VERSION2);
|
|
5649
6098
|
return url;
|
|
5650
6099
|
}
|
|
5651
|
-
function
|
|
6100
|
+
function buildWorkItemUrl3(context, id, expand) {
|
|
5652
6101
|
const url = new URL(
|
|
5653
6102
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
5654
6103
|
);
|
|
@@ -5698,7 +6147,7 @@ async function resolveRelationType(context, cred, alias) {
|
|
|
5698
6147
|
return match;
|
|
5699
6148
|
}
|
|
5700
6149
|
async function getWorkItemWithRelations(context, cred, id) {
|
|
5701
|
-
const url =
|
|
6150
|
+
const url = buildWorkItemUrl3(context, id, "relations");
|
|
5702
6151
|
const response = await fetchWithErrors(url.toString(), {
|
|
5703
6152
|
headers: authHeaders(cred)
|
|
5704
6153
|
});
|
|
@@ -5715,7 +6164,7 @@ async function addWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
5715
6164
|
if (exists) {
|
|
5716
6165
|
return { status: "already_exists", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
5717
6166
|
}
|
|
5718
|
-
const patchUrl =
|
|
6167
|
+
const patchUrl = buildWorkItemUrl3(context, id1);
|
|
5719
6168
|
const response = await fetchWithErrors(patchUrl.toString(), {
|
|
5720
6169
|
method: "PATCH",
|
|
5721
6170
|
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|
|
@@ -5737,7 +6186,7 @@ async function removeWorkItemRelation(context, cred, type, id1, id2) {
|
|
|
5737
6186
|
if (index === -1) {
|
|
5738
6187
|
return { status: "not_found", type: relType.name, referenceName: relType.referenceName, id1, id2 };
|
|
5739
6188
|
}
|
|
5740
|
-
const patchUrl =
|
|
6189
|
+
const patchUrl = buildWorkItemUrl3(context, id1);
|
|
5741
6190
|
const response = await fetchWithErrors(patchUrl.toString(), {
|
|
5742
6191
|
method: "PATCH",
|
|
5743
6192
|
headers: { ...authHeaders(cred), "Content-Type": "application/json-patch+json" },
|