@elisra-devops/docgen-data-provider 1.113.0 → 1.116.0

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.
@@ -492,11 +492,16 @@ export default class GitDataProvider {
492
492
  //First fetch the repo web url
493
493
  if (targetRepo.url) {
494
494
  const repoData = await TFSServices.getItemContent(targetRepo.url, this.token);
495
- const repoWebUrl = repoData._links?.web.href;
496
- const targetRepoProjectId = repoData.project?.id;
495
+ const repoWebUrl = repoData?._links?.web?.href;
496
+ const targetRepoProjectId = repoData?.project?.id;
497
497
  if (repoWebUrl) {
498
498
  targetRepo['url'] = repoWebUrl;
499
- targetRepo['projectId'] = targetRepoProjectId ?? teamProject;
499
+ }
500
+ targetRepo['projectId'] = targetRepoProjectId ?? teamProject;
501
+ if (!targetRepoProjectId) {
502
+ logger.warn(
503
+ `getItemsForPipelineRange: repo response missing project.id for ${targetRepo.url}; falling back to pipeline teamProject=${teamProject}`
504
+ );
500
505
  }
501
506
  }
502
507
  //Then extend the commit information with the related WIs
@@ -219,13 +219,15 @@ export default class PipelinesDataProvider {
219
219
  /**
220
220
  * Extracts the primary repository from a pipeline run details response.
221
221
  *
222
- * Newer YAML runs expose repository resources as an array with a `self` entry, while some
223
- * designer/classic pipeline responses expose the repository under `__designer_repo`.
222
+ * The ADO Pipelines Runs API returns resources.repositories as a named-key object where
223
+ * 'self' is the pipeline's own checkout (e.g. { self: {...}, AppRepo: {...} }). Some older
224
+ * in-process representations wrap it as an array ({ 0: { self: {...} } }) and classic/designer
225
+ * pipelines use __designer_repo.
224
226
  */
225
227
  private getPrimaryPipelineRepository(pipeline: any): any {
226
228
  const repositories = pipeline?.resources?.repositories;
227
229
  if (!repositories) return undefined;
228
- return repositories[0]?.self || repositories.__designer_repo;
230
+ return repositories.self || repositories[0]?.self || repositories.__designer_repo;
229
231
  }
230
232
 
231
233
  /**
@@ -482,9 +484,13 @@ export default class PipelinesDataProvider {
482
484
  }
483
485
 
484
486
  const fromRepo =
485
- fromPipeline.resources.repositories[0]?.self || fromPipeline.resources.repositories.__designer_repo;
487
+ fromPipeline.resources.repositories.self ||
488
+ fromPipeline.resources.repositories[0]?.self ||
489
+ fromPipeline.resources.repositories.__designer_repo;
486
490
  const targetRepo =
487
- targetPipeline.resources.repositories[0]?.self || targetPipeline.resources.repositories.__designer_repo;
491
+ targetPipeline.resources.repositories.self ||
492
+ targetPipeline.resources.repositories[0]?.self ||
493
+ targetPipeline.resources.repositories.__designer_repo;
488
494
 
489
495
  if (!fromRepo?.repository?.id || !targetRepo?.repository?.id) {
490
496
  return false;
@@ -1334,6 +1334,90 @@ describe('GitDataProvider - getItemsForPipelineRange', () => {
1334
1334
  // Assert - should only have 1 work item (no duplicates)
1335
1335
  expect(result.commitChangesArray).toHaveLength(1);
1336
1336
  });
1337
+
1338
+ it('cross-project repo: should resolve WIs using teamProject fallback when _links.web.href is missing', async () => {
1339
+ // Real ADO cross-project resource-pipeline repos can return repo metadata without
1340
+ // _links.web.href. Previously this left targetRepo.projectId undefined, causing
1341
+ // GetWorkItem to call /{orgUrl}undefined/... and silently swallow the 404.
1342
+ const pipelineTeamProject = 'TestProject-CMMI';
1343
+ const extendedCommits = [
1344
+ {
1345
+ commit: {
1346
+ commitId: 'abc1234',
1347
+ workItems: [{ id: 42 }],
1348
+ committer: { date: '2024-01-01', name: 'Dev' },
1349
+ comment: 'cross-project fix',
1350
+ },
1351
+ },
1352
+ ];
1353
+ const targetRepo = { url: 'https://dev.azure.com/org/OtherProject/_apis/git/repositories/repo-id' };
1354
+ const addedWorkItemByIdSet = new Set<number>();
1355
+
1356
+ // Repo lives in a different project; response has no _links.web.href
1357
+ const mockRepoDataNoCrossLink = {
1358
+ project: { id: 'cross-project-id' },
1359
+ // no _links at all
1360
+ };
1361
+ const mockPopulatedWI = { id: 42, fields: { 'System.Title': 'Cross-project WI' } };
1362
+
1363
+ (TFSServices.getItemContent as jest.Mock)
1364
+ .mockResolvedValueOnce(mockRepoDataNoCrossLink) // repo metadata call
1365
+ .mockResolvedValueOnce(mockPopulatedWI); // GetWorkItem call
1366
+
1367
+ const result = await gitDataProvider.getItemsForPipelineRange(
1368
+ pipelineTeamProject,
1369
+ extendedCommits,
1370
+ targetRepo,
1371
+ addedWorkItemByIdSet
1372
+ );
1373
+
1374
+ expect(result.commitChangesArray).toHaveLength(1);
1375
+ expect(result.commitChangesArray[0].workItem.id).toBe(42);
1376
+ // GetWorkItem should have been called with the repo's own projectId from repoData.project.id
1377
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
1378
+ expect.stringContaining('cross-project-id'),
1379
+ expect.any(String)
1380
+ );
1381
+ });
1382
+
1383
+ it('cross-project repo: should fall back to teamProject when repoData.project.id is also missing', async () => {
1384
+ const pipelineTeamProject = 'TestProject-CMMI';
1385
+ const extendedCommits = [
1386
+ {
1387
+ commit: {
1388
+ commitId: 'def5678',
1389
+ workItems: [{ id: 99 }],
1390
+ committer: { date: '2024-01-02', name: 'Dev' },
1391
+ comment: 'fallback test',
1392
+ },
1393
+ },
1394
+ ];
1395
+ const targetRepo = { url: 'https://dev.azure.com/org/SomeProject/_apis/git/repositories/repo-id2' };
1396
+ const addedWorkItemByIdSet = new Set<number>();
1397
+
1398
+ // Response with neither _links.web.href nor project.id
1399
+ const mockRepoDataEmpty = {};
1400
+ const mockPopulatedWI = { id: 99, fields: { 'System.Title': 'Fallback WI' } };
1401
+
1402
+ (TFSServices.getItemContent as jest.Mock)
1403
+ .mockResolvedValueOnce(mockRepoDataEmpty)
1404
+ .mockResolvedValueOnce(mockPopulatedWI);
1405
+
1406
+ const result = await gitDataProvider.getItemsForPipelineRange(
1407
+ pipelineTeamProject,
1408
+ extendedCommits,
1409
+ targetRepo,
1410
+ addedWorkItemByIdSet
1411
+ );
1412
+
1413
+ expect(result.commitChangesArray).toHaveLength(1);
1414
+ expect(result.commitChangesArray[0].workItem.id).toBe(99);
1415
+ // GetWorkItem should have been called with the pipeline teamProject as fallback
1416
+ expect(TFSServices.getItemContent).toHaveBeenCalledWith(
1417
+ expect.stringContaining(pipelineTeamProject),
1418
+ expect.any(String)
1419
+ );
1420
+ });
1337
1421
  });
1338
1422
 
1339
1423
  describe('GitDataProvider - GetItemsInPullRequestRange', () => {
@@ -242,6 +242,37 @@ describe('PipelinesDataProvider', () => {
242
242
  // Assert
243
243
  expect(result).toBe(true);
244
244
  });
245
+
246
+ it('should resolve self from real ADO Runs API format (repositories.self, not repositories[0].self)', () => {
247
+ // Real ADO Pipelines Runs API returns repositories as a named-key object:
248
+ // { self: {...}, DevOpsTemplates: {...} } — NOT an array.
249
+ const fromPipeline = {
250
+ resources: {
251
+ repositories: {
252
+ self: {
253
+ repository: { id: 'repo1' },
254
+ version: 'v1',
255
+ refName: 'refs/heads/main',
256
+ },
257
+ },
258
+ },
259
+ } as unknown as PipelineRun;
260
+
261
+ const targetPipeline = {
262
+ resources: {
263
+ repositories: {
264
+ self: {
265
+ repository: { id: 'repo1' },
266
+ version: 'v2',
267
+ refName: 'refs/heads/main',
268
+ },
269
+ },
270
+ },
271
+ } as unknown as PipelineRun;
272
+
273
+ const result = invokeIsMatchingPipeline(fromPipeline, targetPipeline, false);
274
+ expect(result).toBe(true);
275
+ });
245
276
  });
246
277
 
247
278
  describe('getPipelineRunDetails', () => {