@elisra-devops/docgen-data-provider 1.126.0 → 1.127.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.
@@ -33,6 +33,13 @@ export default class GitDataProvider {
33
33
  return TFSServices.getItemContent(url, this.token, 'get');
34
34
  } //GetGitRepoFromPrId
35
35
 
36
+ /**
37
+ * Retrieves a JSON file from a Git repository
38
+ * @param projectName - The name of the project
39
+ * @param repoName - The name of the repository
40
+ * @param filePath - The path to the file
41
+ * @returns The JSON content of the file
42
+ */
36
43
  async GetJsonFileFromGitRepo(projectName: string, repoName: string, filePath: string) {
37
44
  let url = `${this.orgUrl}${projectName}/_apis/git/repositories/${repoName}/items?path=${filePath}&includeContent=true`;
38
45
  let res = await TFSServices.getItemContent(url, this.token, 'get');
@@ -64,7 +71,7 @@ export default class GitDataProvider {
64
71
  const res = await TFSServices.getItemContent(url, this.token, 'get');
65
72
  if (res && res.value && Array.isArray(res.value) && res.value.length > 0) {
66
73
  const match = res.value.find(
67
- (r: any) => r.name.split('/').pop().toLowerCase() === branch.toLowerCase()
74
+ (r: any) => r.name.split('/').pop().toLowerCase() === branch.toLowerCase(),
68
75
  );
69
76
 
70
77
  if (match) {
@@ -89,7 +96,7 @@ export default class GitDataProvider {
89
96
  repoId: string,
90
97
  fileName: string,
91
98
  version: GitVersionDescriptor,
92
- gitRepoUrl: string = ''
99
+ gitRepoUrl: string = '',
93
100
  ) {
94
101
  // get a single tag
95
102
  let versionFix = '';
@@ -125,7 +132,7 @@ export default class GitDataProvider {
125
132
  async CheckIfItemExist(
126
133
  gitApiUrl: string,
127
134
  itemPath: string,
128
- version: GitVersionDescriptor
135
+ version: GitVersionDescriptor,
129
136
  ): Promise<boolean> {
130
137
  let safePath = encodeURIComponent(itemPath).replace(/%2F/g, '/');
131
138
  let versionFixed = '';
@@ -163,7 +170,7 @@ export default class GitDataProvider {
163
170
  async GetPullRequestsLinkedItemsInCommitRange(
164
171
  projectId: string,
165
172
  repositoryId: string,
166
- commitRangeArray: any
173
+ commitRangeArray: any,
167
174
  ) {
168
175
  let pullRequestsFilteredArray: any = [];
169
176
  let ChangeSetsArray: any = [];
@@ -186,7 +193,7 @@ export default class GitDataProvider {
186
193
  });
187
194
  });
188
195
  logger.info(
189
- `filtered in commit range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`
196
+ `filtered in commit range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`,
190
197
  );
191
198
  //extract linked items and append them to result
192
199
  await Promise.all(
@@ -206,13 +213,13 @@ export default class GitDataProvider {
206
213
  pullrequest: pr,
207
214
  };
208
215
  ChangeSetsArray.push(changeSet);
209
- })
216
+ }),
210
217
  );
211
218
  }
212
219
  } catch (error) {
213
220
  logger.error(error);
214
221
  }
215
- })
222
+ }),
216
223
  );
217
224
  return ChangeSetsArray;
218
225
  } //GetPullRequestsInCommitRange
