@elisra-devops/docgen-data-provider 1.69.14 → 1.69.15
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/bin/modules/PipelinesDataProvider.d.ts +2 -1
- package/bin/modules/PipelinesDataProvider.js +73 -35
- package/bin/modules/PipelinesDataProvider.js.map +1 -1
- package/bin/tests/modules/pipelineDataProvider.test.js +101 -186
- package/bin/tests/modules/pipelineDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/PipelinesDataProvider.ts +117 -32
- package/src/tests/modules/pipelineDataProvider.test.ts +135 -206
|
@@ -218,6 +218,21 @@ export default class PipelinesDataProvider {
|
|
|
218
218
|
return `refs/heads/${b}`;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
private tryBuildBuildApiUrlFromPipelinesApiUrl(url?: string): string | undefined {
|
|
222
|
+
if (!url) return undefined;
|
|
223
|
+
try {
|
|
224
|
+
const parsed = new URL(url);
|
|
225
|
+
parsed.search = '';
|
|
226
|
+
const before = parsed.pathname;
|
|
227
|
+
const after = before.replace('/_apis/pipelines/', '/_apis/build/builds/');
|
|
228
|
+
if (after === before) return undefined;
|
|
229
|
+
parsed.pathname = after;
|
|
230
|
+
return parsed.toString();
|
|
231
|
+
} catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
221
236
|
/**
|
|
222
237
|
* Searches for a build by build number without restricting by definition id.
|
|
223
238
|
*
|
|
@@ -272,8 +287,7 @@ export default class PipelinesDataProvider {
|
|
|
272
287
|
if (projectName) {
|
|
273
288
|
try {
|
|
274
289
|
return await this.getPipelineBuildByBuildId(projectName, buildId);
|
|
275
|
-
} catch (e1: any) {
|
|
276
|
-
}
|
|
290
|
+
} catch (e1: any) {}
|
|
277
291
|
}
|
|
278
292
|
|
|
279
293
|
// Some ADO instances allow build lookup without the {project} path segment.
|
|
@@ -350,7 +364,9 @@ export default class PipelinesDataProvider {
|
|
|
350
364
|
return undefined;
|
|
351
365
|
} catch (e: any) {
|
|
352
366
|
logger.error(
|
|
353
|
-
`Error resolving runId by runName for pipeline ${pipelineId} (${projectName}/${desired}): ${
|
|
367
|
+
`Error resolving runId by runName for pipeline ${pipelineId} (${projectName}/${desired}): ${
|
|
368
|
+
e?.message || e
|
|
369
|
+
}`
|
|
354
370
|
);
|
|
355
371
|
return undefined;
|
|
356
372
|
}
|
|
@@ -431,9 +447,7 @@ export default class PipelinesDataProvider {
|
|
|
431
447
|
const definitionType = buildResponse?.definition?.type;
|
|
432
448
|
const repoType = buildResponse?.repository?.type;
|
|
433
449
|
return (
|
|
434
|
-
!!buildResponse &&
|
|
435
|
-
definitionType === 'build' &&
|
|
436
|
-
(repoType === 'TfsGit' || repoType === 'azureReposGit')
|
|
450
|
+
!!buildResponse && definitionType === 'build' && (repoType === 'TfsGit' || repoType === 'azureReposGit')
|
|
437
451
|
);
|
|
438
452
|
}
|
|
439
453
|
|
|
@@ -461,17 +475,18 @@ export default class PipelinesDataProvider {
|
|
|
461
475
|
const resourcePipelinesByKey: Map<string, any> = new Map();
|
|
462
476
|
|
|
463
477
|
if (!inPipeline?.resources?.pipelines) {
|
|
464
|
-
|
|
478
|
+
logger.debug('getPipelineResourcePipelinesFromObject: no pipeline resources on run');
|
|
479
|
+
return [];
|
|
465
480
|
}
|
|
466
481
|
const pipelineEntries = Object.entries(inPipeline.resources.pipelines);
|
|
482
|
+
logger.debug(
|
|
483
|
+
`getPipelineResourcePipelinesFromObject: resolving ${pipelineEntries.length} pipeline resources`
|
|
484
|
+
);
|
|
467
485
|
|
|
468
486
|
await Promise.all(
|
|
469
487
|
pipelineEntries.map(async ([resourcePipelineAlias, resource]) => {
|
|
470
488
|
const resourcePipelineObj = (resource as any)?.pipeline;
|
|
471
489
|
const pipelineIdCandidate = Number(resourcePipelineObj?.id);
|
|
472
|
-
const source = (resource as any)?.source;
|
|
473
|
-
const version = (resource as any)?.version;
|
|
474
|
-
const branch = (resource as any)?.branch;
|
|
475
490
|
|
|
476
491
|
const rawProjectName =
|
|
477
492
|
(resource as any)?.project?.name ||
|
|
@@ -480,38 +495,101 @@ export default class PipelinesDataProvider {
|
|
|
480
495
|
const projectName = await this.normalizeProjectName(rawProjectName);
|
|
481
496
|
|
|
482
497
|
if (!Number.isFinite(pipelineIdCandidate)) {
|
|
498
|
+
logger.warn(
|
|
499
|
+
`getPipelineResourcePipelinesFromObject: resource ${resourcePipelineAlias} missing numeric pipeline id`
|
|
500
|
+
);
|
|
483
501
|
return;
|
|
484
502
|
}
|
|
485
503
|
|
|
486
|
-
let
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
504
|
+
let buildResponse: any;
|
|
505
|
+
const fixedUrl = this.tryBuildBuildApiUrlFromPipelinesApiUrl(resourcePipelineObj?.url);
|
|
506
|
+
if (fixedUrl) {
|
|
507
|
+
try {
|
|
508
|
+
const fixedBuildResponse = await TFSServices.getItemContent(
|
|
509
|
+
fixedUrl,
|
|
510
|
+
this.token,
|
|
511
|
+
'get',
|
|
512
|
+
null,
|
|
513
|
+
null
|
|
514
|
+
);
|
|
515
|
+
if (this.isSupportedResourcePipelineBuild(fixedBuildResponse)) {
|
|
516
|
+
buildResponse = fixedBuildResponse;
|
|
517
|
+
}
|
|
518
|
+
} catch (err: any) {
|
|
519
|
+
logger.error(
|
|
520
|
+
`Error fetching pipeline ${resourcePipelineAlias} via fixed build url ${fixedUrl} : ${err.message}`
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
logger.warn(
|
|
525
|
+
`getPipelineResourcePipelinesFromObject: could not convert pipelines url to builds url for ${resourcePipelineAlias}: ${String(
|
|
526
|
+
resourcePipelineObj?.url || ''
|
|
527
|
+
)}`
|
|
528
|
+
);
|
|
495
529
|
}
|
|
496
530
|
|
|
497
|
-
|
|
531
|
+
// PowerShell-style logic: do not fall back to runId/buildNumber/run-history resolution.
|
|
532
|
+
if (!this.isSupportedResourcePipelineBuild(buildResponse)) {
|
|
533
|
+
logger.debug(
|
|
534
|
+
`getPipelineResourcePipelinesFromObject: skipping ${resourcePipelineAlias} because resolved build is missing/unsupported (project=${String(
|
|
535
|
+
projectName || ''
|
|
536
|
+
)}, pipelineIdCandidate=${pipelineIdCandidate})`
|
|
537
|
+
);
|
|
498
538
|
return;
|
|
499
539
|
}
|
|
500
540
|
|
|
501
|
-
let buildResponse: any;
|
|
502
|
-
try {
|
|
503
|
-
buildResponse = await this.tryGetBuildByIdWithFallback(projectName, runId);
|
|
504
|
-
} catch (err: any) {
|
|
505
|
-
logger.error(`Error fetching pipeline ${resourcePipelineAlias} run ${runId} : ${err.message}`);
|
|
506
|
-
}
|
|
507
541
|
if (this.isSupportedResourcePipelineBuild(buildResponse)) {
|
|
542
|
+
const buildNumber = String(buildResponse.buildNumber || '').trim();
|
|
543
|
+
if (!buildNumber) {
|
|
544
|
+
logger.warn(
|
|
545
|
+
`Resource pipeline ${resourcePipelineAlias} resolved to buildId=${String(
|
|
546
|
+
buildResponse?.id
|
|
547
|
+
)} but buildNumber is missing, skipping`
|
|
548
|
+
);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const expectedProjectName = String(projectName || '').trim();
|
|
553
|
+
const actualProjectName = String(buildResponse?.project?.name || '').trim();
|
|
554
|
+
if (
|
|
555
|
+
expectedProjectName &&
|
|
556
|
+
actualProjectName &&
|
|
557
|
+
expectedProjectName.toLowerCase() !== actualProjectName.toLowerCase()
|
|
558
|
+
) {
|
|
559
|
+
logger.warn(
|
|
560
|
+
`Resource pipeline ${resourcePipelineAlias} resolved to buildId=${String(
|
|
561
|
+
buildResponse?.id
|
|
562
|
+
)} but project mismatch expected=${expectedProjectName} actual=${actualProjectName}, skipping`
|
|
563
|
+
);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const expectedPipelineName = String(resourcePipelineObj?.name || '').trim();
|
|
568
|
+
const actualPipelineName = String(buildResponse?.definition?.name || '').trim();
|
|
569
|
+
if (
|
|
570
|
+
expectedPipelineName &&
|
|
571
|
+
actualPipelineName &&
|
|
572
|
+
expectedPipelineName.toLowerCase() !== actualPipelineName.toLowerCase()
|
|
573
|
+
) {
|
|
574
|
+
logger.warn(
|
|
575
|
+
`Resource pipeline ${resourcePipelineAlias} resolved to buildId=${String(
|
|
576
|
+
buildResponse?.id
|
|
577
|
+
)} but pipeline name mismatch expected=${expectedPipelineName} actual=${actualPipelineName}, skipping`
|
|
578
|
+
);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
508
581
|
const definitionId = buildResponse.definition?.id ?? pipelineIdCandidate;
|
|
509
|
-
const buildId = buildResponse.id
|
|
582
|
+
const buildId = buildResponse.id;
|
|
583
|
+
logger.debug(
|
|
584
|
+
`getPipelineResourcePipelinesFromObject: resolved ${resourcePipelineAlias} -> ${String(
|
|
585
|
+
buildResponse?.project?.name || projectName || ''
|
|
586
|
+
)}/${definitionId} runId=${buildId} repoType=${String(buildResponse?.repository?.type || '')}`
|
|
587
|
+
);
|
|
510
588
|
const resourcePipelineToAdd = {
|
|
511
|
-
name: resourcePipelineAlias,
|
|
589
|
+
name: resourcePipelineObj?.name ?? resourcePipelineAlias,
|
|
512
590
|
buildId,
|
|
513
591
|
definitionId,
|
|
514
|
-
buildNumber
|
|
592
|
+
buildNumber,
|
|
515
593
|
teamProject: buildResponse.project?.name ?? projectName,
|
|
516
594
|
provider: buildResponse.repository?.type,
|
|
517
595
|
};
|
|
@@ -543,7 +621,7 @@ export default class PipelinesDataProvider {
|
|
|
543
621
|
|
|
544
622
|
for (const prop in repositories) {
|
|
545
623
|
const resourceRepo = repositories[prop];
|
|
546
|
-
if (resourceRepo?.repository?.type
|
|
624
|
+
if (!['azureReposGit', 'TfsGit'].includes(resourceRepo?.repository?.type)) {
|
|
547
625
|
continue;
|
|
548
626
|
}
|
|
549
627
|
const repoId = resourceRepo.repository.id;
|
|
@@ -706,11 +784,18 @@ export default class PipelinesDataProvider {
|
|
|
706
784
|
url += `&continuationToken=${encodeURIComponent(continuationToken)}`;
|
|
707
785
|
}
|
|
708
786
|
try {
|
|
709
|
-
const { data, headers } = await TFSServices.getItemContentWithHeaders(
|
|
787
|
+
const { data, headers } = await TFSServices.getItemContentWithHeaders(
|
|
788
|
+
url,
|
|
789
|
+
this.token,
|
|
790
|
+
'get',
|
|
791
|
+
null,
|
|
792
|
+
null
|
|
793
|
+
);
|
|
710
794
|
const { value = [] } = data || {};
|
|
711
795
|
all.push(...value);
|
|
712
796
|
// Azure DevOps returns continuation token header for next page
|
|
713
|
-
continuationToken =
|
|
797
|
+
continuationToken =
|
|
798
|
+
headers?.['x-ms-continuationtoken'] || headers?.['x-ms-continuation-token'] || undefined;
|
|
714
799
|
page++;
|
|
715
800
|
logger.debug(`GetAllReleaseHistory: fetched page ${page}, cumulative ${all.length} releases`);
|
|
716
801
|
} catch (err: any) {
|
|
@@ -681,7 +681,7 @@ describe('PipelinesDataProvider', () => {
|
|
|
681
681
|
});
|
|
682
682
|
|
|
683
683
|
describe('getPipelineResourcePipelinesFromObject', () => {
|
|
684
|
-
it('should return empty
|
|
684
|
+
it('should return empty array when no pipeline resources exist', async () => {
|
|
685
685
|
// Arrange
|
|
686
686
|
const inPipeline = {
|
|
687
687
|
resources: {},
|
|
@@ -691,7 +691,7 @@ describe('PipelinesDataProvider', () => {
|
|
|
691
691
|
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
692
692
|
|
|
693
693
|
// Assert
|
|
694
|
-
expect(result).toEqual(
|
|
694
|
+
expect(result).toEqual([]);
|
|
695
695
|
});
|
|
696
696
|
|
|
697
697
|
it('should extract pipeline resources from pipeline object', async () => {
|
|
@@ -714,11 +714,11 @@ describe('PipelinesDataProvider', () => {
|
|
|
714
714
|
id: 789,
|
|
715
715
|
definition: { id: 123, type: 'build' },
|
|
716
716
|
buildNumber: '20231201.1',
|
|
717
|
-
project: { name: '
|
|
717
|
+
project: { name: 'project' },
|
|
718
718
|
repository: { type: 'TfsGit' },
|
|
719
719
|
};
|
|
720
720
|
|
|
721
|
-
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockBuildResponse);
|
|
721
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockBuildResponse); // fixed-url attempt
|
|
722
722
|
|
|
723
723
|
// Act
|
|
724
724
|
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
@@ -730,7 +730,7 @@ describe('PipelinesDataProvider', () => {
|
|
|
730
730
|
buildId: 789,
|
|
731
731
|
definitionId: 123,
|
|
732
732
|
buildNumber: '20231201.1',
|
|
733
|
-
teamProject: '
|
|
733
|
+
teamProject: 'project',
|
|
734
734
|
provider: 'TfsGit',
|
|
735
735
|
});
|
|
736
736
|
});
|
|
@@ -760,226 +760,35 @@ describe('PipelinesDataProvider', () => {
|
|
|
760
760
|
expect(result).toEqual([]);
|
|
761
761
|
});
|
|
762
762
|
|
|
763
|
-
it('should
|
|
764
|
-
// Arrange
|
|
765
|
-
const inPipeline = {
|
|
766
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/10/runs/200',
|
|
767
|
-
resources: {
|
|
768
|
-
pipelines: {
|
|
769
|
-
SOME_PACKAGE: {
|
|
770
|
-
pipeline: {
|
|
771
|
-
id: 123,
|
|
772
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/123?revision=1',
|
|
773
|
-
},
|
|
774
|
-
project: { name: 'project1' },
|
|
775
|
-
source: 'project1-system-package',
|
|
776
|
-
version: '1.0.56',
|
|
777
|
-
branch: 'main',
|
|
778
|
-
},
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
} as unknown as PipelineRun;
|
|
782
|
-
|
|
783
|
-
const mockListBuildsResponse = {
|
|
784
|
-
value: [
|
|
785
|
-
{
|
|
786
|
-
id: 789,
|
|
787
|
-
},
|
|
788
|
-
],
|
|
789
|
-
};
|
|
790
|
-
const mockBuildResponse = {
|
|
791
|
-
id: 789,
|
|
792
|
-
definition: { id: 123, type: 'build' },
|
|
793
|
-
buildNumber: '1.0.56',
|
|
794
|
-
project: { name: 'project1' },
|
|
795
|
-
repository: { type: 'TfsGit' },
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
(TFSServices.getItemContent as jest.Mock)
|
|
799
|
-
.mockResolvedValueOnce(mockListBuildsResponse) // findBuildByDefinitionAndBuildNumber
|
|
800
|
-
.mockResolvedValueOnce(mockBuildResponse); // getPipelineBuildByBuildId
|
|
801
|
-
|
|
802
|
-
// Act
|
|
803
|
-
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
804
|
-
|
|
805
|
-
// Assert
|
|
806
|
-
expect(result).toHaveLength(1);
|
|
807
|
-
expect((result as any[])[0]).toEqual({
|
|
808
|
-
name: 'SOME_PACKAGE',
|
|
809
|
-
buildId: 789,
|
|
810
|
-
definitionId: 123,
|
|
811
|
-
buildNumber: '1.0.56',
|
|
812
|
-
teamProject: 'project1',
|
|
813
|
-
provider: 'TfsGit',
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
// Ensure branch normalization was applied in the build search URL
|
|
817
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[0][0]).toContain('branchName=refs%2Fheads%2Fmain');
|
|
818
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[0][0]).toContain('buildNumber=1.0.56');
|
|
819
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[0][0]).toContain('definitions=123');
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
it('should fall back to pipeline run history when buildNumber lookup returns no builds', async () => {
|
|
823
|
-
// Arrange
|
|
824
|
-
const inPipeline = {
|
|
825
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/10/runs/200',
|
|
826
|
-
resources: {
|
|
827
|
-
pipelines: {
|
|
828
|
-
SOME_PACKAGE: {
|
|
829
|
-
pipeline: {
|
|
830
|
-
id: 123,
|
|
831
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/123?revision=1',
|
|
832
|
-
},
|
|
833
|
-
project: { name: 'project1' },
|
|
834
|
-
source: 'project1-system-package',
|
|
835
|
-
version: '20251109.1',
|
|
836
|
-
branch: 'main',
|
|
837
|
-
},
|
|
838
|
-
},
|
|
839
|
-
},
|
|
840
|
-
} as unknown as PipelineRun;
|
|
841
|
-
|
|
842
|
-
const mockListBuildsResponseEmpty = { value: [] };
|
|
843
|
-
const mockRunHistoryResponse = {
|
|
844
|
-
value: [{ id: 789, name: '20251109.1', result: 'succeeded' }],
|
|
845
|
-
};
|
|
846
|
-
const mockBuildResponse = {
|
|
847
|
-
id: 789,
|
|
848
|
-
definition: { id: 123, type: 'build' },
|
|
849
|
-
buildNumber: '20251109.1',
|
|
850
|
-
project: { name: 'project1' },
|
|
851
|
-
repository: { type: 'TfsGit' },
|
|
852
|
-
};
|
|
853
|
-
|
|
854
|
-
(TFSServices.getItemContent as jest.Mock)
|
|
855
|
-
.mockResolvedValueOnce(mockListBuildsResponseEmpty) // findBuildByDefinitionAndBuildNumber
|
|
856
|
-
.mockResolvedValueOnce(mockListBuildsResponseEmpty) // findBuildByBuildNumber (no definition)
|
|
857
|
-
.mockResolvedValueOnce(mockRunHistoryResponse) // GetPipelineRunHistory
|
|
858
|
-
.mockResolvedValueOnce(mockBuildResponse); // getPipelineBuildByBuildId
|
|
859
|
-
|
|
860
|
-
// Act
|
|
861
|
-
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
862
|
-
|
|
863
|
-
// Assert
|
|
864
|
-
expect(result).toHaveLength(1);
|
|
865
|
-
expect((result as any[])[0]).toEqual({
|
|
866
|
-
name: 'SOME_PACKAGE',
|
|
867
|
-
buildId: 789,
|
|
868
|
-
definitionId: 123,
|
|
869
|
-
buildNumber: '20251109.1',
|
|
870
|
-
teamProject: 'project1',
|
|
871
|
-
provider: 'TfsGit',
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
it('should fall back to buildNumber-only lookup when definition-based lookup returns no builds', async () => {
|
|
876
|
-
const inPipeline = {
|
|
877
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/10/runs/200',
|
|
878
|
-
resources: {
|
|
879
|
-
pipelines: {
|
|
880
|
-
SOME_PACKAGE: {
|
|
881
|
-
pipeline: {
|
|
882
|
-
id: 770,
|
|
883
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/770?revision=1',
|
|
884
|
-
},
|
|
885
|
-
project: { name: 'project1' },
|
|
886
|
-
source: 'project1-system-package',
|
|
887
|
-
version: '20251109.1',
|
|
888
|
-
branch: 'main',
|
|
889
|
-
},
|
|
890
|
-
},
|
|
891
|
-
},
|
|
892
|
-
} as unknown as PipelineRun;
|
|
893
|
-
|
|
894
|
-
const mockListBuildsResponseEmpty = { value: [] };
|
|
895
|
-
const mockListBuildsByNumber = {
|
|
896
|
-
value: [
|
|
897
|
-
{
|
|
898
|
-
id: 789,
|
|
899
|
-
definition: { id: 123, name: 'project1-system-package' },
|
|
900
|
-
},
|
|
901
|
-
],
|
|
902
|
-
};
|
|
903
|
-
const mockBuildResponse = {
|
|
904
|
-
id: 789,
|
|
905
|
-
definition: { id: 123, type: 'build' },
|
|
906
|
-
buildNumber: '20251109.1',
|
|
907
|
-
project: { name: 'project1' },
|
|
908
|
-
repository: { type: 'TfsGit' },
|
|
909
|
-
};
|
|
910
|
-
|
|
911
|
-
(TFSServices.getItemContent as jest.Mock)
|
|
912
|
-
.mockResolvedValueOnce(mockListBuildsResponseEmpty) // findBuildByDefinitionAndBuildNumber
|
|
913
|
-
.mockResolvedValueOnce(mockListBuildsByNumber) // findBuildByBuildNumber (no definition)
|
|
914
|
-
.mockResolvedValueOnce(mockBuildResponse); // getPipelineBuildByBuildId
|
|
915
|
-
|
|
916
|
-
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
917
|
-
|
|
918
|
-
expect(result).toHaveLength(1);
|
|
919
|
-
expect((result as any[])[0]).toEqual({
|
|
920
|
-
name: 'SOME_PACKAGE',
|
|
921
|
-
buildId: 789,
|
|
922
|
-
definitionId: 123,
|
|
923
|
-
buildNumber: '20251109.1',
|
|
924
|
-
teamProject: 'project1',
|
|
925
|
-
provider: 'TfsGit',
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// Ensure the second call is the buildNumber-only lookup (no definitions= filter)
|
|
929
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][0]).toContain('buildNumber=20251109.1');
|
|
930
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[1][0]).not.toContain('definitions=');
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
it('should resolve project name when pipeline resource provides project id (GUID)', async () => {
|
|
763
|
+
it('should skip resource when fixed-url returns a build from a different project/pipeline', async () => {
|
|
934
764
|
const inPipeline = {
|
|
935
|
-
url: 'https://dev.azure.com/org/project1/_apis/pipelines/10/runs/200',
|
|
936
765
|
resources: {
|
|
937
766
|
pipelines: {
|
|
938
|
-
|
|
767
|
+
myPipeline: {
|
|
939
768
|
pipeline: {
|
|
940
769
|
id: 123,
|
|
941
|
-
|
|
770
|
+
name: 'ExpectedPipeline',
|
|
771
|
+
url: 'https://dev.azure.com/org/project/_apis/pipelines/123?revision=1',
|
|
942
772
|
},
|
|
943
|
-
project: { name: '1488cb19-7369-4afc-92bf-251d368b85be' },
|
|
944
|
-
source: 'project1-system-package',
|
|
945
|
-
version: '20251109.1',
|
|
946
|
-
branch: 'main',
|
|
947
773
|
},
|
|
948
774
|
},
|
|
949
775
|
},
|
|
950
776
|
} as unknown as PipelineRun;
|
|
951
777
|
|
|
952
|
-
const mockProjectResponse = { id: '1488cb19-7369-4afc-92bf-251d368b85be', name: 'Test CMMI' };
|
|
953
|
-
const mockListBuildsResponse = { value: [{ id: 789 }] };
|
|
954
778
|
const mockBuildResponse = {
|
|
955
779
|
id: 789,
|
|
956
|
-
definition: { id:
|
|
957
|
-
buildNumber: '
|
|
958
|
-
project: { name: '
|
|
780
|
+
definition: { id: 999, name: 'DifferentPipeline', type: 'build' },
|
|
781
|
+
buildNumber: '20231201.1',
|
|
782
|
+
project: { name: 'DifferentProject' },
|
|
959
783
|
repository: { type: 'TfsGit' },
|
|
960
784
|
};
|
|
961
785
|
|
|
962
|
-
(TFSServices.getItemContent as jest.Mock)
|
|
963
|
-
.mockResolvedValueOnce(mockProjectResponse) // normalizeProjectName
|
|
964
|
-
.mockResolvedValueOnce(mockListBuildsResponse) // findBuildByDefinitionAndBuildNumber
|
|
965
|
-
.mockResolvedValueOnce(mockBuildResponse); // getPipelineBuildByBuildId
|
|
786
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce(mockBuildResponse);
|
|
966
787
|
|
|
967
788
|
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
968
789
|
|
|
969
|
-
expect(result).
|
|
970
|
-
expect(
|
|
971
|
-
name: 'SOME_PACKAGE',
|
|
972
|
-
buildId: 789,
|
|
973
|
-
definitionId: 123,
|
|
974
|
-
buildNumber: '20251109.1',
|
|
975
|
-
teamProject: 'Test CMMI',
|
|
976
|
-
provider: 'TfsGit',
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
// Ensure project-id normalization API was called
|
|
980
|
-
expect((TFSServices.getItemContent as jest.Mock).mock.calls[0][0]).toContain(
|
|
981
|
-
`${mockOrgUrl}_apis/projects/1488cb19-7369-4afc-92bf-251d368b85be`
|
|
982
|
-
);
|
|
790
|
+
expect(result).toEqual([]);
|
|
791
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledTimes(1);
|
|
983
792
|
});
|
|
984
793
|
});
|
|
985
794
|
|
|
@@ -1227,6 +1036,126 @@ describe('PipelinesDataProvider', () => {
|
|
|
1227
1036
|
});
|
|
1228
1037
|
});
|
|
1229
1038
|
|
|
1039
|
+
describe('private helper methods', () => {
|
|
1040
|
+
it('tryGetTeamProjectFromAzureDevOpsUrl should extract project segment before _apis', () => {
|
|
1041
|
+
const fn = (pipelinesDataProvider as any).tryGetTeamProjectFromAzureDevOpsUrl.bind(
|
|
1042
|
+
pipelinesDataProvider
|
|
1043
|
+
);
|
|
1044
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/pipelines/123?revision=1')).toBe('Test');
|
|
1045
|
+
expect(fn('https://dev.azure.com/_apis/pipelines/123?revision=1')).toBeUndefined();
|
|
1046
|
+
expect(fn('https://dev.azure.com/org/_apis/pipelines/123?revision=1')).toBe('org');
|
|
1047
|
+
expect(fn('not-a-url')).toBeUndefined();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('tryBuildBuildApiUrlFromPipelinesApiUrl should rewrite pipelines url to builds url and drop query', () => {
|
|
1051
|
+
const fn = (pipelinesDataProvider as any).tryBuildBuildApiUrlFromPipelinesApiUrl.bind(
|
|
1052
|
+
pipelinesDataProvider
|
|
1053
|
+
);
|
|
1054
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/pipelines/123?revision=1')).toBe(
|
|
1055
|
+
'https://dev.azure.com/org/Test/_apis/build/builds/123'
|
|
1056
|
+
);
|
|
1057
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/build/builds/123')).toBeUndefined();
|
|
1058
|
+
expect(fn('not-a-url')).toBeUndefined();
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('tryParseRunIdFromUrl should parse run id from pipelines runs URL and build id from builds URL', () => {
|
|
1062
|
+
const fn = (pipelinesDataProvider as any).tryParseRunIdFromUrl.bind(pipelinesDataProvider);
|
|
1063
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/pipelines/10/runs/555')).toBe(555);
|
|
1064
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/build/builds/777')).toBe(777);
|
|
1065
|
+
expect(fn('https://dev.azure.com/org/Test/_apis/pipelines/10/runs/not-a-number')).toBeUndefined();
|
|
1066
|
+
expect(fn('not-a-url')).toBeUndefined();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('normalizeBranchName should normalize branch to refs/heads form', () => {
|
|
1070
|
+
const fn = (pipelinesDataProvider as any).normalizeBranchName.bind(pipelinesDataProvider);
|
|
1071
|
+
expect(fn('main')).toBe('refs/heads/main');
|
|
1072
|
+
expect(fn('heads/main')).toBe('refs/heads/main');
|
|
1073
|
+
expect(fn('refs/heads/main')).toBe('refs/heads/main');
|
|
1074
|
+
expect(fn('')).toBeUndefined();
|
|
1075
|
+
expect(fn(undefined)).toBeUndefined();
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('normalizeProjectName should resolve GUID project id to name and cache it', async () => {
|
|
1079
|
+
const guid = '009c6fae-b000-47fe-994e-be3354b78fbc';
|
|
1080
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({ name: 'ResolvedProjectName' });
|
|
1081
|
+
|
|
1082
|
+
const fn = (pipelinesDataProvider as any).normalizeProjectName.bind(pipelinesDataProvider);
|
|
1083
|
+
const first = await fn(guid);
|
|
1084
|
+
const second = await fn(guid);
|
|
1085
|
+
|
|
1086
|
+
expect(first).toBe('ResolvedProjectName');
|
|
1087
|
+
expect(second).toBe('ResolvedProjectName');
|
|
1088
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledTimes(1);
|
|
1089
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
1090
|
+
`${mockOrgUrl}_apis/projects/${encodeURIComponent(guid)}?api-version=6.0`,
|
|
1091
|
+
mockToken,
|
|
1092
|
+
'get',
|
|
1093
|
+
null,
|
|
1094
|
+
null,
|
|
1095
|
+
false
|
|
1096
|
+
);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('normalizeProjectName should return raw GUID when project resolution fails', async () => {
|
|
1100
|
+
const guid = '009c6fae-b000-47fe-994e-be3354b78fbc';
|
|
1101
|
+
(TFSServices.getItemContent as jest.Mock).mockRejectedValueOnce(new Error('boom'));
|
|
1102
|
+
const fn = (pipelinesDataProvider as any).normalizeProjectName.bind(pipelinesDataProvider);
|
|
1103
|
+
const result = await fn(guid);
|
|
1104
|
+
expect(result).toBe(guid);
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
it('findBuildByBuildNumber should prefer matching definition name when provided', async () => {
|
|
1108
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({
|
|
1109
|
+
value: [
|
|
1110
|
+
{ id: 1, definition: { name: 'Other' } },
|
|
1111
|
+
{ id: 2, definition: { name: 'Expected' } },
|
|
1112
|
+
],
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
const fn = (pipelinesDataProvider as any).findBuildByBuildNumber.bind(pipelinesDataProvider);
|
|
1116
|
+
const result = await fn('project1', '20251225.2', 'main', 'Expected');
|
|
1117
|
+
expect(result?.id).toBe(2);
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it('tryGetBuildByIdWithFallback should fall back to non-project-scoped URL when project-scoped lookup fails', async () => {
|
|
1121
|
+
jest
|
|
1122
|
+
.spyOn(pipelinesDataProvider as any, 'getPipelineBuildByBuildId')
|
|
1123
|
+
.mockRejectedValueOnce(new Error('not found'));
|
|
1124
|
+
(TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({ id: 123 });
|
|
1125
|
+
|
|
1126
|
+
const fn = (pipelinesDataProvider as any).tryGetBuildByIdWithFallback.bind(pipelinesDataProvider);
|
|
1127
|
+
const result = await fn('project1', 123);
|
|
1128
|
+
|
|
1129
|
+
expect(result).toEqual({ id: 123 });
|
|
1130
|
+
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
1131
|
+
`${mockOrgUrl}_apis/build/builds/123`,
|
|
1132
|
+
mockToken,
|
|
1133
|
+
'get',
|
|
1134
|
+
null,
|
|
1135
|
+
null
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('getPipelineResourcePipelinesFromObject should skip resource when pipelines url cannot be converted to builds url', async () => {
|
|
1140
|
+
const inPipeline = {
|
|
1141
|
+
resources: {
|
|
1142
|
+
pipelines: {
|
|
1143
|
+
myPipeline: {
|
|
1144
|
+
pipeline: {
|
|
1145
|
+
id: 123,
|
|
1146
|
+
url: 'https://dev.azure.com/org/project/_apis/build/builds/123',
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
} as unknown as PipelineRun;
|
|
1152
|
+
|
|
1153
|
+
const result = await pipelinesDataProvider.getPipelineResourcePipelinesFromObject(inPipeline);
|
|
1154
|
+
expect(result).toEqual([]);
|
|
1155
|
+
expect(TFSServices.getItemContent).not.toHaveBeenCalled();
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1230
1159
|
describe('isInvalidPipelineRun', () => {
|
|
1231
1160
|
const invokeIsInvalidPipelineRun = (
|
|
1232
1161
|
pipelineRun: any,
|