@elisra-devops/docgen-data-provider 1.69.13 → 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.
@@ -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}): ${e?.message || e}`
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
- return new Set();
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 runId: number | undefined = this.inferRunIdFromPipelineResource(resource, resourcePipelineObj?.url);
487
- if (typeof runId !== 'number' || !Number.isFinite(runId)) {
488
- runId = await this.resolveRunIdFromVersion({
489
- projectName,
490
- pipelineIdCandidate,
491
- version,
492
- branch,
493
- source,
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
- if (typeof runId !== 'number' || !Number.isFinite(runId)) {
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 ?? runId;
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: buildResponse.buildNumber ?? (resource as any)?.runName ?? (resource as any)?.version,
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 !== 'azureReposGit') {
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(url, this.token, 'get', null, null);
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 = headers?.['x-ms-continuationtoken'] || headers?.['x-ms-continuation-token'] || undefined;
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) {
@@ -825,7 +825,7 @@ export default class ResultDataProvider {
825
825
 
826
826
  const cached = this.workItemDiscussionCache.get(id);
827
827
  if (cached !== undefined) {
828
- if (cached.length === 0) {
828
+ if (cached.length === 0 && this.isVerboseHistoryDebugEnabled()) {
829
829
  logger.debug(
830
830
  `[History] Cache hit but empty for work item ${id} (project=${projectName}, includeAll=${includeAllHistory})`
831
831
  );
@@ -833,11 +833,9 @@ export default class ResultDataProvider {
833
833
  return includeAllHistory ? cached : cached.slice(0, 1);
834
834
  }
835
835
 
836
- logger.debug(`[History] Cache miss. Fetching discussion for work item ${id} (project=${projectName})`);
837
-
838
836
  const fromComments = await this.tryFetchDiscussionFromComments(projectName, id);
839
837
  if (fromComments !== null) {
840
- if (fromComments.length === 0) {
838
+ if (fromComments.length === 0 && this.isVerboseHistoryDebugEnabled()) {
841
839
  logger.debug(
842
840
  `[History] Comments API returned 0 entries for work item ${id} (project=${projectName}, includeAll=${includeAllHistory})`
843
841
  );
@@ -869,11 +867,6 @@ export default class ResultDataProvider {
869
867
  const seen = new Set<string>();
870
868
  const out: any[] = [];
871
869
 
872
- let droppedEmpty = 0;
873
- let droppedSystem = 0;
874
- let droppedAfterStrip = 0;
875
- let droppedDup = 0;
876
-
877
870
  for (const e of list) {
878
871
  const createdDate = String(e?.createdDate ?? '').trim();
879
872
  const createdBy = String(e?.createdBy ?? '').trim();
@@ -881,23 +874,19 @@ export default class ResultDataProvider {
881
874
  const text = typeof textRaw === 'string' ? textRaw.trim() : '';
882
875
 
883
876
  if (!text) {
884
- droppedEmpty++;
885
877
  continue;
886
878
  }
887
879
  if (this.isSystemIdentity(createdBy)) {
888
- droppedSystem++;
889
880
  continue;
890
881
  }
891
882
 
892
883
  const textForKey = this.stripHtmlForEmptiness(text);
893
884
  if (!textForKey) {
894
- droppedAfterStrip++;
895
885
  continue;
896
886
  }
897
887
 
898
888
  const key = `${createdDate}|${createdBy}|${textForKey}`;
899
889
  if (seen.has(key)) {
900
- droppedDup++;
901
890
  continue;
902
891
  }
903
892
  seen.add(key);
@@ -905,15 +894,23 @@ export default class ResultDataProvider {
905
894
  out.push({ createdDate, createdBy, text });
906
895
  }
907
896
 
908
- if (out.length === 0 && list.length > 0) {
909
- logger.debug(
910
- `[History] normalizeDiscussionEntries dropped all entries (in=${list.length}, empty=${droppedEmpty}, system=${droppedSystem}, afterStrip=${droppedAfterStrip}, dup=${droppedDup})`
911
- );
912
- }
913
-
914
897
  return out;
915
898
  }
916
899
 
900
+ private isVerboseHistoryDebugEnabled(): boolean {
901
+ return (
902
+ String(process?.env?.DOCGEN_VERBOSE_HISTORY_DEBUG ?? '').toLowerCase() === 'true' ||
903
+ String(process?.env?.DOCGEN_VERBOSE_HISTORY_DEBUG ?? '') === '1'
904
+ );
905
+ }
906
+
907
+ private isRunResultDebugEnabled(): boolean {
908
+ return (
909
+ String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '').toLowerCase() === 'true' ||
910
+ String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '') === '1'
911
+ );
912
+ }
913
+
917
914
  private extractCommentText(comment: AdoWorkItemComment): string {
918
915
  const rendered = comment?.renderedText;
919
916
  // In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
@@ -1037,6 +1034,7 @@ export default class ResultDataProvider {
1037
1034
  let page = 0;
1038
1035
  const MAX_PAGES = 50;
1039
1036
  const seenTokens = new Set<string>();
1037
+ const verbose = this.isVerboseHistoryDebugEnabled();
1040
1038
  const pageResponsesForDebug: { page: number; data: any; headers: any }[] = [];
1041
1039
  let firstPageDebug: { url: string; data: any; headers: any } | null = null;
1042
1040
 
@@ -1058,14 +1056,14 @@ export default class ResultDataProvider {
1058
1056
  TFSServices.getItemContentWithHeaders(url, this.token, 'get', {}, {}, false)
1059
1057
  );
1060
1058
 
1061
- if (!firstPageDebug) {
1059
+ if (verbose && !firstPageDebug) {
1062
1060
  firstPageDebug = { url, data, headers };
1063
1061
  }
1064
1062
  const response = (data ?? {}) as AdoWorkItemCommentsResponse;
1065
1063
  const comments = Array.isArray(response.comments) ? response.comments : [];
1066
1064
  all.push(...comments);
1067
1065
 
1068
- if (pageResponsesForDebug.length < 2) {
1066
+ if (verbose && pageResponsesForDebug.length < 2) {
1069
1067
  pageResponsesForDebug.push({ page: page + 1, data, headers });
1070
1068
  }
1071
1069
 
@@ -1089,23 +1087,15 @@ export default class ResultDataProvider {
1089
1087
  } while (continuationToken);
1090
1088
 
1091
1089
  if (all.length === 0) {
1092
- if (firstPageDebug) {
1090
+ if (verbose && firstPageDebug) {
1093
1091
  logger.debug(
1094
- `[History][comments] 0 comments returned for work item ${workItemId}. ` +
1095
- `url=${firstPageDebug.url}`
1092
+ `[History][comments] 0 comments returned for work item ${workItemId}. url=${firstPageDebug.url}`
1096
1093
  );
1097
1094
  logger.debug(
1098
1095
  `[History][comments] Raw comments API response (truncated) for work item ${workItemId} (page=1): ` +
1099
- this.stringifyForDebug(firstPageDebug.data, 20000)
1100
- );
1101
- logger.debug(
1102
- `[History][comments] Response headers for work item ${workItemId} (page=1): ` +
1103
- this.stringifyForDebug(firstPageDebug.headers, 2000)
1096
+ this.stringifyForDebug(firstPageDebug.data, 5000)
1104
1097
  );
1105
1098
  }
1106
-
1107
- await this.debugProbeHistoryFromUpdates(projectName, workItemId);
1108
- await this.debugProbeHistoryFromRevisions(projectName, workItemId);
1109
1099
  return [];
1110
1100
  }
1111
1101
 
@@ -1149,22 +1139,19 @@ export default class ResultDataProvider {
1149
1139
  .filter(Boolean) as any[];
1150
1140
 
1151
1141
  if (entries.length === 0 && all.length > 0) {
1152
- logger.debug(
1153
- `[History][comments] Work item ${workItemId} returned ${all.length} comments but 0 usable entries ` +
1154
- `(deleted=${deletedCount}, emptyRaw=${emptyRawCount}, emptyAfterStrip=${emptyAfterStripCount}, system=${systemIdentityCount}, pages=${page}, emptyRawIds=${emptyRawIds.join(
1155
- ','
1156
- )})`
1157
- );
1158
- const maxChars = 20000;
1159
- for (const p of pageResponsesForDebug) {
1160
- logger.debug(
1161
- `[History][comments] Raw comments API response (truncated) for work item ${workItemId} (page=${p.page}): ` +
1162
- this.stringifyForDebug(p.data, maxChars)
1163
- );
1142
+ if (verbose) {
1164
1143
  logger.debug(
1165
- `[History][comments] Response headers for work item ${workItemId} (page=${p.page}): ` +
1166
- this.stringifyForDebug(p.headers, 2000)
1144
+ `[History][comments] Work item ${workItemId} returned ${all.length} comments but 0 usable entries ` +
1145
+ `(deleted=${deletedCount}, emptyRaw=${emptyRawCount}, emptyAfterStrip=${emptyAfterStripCount}, system=${systemIdentityCount}, pages=${page}, emptyRawIds=${emptyRawIds.join(
1146
+ ','
1147
+ )})`
1167
1148
  );
1149
+ for (const p of pageResponsesForDebug) {
1150
+ logger.debug(
1151
+ `[History][comments] Raw comments API response (truncated) for work item ${workItemId} (page=${p.page}): ` +
1152
+ this.stringifyForDebug(p.data, 5000)
1153
+ );
1154
+ }
1168
1155
  }
1169
1156
  }
1170
1157
 
@@ -1180,6 +1167,7 @@ export default class ResultDataProvider {
1180
1167
  }
1181
1168
 
1182
1169
  private async debugProbeHistoryFromUpdates(projectName: string, workItemId: number): Promise<void> {
1170
+ if (!this.isVerboseHistoryDebugEnabled()) return;
1183
1171
  try {
1184
1172
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${workItemId}/updates?$top=200&api-version=7.1-preview.3`;
1185
1173
  const { data, headers } = await this.limit(() =>
@@ -1232,12 +1220,6 @@ export default class ResultDataProvider {
1232
1220
  `[History][updates] No System.History found for work item ${workItemId}. ` +
1233
1221
  `Field keys seen (first ${fieldKeys.length}): ${this.stringifyForDebug(fieldKeys, 4000)}`
1234
1222
  );
1235
- logger.debug(
1236
- `[History][updates] Sample update payloads (truncated) for work item ${workItemId}: ${this.stringifyForDebug(
1237
- updateSamplesForDebug,
1238
- 20000
1239
- )}`
1240
- );
1241
1223
  }
1242
1224
 
1243
1225
  const continuation = this.getContinuationToken(headers, undefined);
@@ -1254,6 +1236,7 @@ export default class ResultDataProvider {
1254
1236
  }
1255
1237
 
1256
1238
  private async debugProbeHistoryFromRevisions(projectName: string, workItemId: number): Promise<void> {
1239
+ if (!this.isVerboseHistoryDebugEnabled()) return;
1257
1240
  try {
1258
1241
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${workItemId}/revisions?$top=200&api-version=7.1-preview.3`;
1259
1242
  const { data } = await this.limit(() =>