@mcp-consultant-tools/azure-devops-admin 27.0.0-beta.2 → 27.0.0-beta.20
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/build/AzureDevOpsAdminService.d.ts +50 -2
- package/build/AzureDevOpsAdminService.d.ts.map +1 -1
- package/build/AzureDevOpsAdminService.js +445 -16
- package/build/AzureDevOpsAdminService.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +301 -7
- package/build/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import axios from 'axios';
|
|
|
2
2
|
export class AzureDevOpsAdminService {
|
|
3
3
|
config;
|
|
4
4
|
baseUrl;
|
|
5
|
+
feedsUrl;
|
|
5
6
|
authHeader;
|
|
6
7
|
apiVersion;
|
|
7
8
|
constructor(config) {
|
|
@@ -18,8 +19,11 @@ export class AzureDevOpsAdminService {
|
|
|
18
19
|
enableAgentPoolDisable: config.enableAgentPoolDisable ?? false,
|
|
19
20
|
enableEnvironmentUpsert: config.enableEnvironmentUpsert ?? false,
|
|
20
21
|
enableEnvironmentDelete: config.enableEnvironmentDelete ?? false,
|
|
22
|
+
enableClassificationNodeUpsert: config.enableClassificationNodeUpsert ?? false,
|
|
23
|
+
enableClassificationNodeDelete: config.enableClassificationNodeDelete ?? false,
|
|
21
24
|
};
|
|
22
25
|
this.baseUrl = `https://dev.azure.com/${this.config.organization}`;
|
|
26
|
+
this.feedsUrl = `https://feeds.dev.azure.com/${this.config.organization}`;
|
|
23
27
|
this.apiVersion = this.config.apiVersion;
|
|
24
28
|
this.authHeader = `Basic ${Buffer.from(`:${this.config.pat}`).toString('base64')}`;
|
|
25
29
|
}
|
|
@@ -28,15 +32,38 @@ export class AzureDevOpsAdminService {
|
|
|
28
32
|
throw new Error(`Project '${project}' is not in the allowed projects list. Allowed projects: ${this.config.projects.join(', ')}`);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
validateFeed(feedName) {
|
|
36
|
+
if (this.config.feeds && this.config.feeds.length > 0
|
|
37
|
+
&& !this.config.feeds.includes(feedName)) {
|
|
38
|
+
throw new Error(`Feed '${feedName}' is not in the allowed feeds list. ` +
|
|
39
|
+
`Allowed feeds: ${this.config.feeds.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Converts a date string to ISO 8601 format with time component.
|
|
44
|
+
* Azure DevOps Classification Nodes API requires full ISO 8601 format.
|
|
45
|
+
* Input: "2026-02-16" → Output: "2026-02-16T00:00:00Z"
|
|
46
|
+
*/
|
|
47
|
+
formatDateForAdo(dateStr) {
|
|
48
|
+
// If already has time component, return as-is
|
|
49
|
+
if (dateStr.includes('T')) {
|
|
50
|
+
return dateStr;
|
|
51
|
+
}
|
|
52
|
+
// Add time component (midnight UTC)
|
|
53
|
+
return `${dateStr}T00:00:00Z`;
|
|
54
|
+
}
|
|
55
|
+
async makeRequest(endpoint, method = 'GET', data, customHeaders, contentType, baseUrlOverride) {
|
|
32
56
|
try {
|
|
33
|
-
const url = `${this.baseUrl}/${endpoint}`;
|
|
57
|
+
const url = `${baseUrlOverride || this.baseUrl}/${endpoint}`;
|
|
58
|
+
// Determine content type: explicit override > default based on method
|
|
59
|
+
// Note: Most PATCH endpoints use json-patch+json, but some (like classification nodes) use regular json
|
|
60
|
+
const defaultContentType = method === 'PATCH' ? 'application/json-patch+json' : 'application/json';
|
|
34
61
|
const response = await axios({
|
|
35
62
|
method,
|
|
36
63
|
url,
|
|
37
64
|
headers: {
|
|
38
65
|
'Authorization': this.authHeader,
|
|
39
|
-
'Content-Type':
|
|
66
|
+
'Content-Type': contentType || defaultContentType,
|
|
40
67
|
'Accept': 'application/json',
|
|
41
68
|
...customHeaders
|
|
42
69
|
},
|
|
@@ -57,7 +84,8 @@ export class AzureDevOpsAdminService {
|
|
|
57
84
|
throw new Error('Azure DevOps authentication failed. Please check your PAT token and permissions.');
|
|
58
85
|
}
|
|
59
86
|
if (error.response?.status === 403) {
|
|
60
|
-
|
|
87
|
+
const detail = typeof errorDetails === 'string' ? errorDetails : '';
|
|
88
|
+
throw new Error(`Azure DevOps access denied: ${detail || 'Please check your PAT scopes and project permissions.'}`);
|
|
61
89
|
}
|
|
62
90
|
if (error.response?.status === 404) {
|
|
63
91
|
throw new Error(`Azure DevOps resource not found: ${endpoint}`);
|
|
@@ -320,15 +348,63 @@ export class AzureDevOpsAdminService {
|
|
|
320
348
|
records: mappedRecords
|
|
321
349
|
};
|
|
322
350
|
}
|
|
323
|
-
|
|
351
|
+
/**
|
|
352
|
+
* Filter log content to reduce noise from progress indicators
|
|
353
|
+
* @param content Raw log content string
|
|
354
|
+
* @param mode Filter mode: 'summary' removes progress, 'errors' shows only errors, 'full' returns everything
|
|
355
|
+
*/
|
|
356
|
+
filterLogContent(content, mode) {
|
|
357
|
+
const lines = content.split('\n');
|
|
358
|
+
const originalLineCount = lines.length;
|
|
359
|
+
if (mode === 'full') {
|
|
360
|
+
return { filtered: content, originalLineCount, filteredLineCount: originalLineCount };
|
|
361
|
+
}
|
|
362
|
+
// Progress indicator patterns to remove in summary mode
|
|
363
|
+
const PROGRESS_PATTERNS = [
|
|
364
|
+
/remote: Counting objects:\s+\d+%/,
|
|
365
|
+
/remote: Compressing objects:\s+\d+%/,
|
|
366
|
+
/Receiving objects:\s+\d+%/,
|
|
367
|
+
/Resolving deltas:\s+\d+%/,
|
|
368
|
+
/Unpacking objects:\s+\d+%/,
|
|
369
|
+
/Updating files:\s+\d+%/,
|
|
370
|
+
];
|
|
371
|
+
// Error/warning patterns for errors mode
|
|
372
|
+
const ERROR_PATTERNS = [
|
|
373
|
+
/##\[error\]/i,
|
|
374
|
+
/##\[warning\]/i,
|
|
375
|
+
/\berror\b.*:/i,
|
|
376
|
+
/\bfailed\b/i,
|
|
377
|
+
/\bexception\b/i,
|
|
378
|
+
/\bfatal\b/i,
|
|
379
|
+
];
|
|
380
|
+
const filteredLines = lines.filter(line => {
|
|
381
|
+
// Remove timestamp prefix for pattern matching
|
|
382
|
+
const trimmedLine = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*/, '');
|
|
383
|
+
if (mode === 'errors') {
|
|
384
|
+
return ERROR_PATTERNS.some(p => p.test(trimmedLine));
|
|
385
|
+
}
|
|
386
|
+
// summary mode: exclude progress indicators, keep everything else
|
|
387
|
+
return !PROGRESS_PATTERNS.some(p => p.test(trimmedLine));
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
filtered: filteredLines.join('\n'),
|
|
391
|
+
originalLineCount,
|
|
392
|
+
filteredLineCount: filteredLines.length
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async getBuildLogs(project, buildId, logId, mode = 'summary') {
|
|
324
396
|
this.validateProject(project);
|
|
325
397
|
if (logId !== undefined) {
|
|
326
398
|
const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs/${logId}?api-version=${this.apiVersion}`);
|
|
399
|
+
const { filtered, originalLineCount, filteredLineCount } = this.filterLogContent(response, mode);
|
|
327
400
|
return {
|
|
328
401
|
buildId,
|
|
329
402
|
logId,
|
|
330
403
|
project,
|
|
331
|
-
|
|
404
|
+
mode,
|
|
405
|
+
originalLineCount,
|
|
406
|
+
filteredLineCount,
|
|
407
|
+
content: filtered
|
|
332
408
|
};
|
|
333
409
|
}
|
|
334
410
|
const response = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/logs?api-version=${this.apiVersion}`);
|
|
@@ -346,11 +422,46 @@ export class AzureDevOpsAdminService {
|
|
|
346
422
|
}))
|
|
347
423
|
};
|
|
348
424
|
}
|
|
349
|
-
async createPipelineDefinition(project, name, repositoryId, yamlPath, folder) {
|
|
425
|
+
async createPipelineDefinition(project, name, repositoryId, yamlPath, folder, repositoryType = 'TfsGit', repositoryUrl, defaultBranch, serviceConnectionId) {
|
|
350
426
|
this.validateProject(project);
|
|
351
427
|
if (!this.config.enablePipelineUpsert) {
|
|
352
428
|
throw new Error('Pipeline upsert operations are disabled. Set AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true to enable.');
|
|
353
429
|
}
|
|
430
|
+
// Validation for external repositories
|
|
431
|
+
if (repositoryType !== 'TfsGit') {
|
|
432
|
+
if (!repositoryUrl) {
|
|
433
|
+
throw new Error(`repositoryUrl is required for ${repositoryType} repositories`);
|
|
434
|
+
}
|
|
435
|
+
if (!serviceConnectionId) {
|
|
436
|
+
throw new Error(`serviceConnectionId is required for ${repositoryType} repositories`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Normalize defaultBranch format
|
|
440
|
+
const normalizedBranch = defaultBranch
|
|
441
|
+
? (defaultBranch.startsWith('refs/heads/') ? defaultBranch : `refs/heads/${defaultBranch}`)
|
|
442
|
+
: 'refs/heads/main';
|
|
443
|
+
// Build repository object based on type
|
|
444
|
+
let repository;
|
|
445
|
+
if (repositoryType === 'TfsGit') {
|
|
446
|
+
// Azure Repos - simple format
|
|
447
|
+
repository = {
|
|
448
|
+
id: repositoryId,
|
|
449
|
+
type: 'TfsGit'
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// GitHub / GitHubEnterprise - extended format
|
|
454
|
+
repository = {
|
|
455
|
+
id: repositoryId,
|
|
456
|
+
name: repositoryId, // Same as id for GitHub
|
|
457
|
+
type: repositoryType,
|
|
458
|
+
url: repositoryUrl,
|
|
459
|
+
defaultBranch: normalizedBranch,
|
|
460
|
+
properties: {
|
|
461
|
+
connectedServiceId: serviceConnectionId
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
354
465
|
const definition = {
|
|
355
466
|
name,
|
|
356
467
|
path: folder || '\\',
|
|
@@ -360,10 +471,7 @@ export class AzureDevOpsAdminService {
|
|
|
360
471
|
type: 2,
|
|
361
472
|
yamlFilename: yamlPath
|
|
362
473
|
},
|
|
363
|
-
repository
|
|
364
|
-
id: repositoryId,
|
|
365
|
-
type: 'TfsGit'
|
|
366
|
-
}
|
|
474
|
+
repository
|
|
367
475
|
};
|
|
368
476
|
const response = await this.makeRequest(`${project}/_apis/build/definitions?api-version=${this.apiVersion}`, 'POST', definition);
|
|
369
477
|
return {
|
|
@@ -372,6 +480,7 @@ export class AzureDevOpsAdminService {
|
|
|
372
480
|
path: response.path,
|
|
373
481
|
revision: response.revision,
|
|
374
482
|
project,
|
|
483
|
+
repositoryType,
|
|
375
484
|
url: response._links?.web?.href
|
|
376
485
|
};
|
|
377
486
|
}
|
|
@@ -463,6 +572,68 @@ export class AzureDevOpsAdminService {
|
|
|
463
572
|
};
|
|
464
573
|
}
|
|
465
574
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
575
|
+
// PIPELINE APPROVAL OPERATIONS
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
577
|
+
async listPendingApprovals(project, buildId) {
|
|
578
|
+
this.validateProject(project);
|
|
579
|
+
// Step 1: Fetch build timeline to find Checkpoint.Approval records
|
|
580
|
+
const timeline = await this.makeRequest(`${project}/_apis/build/builds/${buildId}/timeline?api-version=${this.apiVersion}`);
|
|
581
|
+
const approvalRecords = (timeline.records || []).filter((r) => r.type === 'Checkpoint.Approval');
|
|
582
|
+
if (approvalRecords.length === 0) {
|
|
583
|
+
return {
|
|
584
|
+
project,
|
|
585
|
+
buildId,
|
|
586
|
+
totalCount: 0,
|
|
587
|
+
approvals: [],
|
|
588
|
+
message: 'No approval checkpoints found for this build'
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
// Step 2: Extract approval IDs from record identifiers
|
|
592
|
+
const approvalIds = approvalRecords.map((r) => r.id);
|
|
593
|
+
// Step 3: Query the approvals API with those IDs
|
|
594
|
+
const approvalIdsParam = approvalIds.join(',');
|
|
595
|
+
const approvalsResponse = await this.makeRequest(`${project}/_apis/pipelines/approvals?approvalIds=${approvalIdsParam}&$expand=steps&api-version=${this.apiVersion}`);
|
|
596
|
+
const approvals = (approvalsResponse.value || [approvalsResponse]).map((a) => ({
|
|
597
|
+
id: a.id,
|
|
598
|
+
status: a.status,
|
|
599
|
+
createdOn: a.createdOn,
|
|
600
|
+
executionOrder: a.executionOrder,
|
|
601
|
+
minRequiredApprovers: a.minRequiredApprovers,
|
|
602
|
+
instructions: a.instructions,
|
|
603
|
+
blockedApprovers: a.blockedApprovers,
|
|
604
|
+
steps: (a.steps || []).map((s) => ({
|
|
605
|
+
assignedApprover: s.assignedApprover?.displayName,
|
|
606
|
+
assignedApproverId: s.assignedApprover?.uniqueName,
|
|
607
|
+
status: s.status,
|
|
608
|
+
comment: s.comment,
|
|
609
|
+
initiatedOn: s.initiatedOn,
|
|
610
|
+
lastModifiedOn: s.lastModifiedOn
|
|
611
|
+
}))
|
|
612
|
+
}));
|
|
613
|
+
return {
|
|
614
|
+
project,
|
|
615
|
+
buildId,
|
|
616
|
+
totalCount: approvals.length,
|
|
617
|
+
approvals
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
async approveStage(project, approvalId, status, comment) {
|
|
621
|
+
this.validateProject(project);
|
|
622
|
+
if (!this.config.enablePipelineUpsert) {
|
|
623
|
+
throw new Error('Pipeline upsert operations are disabled. Set AZUREDEVOPS_ENABLE_PIPELINE_UPSERT=true to enable.');
|
|
624
|
+
}
|
|
625
|
+
const body = [{ approvalId, status, comment: comment || '' }];
|
|
626
|
+
const response = await this.makeRequest(`${project}/_apis/pipelines/approvals?api-version=${this.apiVersion}`, 'PATCH', body, { 'Content-Type': 'application/json' });
|
|
627
|
+
const result = Array.isArray(response.value) ? response.value[0] : response;
|
|
628
|
+
return {
|
|
629
|
+
id: result.id,
|
|
630
|
+
status: result.status,
|
|
631
|
+
comment,
|
|
632
|
+
project,
|
|
633
|
+
message: `Approval ${approvalId} ${status}`
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
466
637
|
// SERVICE CONNECTION OPERATIONS
|
|
467
638
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
468
639
|
async listServiceConnections(project) {
|
|
@@ -689,7 +860,7 @@ export class AzureDevOpsAdminService {
|
|
|
689
860
|
name: name
|
|
690
861
|
}]
|
|
691
862
|
};
|
|
692
|
-
const response = await this.makeRequest(
|
|
863
|
+
const response = await this.makeRequest(`_apis/distributedtask/variablegroups?api-version=${this.apiVersion}`, 'POST', variableGroup);
|
|
693
864
|
return {
|
|
694
865
|
id: response.id,
|
|
695
866
|
name: response.name,
|
|
@@ -708,7 +879,7 @@ export class AzureDevOpsAdminService {
|
|
|
708
879
|
name: updates.name || current.name,
|
|
709
880
|
description: updates.description || current.description
|
|
710
881
|
};
|
|
711
|
-
const response = await this.makeRequest(
|
|
882
|
+
const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', updated);
|
|
712
883
|
return {
|
|
713
884
|
id: response.id,
|
|
714
885
|
name: response.name,
|
|
@@ -723,7 +894,7 @@ export class AzureDevOpsAdminService {
|
|
|
723
894
|
}
|
|
724
895
|
const current = await this.makeRequest(`${project}/_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`);
|
|
725
896
|
current.variables[variableName] = { value, isSecret };
|
|
726
|
-
const response = await this.makeRequest(
|
|
897
|
+
const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
|
|
727
898
|
return {
|
|
728
899
|
id: response.id,
|
|
729
900
|
name: response.name,
|
|
@@ -744,7 +915,7 @@ export class AzureDevOpsAdminService {
|
|
|
744
915
|
else {
|
|
745
916
|
throw new Error(`Variable '${variableName}' not found in group ${groupId}`);
|
|
746
917
|
}
|
|
747
|
-
const response = await this.makeRequest(
|
|
918
|
+
const response = await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'PUT', current);
|
|
748
919
|
return {
|
|
749
920
|
id: response.id,
|
|
750
921
|
name: response.name,
|
|
@@ -757,7 +928,7 @@ export class AzureDevOpsAdminService {
|
|
|
757
928
|
if (!this.config.enableVariableGroupDelete) {
|
|
758
929
|
throw new Error('Variable group delete operations are disabled. Set AZUREDEVOPS_ENABLE_VARIABLE_GROUP_DELETE=true to enable.');
|
|
759
930
|
}
|
|
760
|
-
await this.makeRequest(
|
|
931
|
+
await this.makeRequest(`_apis/distributedtask/variablegroups/${groupId}?api-version=${this.apiVersion}`, 'DELETE');
|
|
761
932
|
return {
|
|
762
933
|
groupId,
|
|
763
934
|
project,
|
|
@@ -1061,5 +1232,263 @@ export class AzureDevOpsAdminService {
|
|
|
1061
1232
|
deleted: true
|
|
1062
1233
|
};
|
|
1063
1234
|
}
|
|
1235
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1236
|
+
// CLASSIFICATION NODE OPERATIONS (Iterations & Areas)
|
|
1237
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1238
|
+
/**
|
|
1239
|
+
* Flatten classification node hierarchy into a list
|
|
1240
|
+
*/
|
|
1241
|
+
flattenClassificationNodes(node, parentPath = '') {
|
|
1242
|
+
const result = [];
|
|
1243
|
+
const currentPath = parentPath ? `${parentPath}\\${node.name}` : node.name;
|
|
1244
|
+
result.push({
|
|
1245
|
+
id: node.id,
|
|
1246
|
+
identifier: node.identifier,
|
|
1247
|
+
name: node.name,
|
|
1248
|
+
path: currentPath,
|
|
1249
|
+
structureType: node.structureType,
|
|
1250
|
+
hasChildren: node.hasChildren || false,
|
|
1251
|
+
attributes: node.attributes || {},
|
|
1252
|
+
url: node.url
|
|
1253
|
+
});
|
|
1254
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1255
|
+
for (const child of node.children) {
|
|
1256
|
+
result.push(...this.flattenClassificationNodes(child, currentPath));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return result;
|
|
1260
|
+
}
|
|
1261
|
+
async listClassificationNodes(project, structureType, depth = 10) {
|
|
1262
|
+
this.validateProject(project);
|
|
1263
|
+
const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}?$depth=${depth}&api-version=${this.apiVersion}`);
|
|
1264
|
+
const nodes = this.flattenClassificationNodes(response);
|
|
1265
|
+
return {
|
|
1266
|
+
project,
|
|
1267
|
+
structureType,
|
|
1268
|
+
totalCount: nodes.length,
|
|
1269
|
+
nodes: nodes.map((node) => ({
|
|
1270
|
+
id: node.id,
|
|
1271
|
+
identifier: node.identifier,
|
|
1272
|
+
name: node.name,
|
|
1273
|
+
path: node.path,
|
|
1274
|
+
hasChildren: node.hasChildren,
|
|
1275
|
+
...(structureType === 'iterations' && node.attributes ? {
|
|
1276
|
+
startDate: node.attributes.startDate,
|
|
1277
|
+
finishDate: node.attributes.finishDate,
|
|
1278
|
+
timeFrame: node.attributes.timeFrame
|
|
1279
|
+
} : {})
|
|
1280
|
+
}))
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
async getClassificationNode(project, structureType, path) {
|
|
1284
|
+
this.validateProject(project);
|
|
1285
|
+
// Path should be URL encoded and without leading backslash
|
|
1286
|
+
const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
|
|
1287
|
+
const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
|
|
1288
|
+
const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?api-version=${this.apiVersion}`);
|
|
1289
|
+
return {
|
|
1290
|
+
id: response.id,
|
|
1291
|
+
identifier: response.identifier,
|
|
1292
|
+
name: response.name,
|
|
1293
|
+
structureType: response.structureType,
|
|
1294
|
+
hasChildren: response.hasChildren || false,
|
|
1295
|
+
path: response.path,
|
|
1296
|
+
project,
|
|
1297
|
+
...(structureType === 'iterations' && response.attributes ? {
|
|
1298
|
+
startDate: response.attributes.startDate,
|
|
1299
|
+
finishDate: response.attributes.finishDate,
|
|
1300
|
+
timeFrame: response.attributes.timeFrame
|
|
1301
|
+
} : {}),
|
|
1302
|
+
url: response.url
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async createClassificationNode(project, structureType, name, parentPath, attributes) {
|
|
1306
|
+
this.validateProject(project);
|
|
1307
|
+
if (!this.config.enableClassificationNodeUpsert) {
|
|
1308
|
+
throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
|
|
1309
|
+
}
|
|
1310
|
+
const body = { name };
|
|
1311
|
+
if (structureType === 'iterations' && attributes) {
|
|
1312
|
+
body.attributes = {};
|
|
1313
|
+
// Azure DevOps requires ISO 8601 format with time component
|
|
1314
|
+
if (attributes.startDate)
|
|
1315
|
+
body.attributes.startDate = this.formatDateForAdo(attributes.startDate);
|
|
1316
|
+
if (attributes.finishDate)
|
|
1317
|
+
body.attributes.finishDate = this.formatDateForAdo(attributes.finishDate);
|
|
1318
|
+
}
|
|
1319
|
+
// Build the endpoint - if parentPath is provided, create under that path
|
|
1320
|
+
let endpoint = `${project}/_apis/wit/classificationnodes/${structureType}`;
|
|
1321
|
+
if (parentPath) {
|
|
1322
|
+
const cleanPath = parentPath.replace(/^\\/, '').replace(/\\/g, '/');
|
|
1323
|
+
const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
|
|
1324
|
+
endpoint = `${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}`;
|
|
1325
|
+
}
|
|
1326
|
+
const response = await this.makeRequest(`${endpoint}?api-version=${this.apiVersion}`, 'POST', body);
|
|
1327
|
+
return {
|
|
1328
|
+
id: response.id,
|
|
1329
|
+
identifier: response.identifier,
|
|
1330
|
+
name: response.name,
|
|
1331
|
+
path: response.path,
|
|
1332
|
+
structureType,
|
|
1333
|
+
project,
|
|
1334
|
+
...(structureType === 'iterations' && response.attributes ? {
|
|
1335
|
+
startDate: response.attributes.startDate,
|
|
1336
|
+
finishDate: response.attributes.finishDate
|
|
1337
|
+
} : {}),
|
|
1338
|
+
url: response.url
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
async addIterationToTeam(project, team, iterationId) {
|
|
1342
|
+
this.validateProject(project);
|
|
1343
|
+
if (!this.config.enableClassificationNodeUpsert) {
|
|
1344
|
+
throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
|
|
1345
|
+
}
|
|
1346
|
+
const encodedTeam = encodeURIComponent(team);
|
|
1347
|
+
const endpoint = `${project}/${encodedTeam}/_apis/work/teamsettings/iterations?api-version=${this.apiVersion}`;
|
|
1348
|
+
const response = await this.makeRequest(endpoint, 'POST', { id: iterationId });
|
|
1349
|
+
return {
|
|
1350
|
+
id: response.id,
|
|
1351
|
+
name: response.name,
|
|
1352
|
+
path: response.path,
|
|
1353
|
+
url: response.url,
|
|
1354
|
+
attributes: response.attributes ? {
|
|
1355
|
+
startDate: response.attributes.startDate,
|
|
1356
|
+
finishDate: response.attributes.finishDate,
|
|
1357
|
+
timeFrame: response.attributes.timeFrame,
|
|
1358
|
+
} : undefined,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
async updateClassificationNode(project, structureType, path, updates) {
|
|
1362
|
+
this.validateProject(project);
|
|
1363
|
+
if (!this.config.enableClassificationNodeUpsert) {
|
|
1364
|
+
throw new Error('Classification node upsert operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_UPSERT=true to enable.');
|
|
1365
|
+
}
|
|
1366
|
+
const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
|
|
1367
|
+
const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
|
|
1368
|
+
const body = {};
|
|
1369
|
+
if (updates.name)
|
|
1370
|
+
body.name = updates.name;
|
|
1371
|
+
if (structureType === 'iterations') {
|
|
1372
|
+
if (updates.startDate !== undefined || updates.finishDate !== undefined) {
|
|
1373
|
+
body.attributes = {};
|
|
1374
|
+
// Azure DevOps requires ISO 8601 format with time component
|
|
1375
|
+
if (updates.startDate !== undefined)
|
|
1376
|
+
body.attributes.startDate = this.formatDateForAdo(updates.startDate);
|
|
1377
|
+
if (updates.finishDate !== undefined)
|
|
1378
|
+
body.attributes.finishDate = this.formatDateForAdo(updates.finishDate);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
// Classification Nodes API uses regular JSON, not JSON Patch format
|
|
1382
|
+
const response = await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?api-version=${this.apiVersion}`, 'PATCH', body, undefined, 'application/json' // Override default json-patch+json content type
|
|
1383
|
+
);
|
|
1384
|
+
return {
|
|
1385
|
+
id: response.id,
|
|
1386
|
+
identifier: response.identifier,
|
|
1387
|
+
name: response.name,
|
|
1388
|
+
path: response.path,
|
|
1389
|
+
structureType,
|
|
1390
|
+
project,
|
|
1391
|
+
...(structureType === 'iterations' && response.attributes ? {
|
|
1392
|
+
startDate: response.attributes.startDate,
|
|
1393
|
+
finishDate: response.attributes.finishDate
|
|
1394
|
+
} : {}),
|
|
1395
|
+
url: response.url
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async deleteClassificationNode(project, structureType, path, reclassifyId) {
|
|
1399
|
+
this.validateProject(project);
|
|
1400
|
+
if (!this.config.enableClassificationNodeDelete) {
|
|
1401
|
+
throw new Error('Classification node delete operations are disabled. Set AZUREDEVOPS_ENABLE_CLASSIFICATION_NODE_DELETE=true to enable.');
|
|
1402
|
+
}
|
|
1403
|
+
const cleanPath = path.replace(/^\\/, '').replace(/\\/g, '/');
|
|
1404
|
+
const encodedPath = encodeURIComponent(cleanPath).replace(/%2F/g, '/');
|
|
1405
|
+
await this.makeRequest(`${project}/_apis/wit/classificationnodes/${structureType}/${encodedPath}?$reclassifyId=${reclassifyId}&api-version=${this.apiVersion}`, 'DELETE');
|
|
1406
|
+
return {
|
|
1407
|
+
path,
|
|
1408
|
+
structureType,
|
|
1409
|
+
project,
|
|
1410
|
+
reclassifyId,
|
|
1411
|
+
deleted: true
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
// ========================================
|
|
1415
|
+
// ARTIFACT FEED METHODS
|
|
1416
|
+
// ========================================
|
|
1417
|
+
async listFeedPackages(feedName, options) {
|
|
1418
|
+
this.validateFeed(feedName);
|
|
1419
|
+
if (options?.project) {
|
|
1420
|
+
this.validateProject(options.project);
|
|
1421
|
+
}
|
|
1422
|
+
const projectPrefix = options?.project ? `${options.project}/` : '';
|
|
1423
|
+
const queryParams = new URLSearchParams();
|
|
1424
|
+
queryParams.append('api-version', this.apiVersion);
|
|
1425
|
+
if (options?.namePrefix)
|
|
1426
|
+
queryParams.append('packageNameQuery', options.namePrefix);
|
|
1427
|
+
if (options?.packageType)
|
|
1428
|
+
queryParams.append('protocolType', options.packageType);
|
|
1429
|
+
if (options?.top)
|
|
1430
|
+
queryParams.append('$top', String(options.top));
|
|
1431
|
+
const endpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages?${queryParams.toString()}`;
|
|
1432
|
+
const response = await this.makeRequest(endpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
|
|
1433
|
+
return {
|
|
1434
|
+
feed: feedName,
|
|
1435
|
+
project: options?.project || '(org-scoped)',
|
|
1436
|
+
totalCount: response.value.length,
|
|
1437
|
+
packages: response.value.map((pkg) => ({
|
|
1438
|
+
id: pkg.id,
|
|
1439
|
+
name: pkg.name,
|
|
1440
|
+
protocolType: pkg.protocolType,
|
|
1441
|
+
latestVersion: pkg.versions?.[0]?.version,
|
|
1442
|
+
publishDate: pkg.versions?.[0]?.publishDate,
|
|
1443
|
+
description: pkg.description,
|
|
1444
|
+
})),
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async getPackageVersions(feedName, packageName, options) {
|
|
1448
|
+
this.validateFeed(feedName);
|
|
1449
|
+
if (options?.project) {
|
|
1450
|
+
this.validateProject(options.project);
|
|
1451
|
+
}
|
|
1452
|
+
// Step 1: Resolve package name → package ID
|
|
1453
|
+
const projectPrefix = options?.project ? `${options.project}/` : '';
|
|
1454
|
+
const listParams = new URLSearchParams();
|
|
1455
|
+
listParams.append('api-version', this.apiVersion);
|
|
1456
|
+
listParams.append('packageNameQuery', packageName);
|
|
1457
|
+
if (options?.packageType)
|
|
1458
|
+
listParams.append('protocolType', options.packageType);
|
|
1459
|
+
const listEndpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages?${listParams.toString()}`;
|
|
1460
|
+
const listResponse = await this.makeRequest(listEndpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
|
|
1461
|
+
const exactMatch = listResponse.value.find((pkg) => pkg.name.toLowerCase() === packageName.toLowerCase());
|
|
1462
|
+
if (!exactMatch) {
|
|
1463
|
+
const available = listResponse.value.map((pkg) => pkg.name).slice(0, 10);
|
|
1464
|
+
throw new Error(`Package '${packageName}' not found in feed '${feedName}'. ` +
|
|
1465
|
+
(available.length > 0
|
|
1466
|
+
? `Similar packages: ${available.join(', ')}`
|
|
1467
|
+
: 'No matching packages found.'));
|
|
1468
|
+
}
|
|
1469
|
+
// Step 2: Get versions for the resolved package ID
|
|
1470
|
+
const versionParams = new URLSearchParams();
|
|
1471
|
+
versionParams.append('api-version', this.apiVersion);
|
|
1472
|
+
if (options?.top)
|
|
1473
|
+
versionParams.append('$top', String(options.top));
|
|
1474
|
+
if (options?.includeDelisted)
|
|
1475
|
+
versionParams.append('includeDelisted', 'true');
|
|
1476
|
+
const versionEndpoint = `${projectPrefix}_apis/packaging/Feeds/${encodeURIComponent(feedName)}/packages/${exactMatch.id}/versions?${versionParams.toString()}`;
|
|
1477
|
+
const versionResponse = await this.makeRequest(versionEndpoint, 'GET', undefined, undefined, undefined, this.feedsUrl);
|
|
1478
|
+
return {
|
|
1479
|
+
feed: feedName,
|
|
1480
|
+
project: options?.project || '(org-scoped)',
|
|
1481
|
+
packageName: exactMatch.name,
|
|
1482
|
+
packageId: exactMatch.id,
|
|
1483
|
+
protocolType: exactMatch.protocolType,
|
|
1484
|
+
versions: versionResponse.value.map((v) => ({
|
|
1485
|
+
version: v.version,
|
|
1486
|
+
publishDate: v.publishDate,
|
|
1487
|
+
isLatest: v.isLatest ?? false,
|
|
1488
|
+
isListed: v.isListed ?? true,
|
|
1489
|
+
description: v.description,
|
|
1490
|
+
})),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1064
1493
|
}
|
|
1065
1494
|
//# sourceMappingURL=AzureDevOpsAdminService.js.map
|