@@ -223,12 +230,12 @@ export default class GitDataProvider {
223
230
  commitRange: any,
224
231
  linkedWiOptions: any,
225
232
  includeUnlinkedCommits: boolean = false,
226
- includePullRequestWorkItems: boolean = true
233
+ includePullRequestWorkItems: boolean = true,
227
234
  ) {
228
235
  logger.info(
229
236
  `GetItemsInCommitRange: includeUnlinkedCommits=${includeUnlinkedCommits}, includePullRequestWorkItems=${includePullRequestWorkItems}, commits=${
230
237
  commitRange?.value?.length || 0
231
- }`
238
+ }`,
232
239
  );
233
240
  //get all items linked to commits
234
241
  let res: any = [];
@@ -245,7 +252,7 @@ export default class GitDataProvider {
245
252
  let populatedItem = await this.ticketsDataProvider.GetWorkItem(projectId, wi.id);
246
253
  let linkedItems: LinkedRelation[] = await this.createLinkedRelatedItemsForSVD(
247
254
  linkedWiOptions,
248
- populatedItem
255
+ populatedItem,
249
256
  );
250
257
  let changeSet: any = { workItem: populatedItem, commit: commit, linkedItems };
251
258
  commitChangesArray.push(changeSet);
@@ -267,7 +274,7 @@ export default class GitDataProvider {
267
274
  }
268
275
  }
269
276
  logger.info(
270
- `GetItemsInCommitRange: produced ${commitChangesArray.length} linked changes and ${commitsWithNoRelations.length} unlinked commits`
277
+ `GetItemsInCommitRange: produced ${commitChangesArray.length} linked changes and ${commitsWithNoRelations.length} unlinked commits`,
271
278
  );
272
279
  //get all items and pr data from pr's in commit range - using the above function
273
280
  let pullRequestsChangesArray: any[] = [];
@@ -277,7 +284,7 @@ export default class GitDataProvider {
277
284
  const prLinkedItems = await this.GetPullRequestsLinkedItemsInCommitRange(
278
285
  projectId,
279
286
  repositoryId,
280
- commitRange
287
+ commitRange,
281
288
  );
282
289
  ({ workItemIds: prWorkItemIds, mergeCommitIds: prMergeCommitIds } =
283
290
  this.collectPrWorkItemMetadata(prLinkedItems));
@@ -347,7 +354,7 @@ export default class GitDataProvider {
347
354
  commitEntries.filter((entry: [string | undefined, any]): entry is [string, any] => {
348
355
  const commitId = entry[0];
349
356
  return typeof commitId === 'string' && commitId !== '';
350
- })
357
+ }),
351
358
  );
352
359
  }
353
360
 
@@ -374,7 +381,7 @@ export default class GitDataProvider {
374
381
  commitChangesArray: any[],
375
382
  workItemCommitIds: Map<number, Set<string>>,
376
383
  prWorkItemIds: Set<number>,
377
- prMergeCommitIds: Set<string>
384
+ prMergeCommitIds: Set<string>,
378
385
  ) {
379
386
  if (prWorkItemIds.size === 0) return;
380
387
  for (const change of commitChangesArray || []) {
@@ -398,7 +405,7 @@ export default class GitDataProvider {
398
405
  async GetPullRequestsInCommitRangeWithoutLinkedItems(
399
406
  projectId: string,
400
407
  repositoryId: string,
401
- commitRangeArray: any
408
+ commitRangeArray: any,
402
409
  ) {
403
410
  let pullRequestsFilteredArray: any[] = [];
404
411
 
@@ -433,7 +440,7 @@ export default class GitDataProvider {
433
440
  });
434
441
  });
435
442
  logger.info(
436
- `filtered in commit range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`
443
+ `filtered in commit range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`,
437
444
  );
438
445
 
439
446
  return pullRequestsFilteredArray;
@@ -461,7 +468,7 @@ export default class GitDataProvider {
461
468
  let populatedItem = await this.ticketsDataProvider.GetWorkItem(projectId, wi.id);
462
469
  let changeSet: any = { workItem: populatedItem, build: toBuildId };
463
470
  linkedItemsArray.push(changeSet);
464
- })
471
+ }),
465
472
  );
466
473
  return linkedItemsArray;
467
474
  } //GetCommitForPipeline
@@ -473,12 +480,12 @@ export default class GitDataProvider {
473
480
  targetRepo: any,
474
481
  addedWorkItemByIdSet: Set<number>,
475
482
  linkedWiOptions: any = undefined,
476
- includeUnlinkedCommits: boolean = false
483
+ includeUnlinkedCommits: boolean = false,
477
484
  ) {
478
485
  logger.info(
479
486
  `getItemsForPipelineRange: includeUnlinkedCommits=${includeUnlinkedCommits}, extendedCommits=${
480
487
  extendedCommits?.length || 0
481
- }`
488
+ }`,
482
489
  );
483
490
  let commitChangesArray: any[] = [];
484
491
  let commitsWithNoRelations: any[] = [];
@@ -487,7 +494,7 @@ export default class GitDataProvider {
487
494
  throw new Error('extended commits cannot be empty');
488
495
  }
489
496
  logger.debug(
490
- `getItemsForPipelineRange: ${extendedCommits?.length} commits for ${JSON.stringify(targetRepo)}`
497
+ `getItemsForPipelineRange: ${extendedCommits?.length} commits for ${JSON.stringify(targetRepo)}`,
491
498
  );
492
499
  //First fetch the repo web url
