@achieveai/azuredevops-mcp 1.3.17 → 1.3.19
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 +76 -0
- package/dist/Interfaces/Common.js +37 -1
- package/dist/Interfaces/Common.js.map +1 -1
- package/dist/Services/AzureDevOpsService.js +234 -32
- package/dist/Services/AzureDevOpsService.js.map +1 -1
- package/dist/Services/BoardsSprintsService.js +111 -13
- package/dist/Services/BoardsSprintsService.js.map +1 -1
- package/dist/Services/BuildService.js +157 -24
- package/dist/Services/BuildService.js.map +1 -1
- package/dist/Services/GitService.js +26 -3
- package/dist/Services/GitService.js.map +1 -1
- package/dist/Services/ProjectService.js +47 -6
- package/dist/Services/ProjectService.js.map +1 -1
- package/dist/Services/WorkItemService.js +183 -170
- package/dist/Services/WorkItemService.js.map +1 -1
- package/dist/Tools/BoardsSprintsTools.js +2 -8
- package/dist/Tools/BoardsSprintsTools.js.map +1 -1
- package/dist/Tools/BuildTools.js +5 -8
- package/dist/Tools/BuildTools.js.map +1 -1
- package/dist/Tools/GitTools.js +177 -62
- package/dist/Tools/GitTools.js.map +1 -1
- package/dist/Tools/WorkItemTools.js +110 -172
- package/dist/Tools/WorkItemTools.js.map +1 -1
- package/dist/index.js +31 -26
- package/dist/index.js.map +1 -1
- package/dist/utils/apiUsageGuidance.js +336 -0
- package/dist/utils/apiUsageGuidance.js.map +1 -0
- package/dist/utils/formatHelpers.js +15 -0
- package/dist/utils/formatHelpers.js.map +1 -1
- package/package.json +3 -3
- package/dist/Services/BuildService.project.test.js +0 -91
- package/dist/Services/BuildService.project.test.js.map +0 -1
- package/dist/Services/GitService.project.test.js +0 -407
- package/dist/Services/GitService.project.test.js.map +0 -1
- package/dist/package.json +0 -59
- package/dist/src/Interfaces/AIAssisted.js +0 -3
- package/dist/src/Interfaces/AIAssisted.js.map +0 -1
- package/dist/src/Interfaces/ArtifactManagement.js +0 -3
- package/dist/src/Interfaces/ArtifactManagement.js.map +0 -1
- package/dist/src/Interfaces/AzureDevOps.js +0 -3
- package/dist/src/Interfaces/AzureDevOps.js.map +0 -1
- package/dist/src/Interfaces/BoardsAndSprints.js +0 -3
- package/dist/src/Interfaces/BoardsAndSprints.js.map +0 -1
- package/dist/src/Interfaces/CodeAndRepositories.js +0 -3
- package/dist/src/Interfaces/CodeAndRepositories.js.map +0 -1
- package/dist/src/Interfaces/Common.js +0 -134
- package/dist/src/Interfaces/Common.js.map +0 -1
- package/dist/src/Interfaces/CostResourceManagement.js +0 -3
- package/dist/src/Interfaces/CostResourceManagement.js.map +0 -1
- package/dist/src/Interfaces/DevSecOps.js +0 -3
- package/dist/src/Interfaces/DevSecOps.js.map +0 -1
- package/dist/src/Interfaces/ExternalIntegrations.js +0 -3
- package/dist/src/Interfaces/ExternalIntegrations.js.map +0 -1
- package/dist/src/Interfaces/HybridCrossPlatform.js +0 -3
- package/dist/src/Interfaces/HybridCrossPlatform.js.map +0 -1
- package/dist/src/Interfaces/Pipelines.js +0 -3
- package/dist/src/Interfaces/Pipelines.js.map +0 -1
- package/dist/src/Interfaces/ProjectManagement.js +0 -3
- package/dist/src/Interfaces/ProjectManagement.js.map +0 -1
- package/dist/src/Interfaces/TestingCapabilities.js +0 -3
- package/dist/src/Interfaces/TestingCapabilities.js.map +0 -1
- package/dist/src/Interfaces/Wiki.js +0 -3
- package/dist/src/Interfaces/Wiki.js.map +0 -1
- package/dist/src/Interfaces/WorkItems.js +0 -3
- package/dist/src/Interfaces/WorkItems.js.map +0 -1
- package/dist/src/Services/AIAssistedDevelopmentService.js +0 -195
- package/dist/src/Services/AIAssistedDevelopmentService.js.map +0 -1
- package/dist/src/Services/ArtifactManagementService.js +0 -346
- package/dist/src/Services/ArtifactManagementService.js.map +0 -1
- package/dist/src/Services/AzureDevOpsService.js +0 -385
- package/dist/src/Services/AzureDevOpsService.js.map +0 -1
- package/dist/src/Services/BoardsSprintsService.js +0 -339
- package/dist/src/Services/BoardsSprintsService.js.map +0 -1
- package/dist/src/Services/BuildService.js +0 -405
- package/dist/src/Services/BuildService.js.map +0 -1
- package/dist/src/Services/DevSecOpsService.js +0 -307
- package/dist/src/Services/DevSecOpsService.js.map +0 -1
- package/dist/src/Services/EntraAuthHandler.js +0 -337
- package/dist/src/Services/EntraAuthHandler.js.map +0 -1
- package/dist/src/Services/GitService.js +0 -1595
- package/dist/src/Services/GitService.js.map +0 -1
- package/dist/src/Services/ProjectService.js +0 -257
- package/dist/src/Services/ProjectService.js.map +0 -1
- package/dist/src/Services/TestingCapabilitiesService.js +0 -149
- package/dist/src/Services/TestingCapabilitiesService.js.map +0 -1
- package/dist/src/Services/WikiService.js +0 -90
- package/dist/src/Services/WikiService.js.map +0 -1
- package/dist/src/Services/WorkItemService.js +0 -885
- package/dist/src/Services/WorkItemService.js.map +0 -1
- package/dist/src/Tools/AIAssistedDevelopmentTools.js +0 -137
- package/dist/src/Tools/AIAssistedDevelopmentTools.js.map +0 -1
- package/dist/src/Tools/ArtifactManagementTools.js +0 -140
- package/dist/src/Tools/ArtifactManagementTools.js.map +0 -1
- package/dist/src/Tools/BoardsSprintsTools.js +0 -338
- package/dist/src/Tools/BoardsSprintsTools.js.map +0 -1
- package/dist/src/Tools/BuildTools.js +0 -468
- package/dist/src/Tools/BuildTools.js.map +0 -1
- package/dist/src/Tools/DevSecOpsTools.js +0 -147
- package/dist/src/Tools/DevSecOpsTools.js.map +0 -1
- package/dist/src/Tools/GitTools.js +0 -1475
- package/dist/src/Tools/GitTools.js.map +0 -1
- package/dist/src/Tools/ProjectTools.js +0 -360
- package/dist/src/Tools/ProjectTools.js.map +0 -1
- package/dist/src/Tools/TestingCapabilitiesTools.js +0 -157
- package/dist/src/Tools/TestingCapabilitiesTools.js.map +0 -1
- package/dist/src/Tools/WikiTools.js +0 -137
- package/dist/src/Tools/WikiTools.js.map +0 -1
- package/dist/src/Tools/WorkItemTools.js +0 -862
- package/dist/src/Tools/WorkItemTools.js.map +0 -1
- package/dist/src/config.js +0 -176
- package/dist/src/config.js.map +0 -1
- package/dist/src/index.js +0 -1716
- package/dist/src/index.js.map +0 -1
- package/dist/src/utils/formatHelpers.js +0 -257
- package/dist/src/utils/formatHelpers.js.map +0 -1
- package/dist/src/utils/getClassMethods.js +0 -8
- package/dist/src/utils/getClassMethods.js.map +0 -1
- package/dist/src/utils/repositoryResolver.js +0 -40
- package/dist/src/utils/repositoryResolver.js.map +0 -1
|
@@ -1,885 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WorkItemService = void 0;
|
|
4
|
-
const WorkItemTrackingInterfaces_1 = require("azure-devops-node-api/interfaces/WorkItemTrackingInterfaces");
|
|
5
|
-
const VSSInterfaces_1 = require("azure-devops-node-api/interfaces/common/VSSInterfaces");
|
|
6
|
-
const AzureDevOpsService_1 = require("./AzureDevOpsService");
|
|
7
|
-
const formatHelpers_1 = require("../utils/formatHelpers");
|
|
8
|
-
/** Rich-text fields that expect HTML — markdown is auto-converted for these */
|
|
9
|
-
const RICH_TEXT_FIELDS = new Set([
|
|
10
|
-
'System.Description',
|
|
11
|
-
'System.History',
|
|
12
|
-
'System.ReproSteps',
|
|
13
|
-
'Microsoft.VSTS.TCM.Steps',
|
|
14
|
-
'Microsoft.VSTS.Common.AcceptanceCriteria',
|
|
15
|
-
]);
|
|
16
|
-
class WorkItemService extends AzureDevOpsService_1.AzureDevOpsService {
|
|
17
|
-
constructor(config) {
|
|
18
|
-
super(config);
|
|
19
|
-
this.wiqlCache = new Map();
|
|
20
|
-
this.CACHE_TTL_MS = 60000; // 60 seconds
|
|
21
|
-
this.MAX_CACHE_ENTRIES = 50;
|
|
22
|
-
this.DEFAULT_RECENT_DAYS = 7;
|
|
23
|
-
this.MAX_RECENT_DAYS = 30;
|
|
24
|
-
this.DEFAULT_FIELDS = [
|
|
25
|
-
...AzureDevOpsService_1.AzureDevOpsService.BASE_SUMMARY_FIELDS,
|
|
26
|
-
...AzureDevOpsService_1.AzureDevOpsService.SCHEDULING_FIELDS,
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
getCachedWiql(cacheKey) {
|
|
30
|
-
const entry = this.wiqlCache.get(cacheKey);
|
|
31
|
-
if (entry && Date.now() - entry.timestamp < this.CACHE_TTL_MS) {
|
|
32
|
-
return entry.result;
|
|
33
|
-
}
|
|
34
|
-
if (entry) {
|
|
35
|
-
this.wiqlCache.delete(cacheKey);
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
setCachedWiql(cacheKey, result) {
|
|
40
|
-
if (this.wiqlCache.size >= this.MAX_CACHE_ENTRIES) {
|
|
41
|
-
const oldestKey = this.wiqlCache.keys().next().value;
|
|
42
|
-
if (oldestKey !== undefined) {
|
|
43
|
-
this.wiqlCache.delete(oldestKey);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
this.wiqlCache.set(cacheKey, { result, timestamp: Date.now() });
|
|
47
|
-
}
|
|
48
|
-
buildWiqlCacheKey(query, top) {
|
|
49
|
-
return `${this.config.project}:${top ?? 'none'}:${query}`;
|
|
50
|
-
}
|
|
51
|
-
normalizeRecentDays(days) {
|
|
52
|
-
if (!days || Number.isNaN(days)) {
|
|
53
|
-
return this.DEFAULT_RECENT_DAYS;
|
|
54
|
-
}
|
|
55
|
-
return Math.min(Math.max(Math.floor(days), 1), this.MAX_RECENT_DAYS);
|
|
56
|
-
}
|
|
57
|
-
escapeWiqlLiteral(value) {
|
|
58
|
-
return value.replace(/'/g, "''");
|
|
59
|
-
}
|
|
60
|
-
applyRecentChangesFilter(query, days) {
|
|
61
|
-
const normalizedDays = this.normalizeRecentDays(days);
|
|
62
|
-
if (/\bfrom\s+workitemlinks\b/i.test(query) || /\[System\.ChangedDate\]/i.test(query)) {
|
|
63
|
-
return { query };
|
|
64
|
-
}
|
|
65
|
-
const orderByMatch = query.match(/\border\s+by\b/i);
|
|
66
|
-
const whereMatch = query.match(/\bwhere\b/i);
|
|
67
|
-
const fromWorkItemsMatch = query.match(/\bfrom\s+workitems\b/i);
|
|
68
|
-
if (!whereMatch) {
|
|
69
|
-
if (!fromWorkItemsMatch || fromWorkItemsMatch.index === undefined) {
|
|
70
|
-
return { query };
|
|
71
|
-
}
|
|
72
|
-
const insertAt = fromWorkItemsMatch.index + fromWorkItemsMatch[0].length;
|
|
73
|
-
const scopedQuery = `${query.slice(0, insertAt)}\n WHERE [System.ChangedDate] >= @today - ${normalizedDays}${query.slice(insertAt)}`;
|
|
74
|
-
return { query: scopedQuery, recentDaysApplied: normalizedDays };
|
|
75
|
-
}
|
|
76
|
-
const filter = `\n AND [System.ChangedDate] >= @today - ${normalizedDays}`;
|
|
77
|
-
if (orderByMatch && orderByMatch.index !== undefined) {
|
|
78
|
-
return {
|
|
79
|
-
query: `${query.slice(0, orderByMatch.index)}${filter}\n ${query.slice(orderByMatch.index)}`,
|
|
80
|
-
recentDaysApplied: normalizedDays,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
query: `${query}${filter}`,
|
|
85
|
-
recentDaysApplied: normalizedDays,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Query work items using WIQL
|
|
90
|
-
*/
|
|
91
|
-
async listWorkItems(wiqlQuery, top, days, fields) {
|
|
92
|
-
try {
|
|
93
|
-
const serverTop = top ?? 100;
|
|
94
|
-
const scopedQuery = this.applyRecentChangesFilter(wiqlQuery, days);
|
|
95
|
-
const cacheKey = this.buildWiqlCacheKey(scopedQuery.query, serverTop);
|
|
96
|
-
const cached = this.getCachedWiql(cacheKey);
|
|
97
|
-
if (cached)
|
|
98
|
-
return cached;
|
|
99
|
-
const throttleNotices = [];
|
|
100
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
101
|
-
const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({ query: scopedQuery.query }, { project: this.config.project }, undefined, serverTop), {
|
|
102
|
-
operationName: 'workItems.list.queryByWiql',
|
|
103
|
-
details: { project: this.config.project, top: serverTop, recentDays: scopedQuery.recentDaysApplied },
|
|
104
|
-
}, throttleNotices);
|
|
105
|
-
const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems || [], {
|
|
106
|
-
fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.list.batchHydrate',
|
|
107
|
-
throttleAccumulator: throttleNotices,
|
|
108
|
-
});
|
|
109
|
-
const response = {
|
|
110
|
-
...queryResult,
|
|
111
|
-
workItems: hydratedWorkItems,
|
|
112
|
-
count: hydratedWorkItems.length,
|
|
113
|
-
originalQuery: wiqlQuery,
|
|
114
|
-
effectiveQuery: scopedQuery.query,
|
|
115
|
-
recentDaysApplied: scopedQuery.recentDaysApplied,
|
|
116
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
117
|
-
};
|
|
118
|
-
this.setCachedWiql(cacheKey, response);
|
|
119
|
-
return response;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
console.error('Error listing work items:', error);
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Get a work item by ID
|
|
128
|
-
*/
|
|
129
|
-
async getWorkItemById(params) {
|
|
130
|
-
try {
|
|
131
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
132
|
-
const workItem = await this.withAuthRetry(() => witApi.getWorkItem(params.id, undefined, undefined, WorkItemTrackingInterfaces_1.WorkItemExpand.Relations, this.config.project));
|
|
133
|
-
// Transform to streamlined format for MCP tool consumption
|
|
134
|
-
if (workItem && workItem.fields) {
|
|
135
|
-
const streamlined = {
|
|
136
|
-
id: workItem.id,
|
|
137
|
-
rev: workItem.rev,
|
|
138
|
-
title: workItem.fields['System.Title'],
|
|
139
|
-
workItemType: workItem.fields['System.WorkItemType'],
|
|
140
|
-
state: workItem.fields['System.State'],
|
|
141
|
-
areaPath: workItem.fields['System.AreaPath'],
|
|
142
|
-
iterationPath: workItem.fields['System.IterationPath'],
|
|
143
|
-
assignedTo: workItem.fields['System.AssignedTo'] ? {
|
|
144
|
-
displayName: workItem.fields['System.AssignedTo'].displayName,
|
|
145
|
-
uniqueName: workItem.fields['System.AssignedTo'].uniqueName
|
|
146
|
-
} : null,
|
|
147
|
-
createdBy: workItem.fields['System.CreatedBy'] ? {
|
|
148
|
-
displayName: workItem.fields['System.CreatedBy'].displayName,
|
|
149
|
-
uniqueName: workItem.fields['System.CreatedBy'].uniqueName
|
|
150
|
-
} : null,
|
|
151
|
-
createdDate: workItem.fields['System.CreatedDate'],
|
|
152
|
-
changedDate: workItem.fields['System.ChangedDate'],
|
|
153
|
-
description: workItem.fields['System.Description'],
|
|
154
|
-
priority: workItem.fields['Microsoft.VSTS.Common.Priority'],
|
|
155
|
-
originalEstimate: workItem.fields['Microsoft.VSTS.Scheduling.OriginalEstimate'],
|
|
156
|
-
completedWork: workItem.fields['Microsoft.VSTS.Scheduling.CompletedWork'],
|
|
157
|
-
remainingWork: workItem.fields['Microsoft.VSTS.Scheduling.RemainingWork']
|
|
158
|
-
};
|
|
159
|
-
// Add work item relations/dependencies
|
|
160
|
-
if (workItem.relations && workItem.relations.length > 0) {
|
|
161
|
-
streamlined.relations = workItem.relations.map((relation) => {
|
|
162
|
-
if (relation.rel === 'ArtifactLink') {
|
|
163
|
-
// Artifact link (PR, Build, Branch, Commit, etc.)
|
|
164
|
-
const artifactInfo = this.parseArtifactUri(relation.url);
|
|
165
|
-
return {
|
|
166
|
-
relationshipType: relation.rel,
|
|
167
|
-
artifactType: artifactInfo.type,
|
|
168
|
-
artifactId: artifactInfo.id,
|
|
169
|
-
artifactDisplayName: relation.attributes?.name || artifactInfo.type,
|
|
170
|
-
artifactUri: relation.url,
|
|
171
|
-
comment: relation.attributes?.comment || null
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
// Work item link
|
|
176
|
-
const urlParts = relation.url.split('/');
|
|
177
|
-
const relatedId = parseInt(urlParts[urlParts.length - 1]);
|
|
178
|
-
return {
|
|
179
|
-
relationshipType: relation.rel,
|
|
180
|
-
relatedWorkItemId: relatedId,
|
|
181
|
-
comment: relation.attributes?.comment || null
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
// Remove undefined fields to keep response clean
|
|
187
|
-
Object.keys(streamlined).forEach(key => {
|
|
188
|
-
if (streamlined[key] === undefined) {
|
|
189
|
-
delete streamlined[key];
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
return streamlined;
|
|
193
|
-
}
|
|
194
|
-
return workItem;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
console.error(`Error getting work item ${params.id}:`, error);
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Get work item with child effort roll-up
|
|
203
|
-
*/
|
|
204
|
-
async getWorkItemWithEffortRollup(params) {
|
|
205
|
-
try {
|
|
206
|
-
const workItem = await this.getWorkItemById(params);
|
|
207
|
-
// If this work item has child relationships, get effort roll-up
|
|
208
|
-
if (workItem.relations) {
|
|
209
|
-
const childRelations = workItem.relations.filter((rel) => rel.relationshipType === 'System.LinkTypes.Hierarchy-Forward');
|
|
210
|
-
if (childRelations.length > 0) {
|
|
211
|
-
// Fetch child work items to calculate effort roll-up
|
|
212
|
-
const childEffort = await this.calculateChildEffort(childRelations);
|
|
213
|
-
// Add roll-up information to the work item
|
|
214
|
-
workItem.childEffortRollup = {
|
|
215
|
-
childCount: childRelations.length,
|
|
216
|
-
totalOriginalEstimate: childEffort.totalOriginal,
|
|
217
|
-
totalCompletedWork: childEffort.totalCompleted,
|
|
218
|
-
totalRemainingWork: childEffort.totalRemaining,
|
|
219
|
-
childWorkItems: childEffort.childDetails
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return workItem;
|
|
224
|
-
}
|
|
225
|
-
catch (error) {
|
|
226
|
-
console.error(`Error getting work item with effort rollup ${params.id}:`, error);
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Parse a vstfs:/// artifact URI into type and ID.
|
|
232
|
-
* Handles both %2F and / separators in composite IDs.
|
|
233
|
-
*/
|
|
234
|
-
parseArtifactUri(uri) {
|
|
235
|
-
// Normalize %2F to / for easier parsing
|
|
236
|
-
const normalized = uri.replace(/%2[fF]/g, '/');
|
|
237
|
-
// vstfs:///Git/PullRequestId/{projectId}/{repoId}/{prId}
|
|
238
|
-
const prMatch = normalized.match(/vstfs:\/\/\/Git\/PullRequestId\/[^/]+\/[^/]+\/(\d+)/);
|
|
239
|
-
if (prMatch)
|
|
240
|
-
return { type: 'Pull Request', id: prMatch[1] };
|
|
241
|
-
// vstfs:///Build/Build/{buildId}
|
|
242
|
-
const buildMatch = normalized.match(/vstfs:\/\/\/Build\/Build\/(\d+)/);
|
|
243
|
-
if (buildMatch)
|
|
244
|
-
return { type: 'Build', id: buildMatch[1] };
|
|
245
|
-
// vstfs:///Git/Ref/{projectId}/{repoId}/GB{branchName}
|
|
246
|
-
const branchMatch = normalized.match(/vstfs:\/\/\/Git\/Ref\/[^/]+\/[^/]+\/GB(.+)/);
|
|
247
|
-
if (branchMatch)
|
|
248
|
-
return { type: 'Branch', id: branchMatch[1] };
|
|
249
|
-
// vstfs:///Git/Commit/{projectId}/{repoId}/{commitSha}
|
|
250
|
-
const commitMatch = normalized.match(/vstfs:\/\/\/Git\/Commit\/[^/]+\/[^/]+\/([a-f0-9]+)/i);
|
|
251
|
-
if (commitMatch)
|
|
252
|
-
return { type: 'Commit', id: commitMatch[1] };
|
|
253
|
-
// Fallback: extract tool/type from URI pattern vstfs:///{tool}/{type}/...
|
|
254
|
-
const genericMatch = normalized.match(/vstfs:\/\/\/([^/]+)\/([^/]+)\/(.*)/);
|
|
255
|
-
if (genericMatch)
|
|
256
|
-
return { type: `${genericMatch[1]}/${genericMatch[2]}`, id: genericMatch[3] };
|
|
257
|
-
return { type: 'Unknown', id: uri };
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Calculate effort roll-up from child work items
|
|
261
|
-
*/
|
|
262
|
-
async calculateChildEffort(childRelations) {
|
|
263
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
264
|
-
let totalOriginal = 0;
|
|
265
|
-
let totalCompleted = 0;
|
|
266
|
-
let totalRemaining = 0;
|
|
267
|
-
const childDetails = [];
|
|
268
|
-
// Fetch child work items in batch
|
|
269
|
-
const childIds = childRelations.map((rel) => rel.relatedWorkItemId);
|
|
270
|
-
try {
|
|
271
|
-
const childWorkItems = await witApi.getWorkItems(childIds, ['System.Id', 'System.Title', 'System.WorkItemType', 'System.State',
|
|
272
|
-
'Microsoft.VSTS.Scheduling.OriginalEstimate',
|
|
273
|
-
'Microsoft.VSTS.Scheduling.CompletedWork',
|
|
274
|
-
'Microsoft.VSTS.Scheduling.RemainingWork'], undefined, undefined, undefined, this.config.project);
|
|
275
|
-
childWorkItems.forEach((child) => {
|
|
276
|
-
const original = child.fields['Microsoft.VSTS.Scheduling.OriginalEstimate'] || 0;
|
|
277
|
-
const completed = child.fields['Microsoft.VSTS.Scheduling.CompletedWork'] || 0;
|
|
278
|
-
const remaining = child.fields['Microsoft.VSTS.Scheduling.RemainingWork'] || 0;
|
|
279
|
-
totalOriginal += original;
|
|
280
|
-
totalCompleted += completed;
|
|
281
|
-
totalRemaining += remaining;
|
|
282
|
-
childDetails.push({
|
|
283
|
-
id: child.id,
|
|
284
|
-
title: child.fields['System.Title'],
|
|
285
|
-
workItemType: child.fields['System.WorkItemType'],
|
|
286
|
-
state: child.fields['System.State'],
|
|
287
|
-
originalEstimate: original,
|
|
288
|
-
completedWork: completed,
|
|
289
|
-
remainingWork: remaining
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
console.error('Error fetching child work items for effort calculation:', error);
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
totalOriginal,
|
|
298
|
-
totalCompleted,
|
|
299
|
-
totalRemaining,
|
|
300
|
-
childDetails
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Search work items using text
|
|
305
|
-
*/
|
|
306
|
-
async searchWorkItems(params) {
|
|
307
|
-
try {
|
|
308
|
-
const serverTop = params.top || 25;
|
|
309
|
-
const recentDays = this.normalizeRecentDays(params.days);
|
|
310
|
-
const searchText = this.escapeWiqlLiteral(params.searchText);
|
|
311
|
-
const query = `SELECT [System.Id], [System.Title], [System.State], [System.ChangedDate]
|
|
312
|
-
FROM WorkItems
|
|
313
|
-
WHERE [System.TeamProject] = @project
|
|
314
|
-
AND [System.ChangedDate] >= @today - ${recentDays}
|
|
315
|
-
AND (
|
|
316
|
-
[System.Title] CONTAINS '${searchText}'
|
|
317
|
-
OR [System.Description] CONTAINS '${searchText}'
|
|
318
|
-
)
|
|
319
|
-
ORDER BY [System.ChangedDate] DESC`;
|
|
320
|
-
const cacheKey = this.buildWiqlCacheKey(query, serverTop);
|
|
321
|
-
const cached = this.getCachedWiql(cacheKey);
|
|
322
|
-
if (cached)
|
|
323
|
-
return cached;
|
|
324
|
-
const throttleNotices = [];
|
|
325
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
326
|
-
const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
|
|
327
|
-
query
|
|
328
|
-
}, {
|
|
329
|
-
project: this.config.project
|
|
330
|
-
}, undefined, serverTop), {
|
|
331
|
-
operationName: 'workItems.search.queryByWiql',
|
|
332
|
-
details: { project: this.config.project, top: serverTop, recentDays },
|
|
333
|
-
}, throttleNotices);
|
|
334
|
-
const advisory = 'Search uses CONTAINS clauses, which are expensive in Azure DevOps. Prefer listWorkItems with focused WIQL or saved queries when possible.';
|
|
335
|
-
if (queryResult.workItems && queryResult.workItems.length > 0) {
|
|
336
|
-
const transformedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems, {
|
|
337
|
-
fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.search.batchHydrate',
|
|
338
|
-
throttleAccumulator: throttleNotices,
|
|
339
|
-
});
|
|
340
|
-
const response = {
|
|
341
|
-
searchQuery: params.searchText,
|
|
342
|
-
totalResults: queryResult.workItems.length,
|
|
343
|
-
returnedResults: transformedWorkItems.length,
|
|
344
|
-
workItems: transformedWorkItems,
|
|
345
|
-
wiql: query,
|
|
346
|
-
recentDaysApplied: recentDays,
|
|
347
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
348
|
-
advisory,
|
|
349
|
-
};
|
|
350
|
-
this.setCachedWiql(cacheKey, response);
|
|
351
|
-
return response;
|
|
352
|
-
}
|
|
353
|
-
const emptyResponse = {
|
|
354
|
-
searchQuery: params.searchText,
|
|
355
|
-
totalResults: 0,
|
|
356
|
-
returnedResults: 0,
|
|
357
|
-
workItems: [],
|
|
358
|
-
wiql: query,
|
|
359
|
-
recentDaysApplied: recentDays,
|
|
360
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
361
|
-
advisory,
|
|
362
|
-
};
|
|
363
|
-
this.setCachedWiql(cacheKey, emptyResponse);
|
|
364
|
-
return emptyResponse;
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
console.error('Error searching work items:', error);
|
|
368
|
-
throw error;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Get recently updated work items
|
|
373
|
-
*/
|
|
374
|
-
async getRecentWorkItems(params) {
|
|
375
|
-
try {
|
|
376
|
-
const days = this.normalizeRecentDays(params.days);
|
|
377
|
-
const top = params.top || 10;
|
|
378
|
-
const skip = params.skip || 0;
|
|
379
|
-
const serverTop = skip + top;
|
|
380
|
-
const query = `SELECT [System.Id], [System.Title], [System.State], [System.ChangedDate]
|
|
381
|
-
FROM WorkItems
|
|
382
|
-
WHERE [System.TeamProject] = @project
|
|
383
|
-
AND [System.ChangedDate] >= @today - ${days}
|
|
384
|
-
ORDER BY [System.ChangedDate] DESC`;
|
|
385
|
-
const cacheKey = this.buildWiqlCacheKey(query, serverTop);
|
|
386
|
-
const cached = this.getCachedWiql(cacheKey);
|
|
387
|
-
if (cached)
|
|
388
|
-
return cached;
|
|
389
|
-
const throttleNotices = [];
|
|
390
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
391
|
-
const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
|
|
392
|
-
query
|
|
393
|
-
}, {
|
|
394
|
-
project: this.config.project
|
|
395
|
-
}, undefined, serverTop), {
|
|
396
|
-
operationName: 'workItems.recent.queryByWiql',
|
|
397
|
-
details: { project: this.config.project, top: serverTop, recentDays: days },
|
|
398
|
-
}, throttleNotices);
|
|
399
|
-
const pageRefs = queryResult.workItems ? queryResult.workItems.slice(skip, skip + top) : [];
|
|
400
|
-
const hydratedWorkItems = await this.hydrateWorkItemRefs(pageRefs, {
|
|
401
|
-
fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.recent.batchHydrate',
|
|
402
|
-
throttleAccumulator: throttleNotices,
|
|
403
|
-
});
|
|
404
|
-
const response = {
|
|
405
|
-
...queryResult,
|
|
406
|
-
workItems: hydratedWorkItems,
|
|
407
|
-
count: hydratedWorkItems.length,
|
|
408
|
-
recentDaysApplied: days,
|
|
409
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
410
|
-
};
|
|
411
|
-
this.setCachedWiql(cacheKey, response);
|
|
412
|
-
return response;
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
console.error('Error getting recent work items:', error);
|
|
416
|
-
throw error;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Get work items assigned to current user
|
|
421
|
-
*/
|
|
422
|
-
async getMyWorkItems(params) {
|
|
423
|
-
try {
|
|
424
|
-
let stateCondition = '';
|
|
425
|
-
if (params.state) {
|
|
426
|
-
stateCondition = `AND [System.State] = '${this.escapeWiqlLiteral(params.state)}'`;
|
|
427
|
-
}
|
|
428
|
-
const serverTop = params.top || 50;
|
|
429
|
-
const recentDays = this.normalizeRecentDays(params.days);
|
|
430
|
-
const query = `SELECT [System.Id], [System.Title], [System.State], [System.CreatedDate]
|
|
431
|
-
FROM WorkItems
|
|
432
|
-
WHERE [System.TeamProject] = @project
|
|
433
|
-
AND [System.AssignedTo] = @me
|
|
434
|
-
AND [System.ChangedDate] >= @today - ${recentDays}
|
|
435
|
-
${stateCondition}
|
|
436
|
-
ORDER BY [System.CreatedDate] DESC`;
|
|
437
|
-
const cacheKey = this.buildWiqlCacheKey(query, serverTop);
|
|
438
|
-
const cached = this.getCachedWiql(cacheKey);
|
|
439
|
-
if (cached)
|
|
440
|
-
return cached;
|
|
441
|
-
const throttleNotices = [];
|
|
442
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
443
|
-
const queryResult = await this.withAuthRetry(() => witApi.queryByWiql({
|
|
444
|
-
query
|
|
445
|
-
}, {
|
|
446
|
-
project: this.config.project
|
|
447
|
-
}, undefined, serverTop), {
|
|
448
|
-
operationName: 'workItems.mine.queryByWiql',
|
|
449
|
-
details: { project: this.config.project, top: serverTop, recentDays },
|
|
450
|
-
}, throttleNotices);
|
|
451
|
-
const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems || [], {
|
|
452
|
-
fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.mine.batchHydrate',
|
|
453
|
-
throttleAccumulator: throttleNotices,
|
|
454
|
-
});
|
|
455
|
-
const response = {
|
|
456
|
-
...queryResult,
|
|
457
|
-
workItems: hydratedWorkItems,
|
|
458
|
-
count: hydratedWorkItems.length,
|
|
459
|
-
recentDaysApplied: recentDays,
|
|
460
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
461
|
-
};
|
|
462
|
-
this.setCachedWiql(cacheKey, response);
|
|
463
|
-
return response;
|
|
464
|
-
}
|
|
465
|
-
catch (error) {
|
|
466
|
-
console.error('Error getting my work items:', error);
|
|
467
|
-
throw error;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Create a work item
|
|
472
|
-
*/
|
|
473
|
-
async createWorkItem(params) {
|
|
474
|
-
try {
|
|
475
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
476
|
-
const patchDocument = [];
|
|
477
|
-
// Add title
|
|
478
|
-
patchDocument.push({
|
|
479
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
480
|
-
path: "/fields/System.Title",
|
|
481
|
-
value: params.title
|
|
482
|
-
});
|
|
483
|
-
// Add description if provided (convert markdown to HTML for Azure DevOps rich-text field)
|
|
484
|
-
if (params.description) {
|
|
485
|
-
patchDocument.push({
|
|
486
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
487
|
-
path: "/fields/System.Description",
|
|
488
|
-
value: (0, formatHelpers_1.markdownToHtml)(params.description)
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
// Add assigned to if provided
|
|
492
|
-
if (params.assignedTo) {
|
|
493
|
-
patchDocument.push({
|
|
494
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
495
|
-
path: "/fields/System.AssignedTo",
|
|
496
|
-
value: params.assignedTo
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
// Add state if provided
|
|
500
|
-
if (params.state) {
|
|
501
|
-
patchDocument.push({
|
|
502
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
503
|
-
path: "/fields/System.State",
|
|
504
|
-
value: params.state
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
// Add area path if provided
|
|
508
|
-
if (params.areaPath) {
|
|
509
|
-
patchDocument.push({
|
|
510
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
511
|
-
path: "/fields/System.AreaPath",
|
|
512
|
-
value: params.areaPath
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
// Add iteration path if provided
|
|
516
|
-
if (params.iterationPath) {
|
|
517
|
-
patchDocument.push({
|
|
518
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
519
|
-
path: "/fields/System.IterationPath",
|
|
520
|
-
value: params.iterationPath
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
// Add additional fields if provided
|
|
524
|
-
if (params.additionalFields) {
|
|
525
|
-
for (const [key, value] of Object.entries(params.additionalFields)) {
|
|
526
|
-
patchDocument.push({
|
|
527
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
528
|
-
path: `/fields/${key}`,
|
|
529
|
-
value: value
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const workItem = await witApi.createWorkItem(undefined, patchDocument, this.config.project, params.workItemType);
|
|
534
|
-
return workItem;
|
|
535
|
-
}
|
|
536
|
-
catch (error) {
|
|
537
|
-
console.error('Error creating work item:', error);
|
|
538
|
-
throw error;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Update a work item
|
|
543
|
-
*/
|
|
544
|
-
async updateWorkItem(params) {
|
|
545
|
-
try {
|
|
546
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
547
|
-
const patchDocument = [];
|
|
548
|
-
// Add fields from the params (rich-text fields auto-converted from markdown to HTML)
|
|
549
|
-
for (const [key, value] of Object.entries(params.fields)) {
|
|
550
|
-
patchDocument.push({
|
|
551
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
552
|
-
path: `/fields/${key}`,
|
|
553
|
-
value: RICH_TEXT_FIELDS.has(key) && typeof value === 'string' ? (0, formatHelpers_1.markdownToHtml)(value) : value
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
|
|
557
|
-
return workItem;
|
|
558
|
-
}
|
|
559
|
-
catch (error) {
|
|
560
|
-
console.error(`Error updating work item ${params.id}:`, error);
|
|
561
|
-
throw error;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Get comments on a work item
|
|
566
|
-
*/
|
|
567
|
-
async getWorkItemComments(params) {
|
|
568
|
-
try {
|
|
569
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
570
|
-
const sortOrder = params.order === 'asc' ? WorkItemTrackingInterfaces_1.CommentSortOrder.Asc : WorkItemTrackingInterfaces_1.CommentSortOrder.Desc;
|
|
571
|
-
const commentList = await this.withAuthRetry(() => witApi.getComments(this.config.project, params.id, params.top, undefined, // continuationToken
|
|
572
|
-
params.includeDeleted ?? false, WorkItemTrackingInterfaces_1.CommentExpandOptions.None, sortOrder));
|
|
573
|
-
return commentList;
|
|
574
|
-
}
|
|
575
|
-
catch (error) {
|
|
576
|
-
console.error(`Error getting comments for work item ${params.id}:`, error);
|
|
577
|
-
throw error;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Add a comment to a work item.
|
|
582
|
-
* Uses ADO's server-side markdown renderer (format=markdown) for comments.
|
|
583
|
-
*/
|
|
584
|
-
async addWorkItemComment(params) {
|
|
585
|
-
try {
|
|
586
|
-
const format = params.format === 'html' ? 'html' : 'markdown';
|
|
587
|
-
const text = format === 'markdown' ? (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.text)) : params.text;
|
|
588
|
-
// Sanitise inputs before URL interpolation (SDK calls handle this internally, but this is a raw REST call)
|
|
589
|
-
const id = Math.floor(Number(params.id));
|
|
590
|
-
if (!Number.isSafeInteger(id) || id <= 0)
|
|
591
|
-
throw new Error(`Invalid work item ID: ${params.id}`);
|
|
592
|
-
const baseUrl = this.connection.serverUrl.replace(/\/+$/, '');
|
|
593
|
-
const project = encodeURIComponent(this.config.project);
|
|
594
|
-
const url = `${baseUrl}/${project}/_apis/wit/workItems/${id}/comments?format=${format}&api-version=7.2-preview.4`;
|
|
595
|
-
const response = await this.withAuthRetry(() => this.connection.rest.create(url, { text }));
|
|
596
|
-
if (!response.result) {
|
|
597
|
-
throw new Error(`Azure DevOps API returned no data when creating comment on work item ${params.id}`);
|
|
598
|
-
}
|
|
599
|
-
return response.result;
|
|
600
|
-
}
|
|
601
|
-
catch (error) {
|
|
602
|
-
console.error(`Error adding comment to work item ${params.id}:`, error);
|
|
603
|
-
throw error;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Update an existing comment on a work item.
|
|
608
|
-
* Uses ADO's server-side markdown renderer (format=markdown) for comments.
|
|
609
|
-
*/
|
|
610
|
-
async updateWorkItemComment(params) {
|
|
611
|
-
try {
|
|
612
|
-
const format = params.format === 'html' ? 'html' : 'markdown';
|
|
613
|
-
const text = format === 'markdown' ? (0, formatHelpers_1.normalizeLiteralEscapes)((0, formatHelpers_1.unescapeHtmlEntities)(params.text)) : params.text;
|
|
614
|
-
const id = Math.floor(Number(params.id));
|
|
615
|
-
if (!Number.isSafeInteger(id) || id <= 0)
|
|
616
|
-
throw new Error(`Invalid work item ID: ${params.id}`);
|
|
617
|
-
const commentId = Math.floor(Number(params.commentId));
|
|
618
|
-
if (!Number.isSafeInteger(commentId) || commentId <= 0)
|
|
619
|
-
throw new Error(`Invalid comment ID: ${params.commentId}`);
|
|
620
|
-
const baseUrl = this.connection.serverUrl.replace(/\/+$/, '');
|
|
621
|
-
const project = encodeURIComponent(this.config.project);
|
|
622
|
-
const url = `${baseUrl}/${project}/_apis/wit/workItems/${id}/comments/${commentId}?format=${format}&api-version=7.2-preview.4`;
|
|
623
|
-
const response = await this.withAuthRetry(() => this.connection.rest.update(url, { text }));
|
|
624
|
-
if (!response.result) {
|
|
625
|
-
throw new Error(`Azure DevOps API returned no data when updating comment ${params.commentId} on work item ${params.id}`);
|
|
626
|
-
}
|
|
627
|
-
return response.result;
|
|
628
|
-
}
|
|
629
|
-
catch (error) {
|
|
630
|
-
console.error(`Error updating comment ${params.commentId} on work item ${params.id}:`, error);
|
|
631
|
-
throw error;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Update work item state
|
|
636
|
-
*/
|
|
637
|
-
async updateWorkItemState(params) {
|
|
638
|
-
try {
|
|
639
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
640
|
-
const patchDocument = [
|
|
641
|
-
{
|
|
642
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
643
|
-
path: "/fields/System.State",
|
|
644
|
-
value: params.state
|
|
645
|
-
}
|
|
646
|
-
];
|
|
647
|
-
// Add comment if provided (convert markdown to HTML for rich-text field)
|
|
648
|
-
if (params.comment) {
|
|
649
|
-
patchDocument.push({
|
|
650
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
651
|
-
path: "/fields/System.History",
|
|
652
|
-
value: (0, formatHelpers_1.markdownToHtml)(params.comment)
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
|
|
656
|
-
return workItem;
|
|
657
|
-
}
|
|
658
|
-
catch (error) {
|
|
659
|
-
console.error(`Error updating state for work item ${params.id}:`, error);
|
|
660
|
-
throw error;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Assign work item to a user
|
|
665
|
-
*/
|
|
666
|
-
async assignWorkItem(params) {
|
|
667
|
-
try {
|
|
668
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
669
|
-
const patchDocument = [
|
|
670
|
-
{
|
|
671
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
672
|
-
path: "/fields/System.AssignedTo",
|
|
673
|
-
value: params.assignedTo
|
|
674
|
-
}
|
|
675
|
-
];
|
|
676
|
-
const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.id, this.config.project);
|
|
677
|
-
return workItem;
|
|
678
|
-
}
|
|
679
|
-
catch (error) {
|
|
680
|
-
console.error(`Error assigning work item ${params.id}:`, error);
|
|
681
|
-
throw error;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
/**
|
|
685
|
-
* Create a link between a work item and another work item or artifact
|
|
686
|
-
*/
|
|
687
|
-
async createLink(params) {
|
|
688
|
-
try {
|
|
689
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
690
|
-
let relationValue;
|
|
691
|
-
if (params.artifactUri) {
|
|
692
|
-
// Artifact link (PR, Build, Branch, Commit)
|
|
693
|
-
relationValue = {
|
|
694
|
-
rel: "ArtifactLink",
|
|
695
|
-
url: params.artifactUri,
|
|
696
|
-
attributes: {
|
|
697
|
-
comment: params.comment || "",
|
|
698
|
-
name: params.artifactName || ""
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
// Work item link (existing behavior)
|
|
704
|
-
relationValue = {
|
|
705
|
-
rel: params.linkType,
|
|
706
|
-
url: `${this.config.orgUrl}/_apis/wit/workItems/${params.targetWorkItemId}`,
|
|
707
|
-
attributes: {
|
|
708
|
-
comment: params.comment || ""
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
const patchDocument = [
|
|
713
|
-
{
|
|
714
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
715
|
-
path: "/relations/-",
|
|
716
|
-
value: relationValue
|
|
717
|
-
}
|
|
718
|
-
];
|
|
719
|
-
const workItem = await witApi.updateWorkItem(undefined, patchDocument, params.sourceId, this.config.project);
|
|
720
|
-
return workItem;
|
|
721
|
-
}
|
|
722
|
-
catch (error) {
|
|
723
|
-
console.error(`Error creating link:`, error);
|
|
724
|
-
throw error;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Bulk create or update work items
|
|
729
|
-
*/
|
|
730
|
-
async bulkUpdateWorkItems(params) {
|
|
731
|
-
try {
|
|
732
|
-
const results = [];
|
|
733
|
-
for (const workItemParams of params.workItems) {
|
|
734
|
-
if ('id' in workItemParams) {
|
|
735
|
-
// It's an update
|
|
736
|
-
const result = await this.updateWorkItem(workItemParams);
|
|
737
|
-
results.push(result);
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
// It's a create
|
|
741
|
-
const result = await this.createWorkItem(workItemParams);
|
|
742
|
-
results.push(result);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return {
|
|
746
|
-
count: results.length,
|
|
747
|
-
workItems: results
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
console.error('Error in bulk work item operation:', error);
|
|
752
|
-
throw error;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
// ── New Work Item Enhancement Methods ──────────────────────────
|
|
756
|
-
/**
|
|
757
|
-
* Get multiple work items by IDs in a single call.
|
|
758
|
-
*/
|
|
759
|
-
async getWorkItemsBatch(params) {
|
|
760
|
-
try {
|
|
761
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
762
|
-
const workItems = await witApi.getWorkItems(params.ids, params.fields, undefined, // asOf
|
|
763
|
-
WorkItemTrackingInterfaces_1.WorkItemExpand.Relations);
|
|
764
|
-
return workItems || [];
|
|
765
|
-
}
|
|
766
|
-
catch (error) {
|
|
767
|
-
console.error('Error getting work items batch:', error);
|
|
768
|
-
throw error;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Get revision history for a work item.
|
|
773
|
-
*/
|
|
774
|
-
async getWorkItemRevisions(params) {
|
|
775
|
-
try {
|
|
776
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
777
|
-
const revisions = await witApi.getRevisions(params.id, params.top, params.skip);
|
|
778
|
-
return revisions || [];
|
|
779
|
-
}
|
|
780
|
-
catch (error) {
|
|
781
|
-
console.error('Error getting work item revisions:', error);
|
|
782
|
-
throw error;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Execute a saved WIQL query by query ID and return the work items.
|
|
787
|
-
*/
|
|
788
|
-
async getQueryResults(params) {
|
|
789
|
-
try {
|
|
790
|
-
const throttleNotices = [];
|
|
791
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
792
|
-
const queryResult = await this.withAuthRetry(() => witApi.queryById(params.queryId, { project: this.config.project }), {
|
|
793
|
-
operationName: 'workItems.savedQuery.queryById',
|
|
794
|
-
details: { project: this.config.project, queryId: params.queryId },
|
|
795
|
-
}, throttleNotices);
|
|
796
|
-
if (!queryResult || !queryResult.workItems || queryResult.workItems.length === 0) {
|
|
797
|
-
return {
|
|
798
|
-
workItems: [],
|
|
799
|
-
count: 0,
|
|
800
|
-
queryType: queryResult?.queryType,
|
|
801
|
-
columns: queryResult?.columns?.map((c) => c.referenceName),
|
|
802
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
const hydratedWorkItems = await this.hydrateWorkItemRefs(queryResult.workItems, {
|
|
806
|
-
fields: params.fields, defaults: this.DEFAULT_FIELDS, operationName: 'workItems.savedQuery.batchHydrate',
|
|
807
|
-
throttleAccumulator: throttleNotices,
|
|
808
|
-
});
|
|
809
|
-
return {
|
|
810
|
-
workItems: hydratedWorkItems,
|
|
811
|
-
count: hydratedWorkItems.length,
|
|
812
|
-
queryType: queryResult.queryType,
|
|
813
|
-
columns: queryResult.columns?.map((c) => c.referenceName),
|
|
814
|
-
throttleInfo: AzureDevOpsService_1.AzureDevOpsService.buildThrottleInfo(throttleNotices),
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
catch (error) {
|
|
818
|
-
console.error('Error executing saved query:', error);
|
|
819
|
-
throw error;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Create a child work item linked to a parent.
|
|
824
|
-
*/
|
|
825
|
-
async addChildWorkItem(params) {
|
|
826
|
-
try {
|
|
827
|
-
// First create the work item
|
|
828
|
-
const createParams = {
|
|
829
|
-
workItemType: params.workItemType,
|
|
830
|
-
title: params.title,
|
|
831
|
-
description: params.description,
|
|
832
|
-
assignedTo: params.assignedTo,
|
|
833
|
-
state: params.state,
|
|
834
|
-
areaPath: params.areaPath,
|
|
835
|
-
iterationPath: params.iterationPath,
|
|
836
|
-
additionalFields: params.additionalFields,
|
|
837
|
-
};
|
|
838
|
-
const child = await this.createWorkItem(createParams);
|
|
839
|
-
if (!child || !child.id) {
|
|
840
|
-
throw new Error('Failed to create child work item.');
|
|
841
|
-
}
|
|
842
|
-
// Link child to parent
|
|
843
|
-
const patchDocument = [
|
|
844
|
-
{
|
|
845
|
-
op: VSSInterfaces_1.Operation.Add,
|
|
846
|
-
path: '/relations/-',
|
|
847
|
-
value: {
|
|
848
|
-
rel: 'System.LinkTypes.Hierarchy-Reverse',
|
|
849
|
-
url: `${this.config.orgUrl}/_apis/wit/workItems/${params.parentId}`,
|
|
850
|
-
attributes: { comment: 'Created as child work item' },
|
|
851
|
-
},
|
|
852
|
-
},
|
|
853
|
-
];
|
|
854
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
855
|
-
const updated = await witApi.updateWorkItem({}, // customHeaders
|
|
856
|
-
patchDocument, child.id, this.config.project);
|
|
857
|
-
return updated || child;
|
|
858
|
-
}
|
|
859
|
-
catch (error) {
|
|
860
|
-
console.error('Error creating child work item:', error);
|
|
861
|
-
throw error;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
/**
|
|
865
|
-
* Remove a relation (link) from a work item by relation index.
|
|
866
|
-
*/
|
|
867
|
-
async unlinkWorkItem(params) {
|
|
868
|
-
try {
|
|
869
|
-
const witApi = await this.getWorkItemTrackingApi();
|
|
870
|
-
const patchDocument = [
|
|
871
|
-
{
|
|
872
|
-
op: VSSInterfaces_1.Operation.Remove,
|
|
873
|
-
path: `/relations/${params.relationIndex}`,
|
|
874
|
-
},
|
|
875
|
-
];
|
|
876
|
-
return await witApi.updateWorkItem({}, patchDocument, params.id, this.config.project);
|
|
877
|
-
}
|
|
878
|
-
catch (error) {
|
|
879
|
-
console.error('Error unlinking work item:', error);
|
|
880
|
-
throw error;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
exports.WorkItemService = WorkItemService;
|
|
885
|
-
//# sourceMappingURL=WorkItemService.js.map
|