Skip to content

Commit 382f4d0

Browse files
authored
Adding new param to link PR to work items to account for cross project linking (#333)
This pull request introduces support for linking pull requests across different projects by adding an optional `pullRequestProjectId` parameter. It includes updates to both the implementation and corresponding tests to handle this new functionality. ### Feature Enhancements: * **Cross-Project Linking Support**: - Added an optional `pullRequestProjectId` parameter in the `configureWorkItemTools` function to allow linking pull requests from a different project. If not provided, it defaults to the `projectId` for same-project linking. (`src/tools/workitems.ts`, [src/tools/workitems.tsR367-R377](diffhunk://#diff-86312c74c8d340f1b252bb6a34ae3d610c400cf9151d45223ec54d2d2ab2b0c9R367-R377)) ### Test Updates: * **New Test Cases for Cross-Project Linking**: - Added a test to verify that `pullRequestProjectId` is used instead of `projectId` when provided. (`test/src/tools/workitems.test.ts`, [test/src/tools/workitems.test.tsR565-R642](diffhunk://#diff-81b8c1c7196cc4eba4c6b2a30eaec7c3101f72c5a88803d07b942b71e3404b9dR565-R642)) - Added a test to ensure fallback to `projectId` when `pullRequestProjectId` is empty. (`test/src/tools/workitems.test.ts`, [test/src/tools/workitems.test.tsR565-R642](diffhunk://#diff-81b8c1c7196cc4eba4c6b2a30eaec7c3101f72c5a88803d07b942b71e3404b9dR565-R642)) * **Parameter Updates in Existing Tests**: - Updated existing test cases to include the `pullRequestProjectId` parameter where relevant. (`test/src/tools/workitems.test.ts`, [[1]](diffhunk://#diff-81b8c1c7196cc4eba4c6b2a30eaec7c3101f72c5a88803d07b942b71e3404b9dR658) [[2]](diffhunk://#diff-81b8c1c7196cc4eba4c6b2a30eaec7c3101f72c5a88803d07b942b71e3404b9dL1238-R1321) [[3]](diffhunk://#diff-81b8c1c7196cc4eba4c6b2a30eaec7c3101f72c5a88803d07b942b71e3404b9dL1260-R1344) ## GitHub issue number #330 ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [ ] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Updated tests and tested manually
1 parent f54fa9b commit 382f4d0

File tree

2 files changed

+87
-4
lines changed

2 files changed

+87
-4
lines changed

src/tools/workitems.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,17 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
364364
repositoryId: z.string().describe("The ID of the repository containing the pull request. Do not use the repository name here, use the ID instead."),
365365
pullRequestId: z.number().describe("The ID of the pull request to link to."),
366366
workItemId: z.number().describe("The ID of the work item to link to the pull request."),
367+
pullRequestProjectId: z.string().optional().describe("The project ID containing the pull request. If not provided, defaults to the work item's project ID (for same-project linking)."),
367368
},
368-
async ({ projectId, repositoryId, pullRequestId, workItemId }) => {
369+
async ({ projectId, repositoryId, pullRequestId, workItemId, pullRequestProjectId }) => {
369370
try {
370371
const connection = await connectionProvider();
371372
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
372373

373374
// Create artifact link relation using vstfs format
374375
// Format: vstfs:///Git/PullRequestId/{project}/{repositoryId}/{pullRequestId}
375-
const artifactPathValue = `${projectId}/${repositoryId}/${pullRequestId}`;
376+
const artifactProjectId = pullRequestProjectId && pullRequestProjectId.trim() !== "" ? pullRequestProjectId : projectId;
377+
const artifactPathValue = `${artifactProjectId}/${repositoryId}/${pullRequestId}`;
376378
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
377379

378380
// Use the PATCH document format for adding a relation

test/src/tools/workitems.test.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,84 @@ describe("configureWorkItemTools", () => {
562562
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId);
563563
});
564564

565+
it("should use pullRequestProjectId instead of projectId when provided", async () => {
566+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
567+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request");
568+
if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered");
569+
570+
const [, , , handler] = call;
571+
(mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]);
572+
573+
const params = {
574+
projectId: "work-item-project-id",
575+
repositoryId: "repo-123",
576+
pullRequestId: 67890,
577+
workItemId: 131489,
578+
pullRequestProjectId: "different-project-id",
579+
};
580+
581+
// Should use pullRequestProjectId instead of projectId
582+
const artifactPathValue = `${params.pullRequestProjectId}/${params.repositoryId}/${params.pullRequestId}`;
583+
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
584+
585+
const document = [
586+
{
587+
op: "add",
588+
path: "/relations/-",
589+
value: {
590+
rel: "ArtifactLink",
591+
url: vstfsUrl,
592+
attributes: {
593+
name: "Pull Request",
594+
},
595+
},
596+
},
597+
];
598+
await handler(params);
599+
600+
// Note: Work item should still be updated in the original project
601+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId);
602+
});
603+
604+
it("should fall back to projectId when pullRequestProjectId is empty", async () => {
605+
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
606+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_link_work_item_to_pull_request");
607+
if (!call) throw new Error("wit_link_work_item_to_pull_request tool not registered");
608+
609+
const [, , , handler] = call;
610+
(mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue([_mockWorkItem]);
611+
612+
// Testing with empty string for pullRequestProjectId
613+
const params = {
614+
projectId: "work-item-project-id",
615+
repositoryId: "repo-123",
616+
pullRequestId: 67890,
617+
workItemId: 131489,
618+
pullRequestProjectId: "",
619+
};
620+
621+
// Should use projectId since pullRequestProjectId is empty
622+
const artifactPathValue = `${params.projectId}/${params.repositoryId}/${params.pullRequestId}`;
623+
const vstfsUrl = `vstfs:///Git/PullRequestId/${encodeURIComponent(artifactPathValue)}`;
624+
625+
const document = [
626+
{
627+
op: "add",
628+
path: "/relations/-",
629+
value: {
630+
rel: "ArtifactLink",
631+
url: vstfsUrl,
632+
attributes: {
633+
name: "Pull Request",
634+
},
635+
},
636+
},
637+
];
638+
await handler(params);
639+
640+
expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith({}, document, params.workItemId, params.projectId);
641+
});
642+
565643
it("should handle link_work_item_to_pull_request unknown error type", async () => {
566644
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);
567645

@@ -577,6 +655,7 @@ describe("configureWorkItemTools", () => {
577655
repositoryId: "repo-123",
578656
pullRequestId: 42,
579657
workItemId: 1,
658+
pullRequestProjectId: "other-project",
580659
};
581660

582661
const result = await handler(params);
@@ -1235,10 +1314,11 @@ describe("configureWorkItemTools", () => {
12351314
(mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockRejectedValue(new Error("Linking failed"));
12361315

12371316
const params = {
1238-
project: "TestProject",
1317+
projectId: "TestProject",
12391318
repositoryId: "repo-123",
12401319
pullRequestId: 42,
12411320
workItemId: 1,
1321+
pullRequestProjectId: "OtherProject",
12421322
};
12431323

12441324
const result = await handler(params);
@@ -1257,10 +1337,11 @@ describe("configureWorkItemTools", () => {
12571337
(mockWorkItemTrackingApi.updateWorkItem as jest.Mock).mockResolvedValue(null);
12581338

12591339
const params = {
1260-
project: "TestProject",
1340+
projectId: "TestProject",
12611341
repositoryId: "repo-123",
12621342
pullRequestId: 42,
12631343
workItemId: 1,
1344+
pullRequestProjectId: "OtherProject",
12641345
};
12651346

12661347
const result = await handler(params);

0 commit comments

Comments
 (0)