493
500
  if (targetRepo.url) {
@@ -500,7 +507,7 @@ export default class GitDataProvider {
500
507
  targetRepo['projectId'] = targetRepoProjectId ?? teamProject;
501
508
  if (!targetRepoProjectId) {
502
509
  logger.warn(
503
- `getItemsForPipelineRange: repo response missing project.id for ${targetRepo.url}; falling back to pipeline teamProject=${teamProject}`
510
+ `getItemsForPipelineRange: repo response missing project.id for ${targetRepo.url}; falling back to pipeline teamProject=${teamProject}`,
504
511
  );
505
512
  }
506
513
  }
@@ -522,11 +529,11 @@ export default class GitDataProvider {
522
529
  for (const wi of commit.workItems) {
523
530
  const populatedWorkItem = await this.ticketsDataProvider.GetWorkItem(
524
531
  targetRepo['projectId'],
525
- wi.id
532
+ wi.id,
526
533
  );
527
534
  let linkedItems: LinkedRelation[] = await this.createLinkedRelatedItemsForSVD(
528
535
  linkedWiOptions,
529
- populatedWorkItem
536
+ populatedWorkItem,
530
537
  );
531
538
  let changeSet: any = {
532
539
  workItem: populatedWorkItem,
@@ -554,7 +561,7 @@ export default class GitDataProvider {
554
561
  logger.error(error.message);
555
562
  }
556
563
  logger.info(
557
- `getItemsForPipelineRange: produced ${commitChangesArray.length} linked changes and ${commitsWithNoRelations.length} unlinked commits`
564
+ `getItemsForPipelineRange: produced ${commitChangesArray.length} linked changes and ${commitsWithNoRelations.length} unlinked commits`,
558
565
  );
559
566
  return { commitChangesArray, commitsWithNoRelations };
560
567
  }
@@ -614,7 +621,7 @@ export default class GitDataProvider {
614
621
  wiItemType,
615
622
  relatedItemContent.fields['System.Title'],
616
623
  relatedItemContent._links?.html?.href || '',
617
- relation.attributes['name'] || ''
624
+ relation.attributes['name'] || '',
618
625
  );
619
626
 
620
627
  const isAffectsRelation = relName.includes('Affects');
@@ -662,7 +669,7 @@ export default class GitDataProvider {
662
669
  repositoryId: string,
663
670
  fromDate: string,
664
671
  toDate: string,
665
- branchName?: string
672
+ branchName?: string,
666
673
  ) {
667
674
  let url: string;
668
675
  if (typeof branchName !== 'undefined') {
@@ -735,7 +742,7 @@ export default class GitDataProvider {
735
742
  });
736
743
  });
737
744
  logger.info(
738
- `filtered in prId range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`
745
+ `filtered in prId range ${pullRequestsFilteredArray.length} pullrequests for repo: ${repositoryId}`,
739
746
  );
740
747
  //extract linked items and append them to result
741
748
  await Promise.all(
@@ -755,13 +762,13 @@ export default class GitDataProvider {
755
762
  pullrequest: pr,
756
763
  };
757
764
  ChangeSetsArray.push(changeSet);
758
- })
765
+ }),
759
766
  );
760
767
  }
761
768
  } catch (error) {
762
769
  logger.error(error);
763
770
  }
764
- })
771
+ }),
765
772
  );
766
773
  return ChangeSetsArray;
767
774
  }
@@ -810,7 +817,7 @@ export default class GitDataProvider {
810
817
  // If commit cannot be resolved, leave timestamp as 0
811
818
  }
812
819
  return { refItem, ts, dateStr };
813
- })
820
+ }),
814
821
  );
815
822
 
816
823
  taggedWithDates.sort((a: any, b: any) => b.ts - a.ts);
@@ -837,7 +844,7 @@ export default class GitDataProvider {
837
844
  // ignore and keep ts=0
838
845
  }
839
846
  return { refItem, ts, dateStr };
840
- })
847
+ }),
841
848
  );
842
849
 
843
850
  branchesWithDates.sort((a: any, b: any) => b.ts - a.ts);
@@ -895,7 +902,7 @@ export default class GitDataProvider {
895
902
  gitUrl: string,
896
903
  itemVersion: GitVersionDescriptor,
897
904
  compareVersion: GitVersionDescriptor,
898
- specificItemPath: string = ''
905
+ specificItemPath: string = '',
899
906
  ) {
900
907
  const allCommitsExtended: any[] = [];
901
908
  let skipping = 0;
@@ -962,7 +969,7 @@ export default class GitDataProvider {
962
969
  repoId: string,
963
970
  targetVersion: GitVersionDescriptor,
964
971
  sourceVersion: GitVersionDescriptor,
965
- allCommitsExtended: any[]
972
+ allCommitsExtended: any[],
966
973
  ) {
967
974
  let submodules: any[] = [];
968
975
  try {
@@ -1044,20 +1051,20 @@ export default class GitDataProvider {
1044
1051
 
1045
1052
  if (!sourceSha1) {
1046
1053
  logger.warn(
1047
- `${gitSubModuleName} pointer not exist in source version ${sourceVersion.versionType} ${sourceVersion.version} in repository ${gitRepoUrl}`
1054
+ `${gitSubModuleName} pointer not exist in source version ${sourceVersion.versionType} ${sourceVersion.version} in repository ${gitRepoUrl}`,
1048
1055
  );
1049
1056
  continue;
1050
1057
  }
1051
1058
 
1052
1059
  if (!targetSha1) {
1053
1060
  logger.warn(
1054
- `${gitSubModuleName} pointer not exist in target version ${targetVersion.versionType} ${targetVersion.version} in repository ${gitRepoUrl}`
1061
+ `${gitSubModuleName} pointer not exist in target version ${targetVersion.versionType} ${targetVersion.version} in repository ${gitRepoUrl}`,
1055
1062
  );
1056
1063
  continue;
1057
1064
  }
1058
1065
  if (sourceSha1 === targetSha1) {
1059
1066
  logger.warn(
1060
- `${gitSubModuleName} pointer is the same in source and target version in repository ${gitRepoUrl}`
1067
+ `${gitSubModuleName} pointer is the same in source and target version in repository ${gitRepoUrl}`,
1061
1068
  );
1062
1069
  continue;
1063
1070
  }
@@ -1757,17 +1757,25 @@ export default class TicketsDataProvider {
1757
1757
  }
1758
1758
 
1759
1759
  private shouldRetryHistoricalWithLowerVersion(error: any): boolean {
1760
+ if (error?._historicalFallbackExhausted) {
1761
+ return false;
1762
+ }
1760
1763
  const status = Number(error?.response?.status || 0);
1761
1764
  if (status === 401 || status === 403) {
1762
1765
  return false;
1763
1766
  }
1767
+ const message = String(error?.response?.data?.message || error?.message || '');
1768
+ // TF##### errors are semantic ADO rejections (invalid path, missing field, etc.) —
1769
+ // not api-version compatibility issues. Retrying wastes time and amplifies timeouts.
1770
+ if (/(^|\s)TF\d{4,6}:/i.test(message)) {
1771
+ return false;
1772
+ }
1764
1773
  if (status >= 500) {
1765
1774
  return true;
1766
1775
  }
1767
1776
  if ([400, 404, 405, 406, 410].includes(status)) {
1768
1777
  return true;
1769
1778
  }
1770
- const message = String(error?.response?.data?.message || error?.message || '');
1771
1779
  return /api[- ]?version/i.test(message);
1772
1780
  }
1773
1781
 
@@ -1941,50 +1949,73 @@ export default class TicketsDataProvider {
1941
1949
  queryId: string,
1942
1950
  asOfIso: string,
1943
1951
  ): Promise<{ queryDefinition: any; queryResult: any; apiVersion: string | null }> {
1944
- const { apiVersion, result } = await this.withHistoricalApiVersionFallback(
1945
- 'historical-query-execution',
1946
- async (resolvedApiVersion) => {
1947
- const queryDefUrl = this.appendApiVersion(
1948
- `${this.orgUrl}${project}/_apis/wit/queries/${encodeURIComponent(queryId)}?$expand=all`,
1949
- resolvedApiVersion,
1950
- );
1951
- const queryDefinition = await TFSServices.getItemContent(queryDefUrl, this.token);
1952
-
1953
- const wiqlText = String(queryDefinition?.wiql || '').trim();
1954
- let queryResult: any = null;
1955
- try {
1956
- if (!wiqlText) {
1957
- throw new Error(`Could not resolve WIQL text for query ${queryId}`);
1958
- }
1959
- const wiqlWithAsOf = this.appendAsOfToWiql(wiqlText, asOfIso);
1960
- if (!wiqlWithAsOf) {
1961
- throw new Error(`Could not build WIQL for historical query ${queryId}`);
1962
- }
1963
- const executeUrl = this.appendApiVersion(
1964
- `${this.orgUrl}${project}/_apis/wit/wiql?$top=2147483646&timePrecision=true`,
1965
- resolvedApiVersion,
1966
- );
1967
- queryResult = await TFSServices.getItemContent(executeUrl, this.token, 'post', {
1968
- query: wiqlWithAsOf,
1969
- });
1970
- } catch (inlineWiqlError: any) {
1971
- logger.warn(
1972
- `[historical-query-execution] inline WIQL failed for query ${queryId}${
1973
- resolvedApiVersion ? ` (api-version=${resolvedApiVersion})` : ''
1974
- }, trying WIQL-by-id fallback: ${inlineWiqlError?.message || inlineWiqlError}`,
1975
- );
1976
- const executeByIdUrl = this.appendApiVersion(
1977
- `${this.orgUrl}${project}/_apis/wit/wiql/${encodeURIComponent(
1978
- queryId,
1979
- )}?$top=2147483646&timePrecision=true&asOf=${encodeURIComponent(asOfIso)}`,
1952
+ try {
1953
+ const { apiVersion, result } = await this.withHistoricalApiVersionFallback(
1954
+ 'historical-query-execution',
1955
+ async (resolvedApiVersion) => {
1956
+ const queryDefUrl = this.appendApiVersion(
1957
+ `${this.orgUrl}${project}/_apis/wit/queries/${encodeURIComponent(queryId)}?$expand=all`,
1980
1958
  resolvedApiVersion,
1981
1959
  );
1982
- queryResult = await TFSServices.getItemContent(executeByIdUrl, this.token);
1983
- }
1984
- return { queryDefinition, queryResult };
1985
- },
1986
- );
1987
- return { ...result, apiVersion };
1960
+ const queryDefinition = await TFSServices.getItemContent(queryDefUrl, this.token);
1961
+
1962
+ const wiqlText = String(queryDefinition?.wiql || '').trim();
1963
+ let queryResult: any = null;
1964
+ try {
1965
+ if (!wiqlText) {
1966
+ throw new Error(`Could not resolve WIQL text for query ${queryId}`);
1967
+ }
1968
+ const wiqlWithAsOf = this.appendAsOfToWiql(wiqlText, asOfIso);
1969
+ if (!wiqlWithAsOf) {
1970
+ throw new Error(`Could not build WIQL for historical query ${queryId}`);
1971
+ }
1972
+ const executeUrl = this.appendApiVersion(
1973
+ `${this.orgUrl}${project}/_apis/wit/wiql?$top=2147483646&timePrecision=true`,
1974
+ resolvedApiVersion,
1975
+ );
1976
+ queryResult = await TFSServices.getItemContent(executeUrl, this.token, 'post', {
1977
+ query: wiqlWithAsOf,
1978
+ });
1979
+ } catch (inlineWiqlError: any) {
1980
+ logger.warn(
1981
+ `[historical-query-execution] inline WIQL failed for query ${queryId}${
1982
+ resolvedApiVersion ? ` (api-version=${resolvedApiVersion})` : ''
1983
+ }, trying WIQL-by-id fallback: ${inlineWiqlError?.message || inlineWiqlError}`,
1984
+ );
1985
+ const executeByIdUrl = this.appendApiVersion(
1986
+ `${this.orgUrl}${project}/_apis/wit/wiql/${encodeURIComponent(
1987
+ queryId,
1988
+ )}?$top=2147483646&timePrecision=true&asOf=${encodeURIComponent(asOfIso)}`,
1989
+ resolvedApiVersion,
1990
+ );
1991
+ try {
1992
+ queryResult = await TFSServices.getItemContent(executeByIdUrl, this.token);
1993
+ } catch (byIdError: any) {
1994
+ byIdError._historicalFallbackExhausted = true;
1995
+ throw byIdError;
1996
+ }
1997
+ }
1998
+ return { queryDefinition, queryResult };
1999
+ },
2000
+ );
2001
+ return { ...result, apiVersion };
2002
+ } catch (err: any) {
2003
+ const raw = String(err?.response?.data?.message || err?.message || '');
2004
+ const tfMatch = raw.match(/(^|\s)(TF\d{4,6}):/i);
2005
+ if (tfMatch) {
2006
+ const tfCode = tfMatch[2];
2007
+ const friendly = new Error(
2008
+ `Azure DevOps rejected the historical query (${tfCode}): ${raw} ` +
2009
+ `The query references an area or iteration path that no longer exists. ` +
2010
+ `Please open the query in Azure DevOps and fix the filter.`,
2011
+ ) as any;
2012
+ friendly.response = err?.response;
2013
+ friendly.status = err?.response?.status ?? err?.status ?? 400;
2014
+ friendly.code = tfCode;
2015
+ throw friendly;
2016
+ }
2017
+ throw err;
2018
+ }
1988
2019
  }
1989
2020
 
1990
2021
  private toHistoricalWorkItemSnapshot(project: string, workItem: any): HistoricalWorkItemSnapshot {