@gitkraken/core-gitlens 0.5.114 → 0.5.116

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.
Files changed (162) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/plus/git-github/api/github.d.ts +3 -2
  3. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  4. package/dist/plus/git-github/api/github.js +37 -37
  5. package/dist/plus/git-github/api/github.js.map +1 -1
  6. package/dist/plus/integrations/collectionMetadata.d.ts +2 -0
  7. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  8. package/dist/plus/integrations/collectionMetadata.js +17 -1
  9. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  10. package/dist/plus/integrations/context.d.ts +4 -1
  11. package/dist/plus/integrations/context.d.ts.map +1 -1
  12. package/dist/plus/integrations/index.d.ts +1 -0
  13. package/dist/plus/integrations/index.d.ts.map +1 -1
  14. package/dist/plus/integrations/integrationService.d.ts +9 -1
  15. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  16. package/dist/plus/integrations/integrationService.js +8 -4
  17. package/dist/plus/integrations/integrationService.js.map +1 -1
  18. package/dist/plus/integrations/manager.d.ts +65 -6
  19. package/dist/plus/integrations/manager.d.ts.map +1 -1
  20. package/dist/plus/integrations/models/gitHostIntegration.d.ts +22 -26
  21. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  22. package/dist/plus/integrations/models/gitHostIntegration.js +92 -47
  23. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  24. package/dist/plus/integrations/models/integration.d.ts +9 -0
  25. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  26. package/dist/plus/integrations/models/integration.js +52 -19
  27. package/dist/plus/integrations/models/integration.js.map +1 -1
  28. package/dist/plus/integrations/models/issueCache.d.ts +16 -0
  29. package/dist/plus/integrations/models/issueCache.d.ts.map +1 -0
  30. package/dist/plus/integrations/models/issueCache.js +9 -0
  31. package/dist/plus/integrations/models/issueCache.js.map +1 -0
  32. package/dist/plus/integrations/models/issueReads.d.ts +13 -0
  33. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  34. package/dist/plus/integrations/models/issuesIntegration.d.ts +11 -13
  35. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  36. package/dist/plus/integrations/models/issuesIntegration.js +15 -1
  37. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  38. package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -5
  39. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  40. package/dist/plus/integrations/providers/azureDevOps.js +9 -8
  41. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  42. package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -5
  43. package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -1
  44. package/dist/plus/integrations/providers/bitbucket-server.js +2 -2
  45. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  46. package/dist/plus/integrations/providers/bitbucket.d.ts +3 -5
  47. package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -1
  48. package/dist/plus/integrations/providers/bitbucket.js +2 -1
  49. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  50. package/dist/plus/integrations/providers/github.d.ts +3 -5
  51. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  52. package/dist/plus/integrations/providers/github.js +3 -3
  53. package/dist/plus/integrations/providers/github.js.map +1 -1
  54. package/dist/plus/integrations/providers/gitlab.d.ts +3 -5
  55. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  56. package/dist/plus/integrations/providers/gitlab.js +2 -2
  57. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  58. package/dist/plus/integrations/providers/jira.d.ts +3 -7
  59. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  60. package/dist/plus/integrations/providers/jira.js +35 -22
  61. package/dist/plus/integrations/providers/jira.js.map +1 -1
  62. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +7 -0
  63. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts.map +1 -0
  64. package/dist/plus/integrations/providers/jiraIssueByKey.js +102 -0
  65. package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -0
  66. package/dist/plus/integrations/providers/linear.d.ts +3 -7
  67. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  68. package/dist/plus/integrations/providers/linear.js +23 -22
  69. package/dist/plus/integrations/providers/linear.js.map +1 -1
  70. package/dist/plus/integrations/providers/models.d.ts +3 -1
  71. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  72. package/dist/plus/integrations/providers/models.js +10 -4
  73. package/dist/plus/integrations/providers/models.js.map +1 -1
  74. package/dist/plus/integrations/providers/providerErrors.d.ts +7 -0
  75. package/dist/plus/integrations/providers/providerErrors.d.ts.map +1 -0
  76. package/dist/plus/integrations/providers/providerErrors.js +91 -0
  77. package/dist/plus/integrations/providers/providerErrors.js.map +1 -0
  78. package/dist/plus/integrations/providers/providersApi.d.ts +3 -2
  79. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  80. package/dist/plus/integrations/providers/providersApi.js +18 -52
  81. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  82. package/dist/plus/integrations/providers/trello.d.ts +2 -7
  83. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  84. package/dist/plus/integrations/providers/trello.js +3 -1
  85. package/dist/plus/integrations/providers/trello.js.map +1 -1
  86. package/dist/plus/integrations/providers/utils/providerPaging.d.ts +2 -0
  87. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  88. package/dist/plus/integrations/providers/utils/providerPaging.js +12 -7
  89. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  90. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  91. package/dist/plus/integrations/reads/broaden.js +26 -14
  92. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  93. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  94. package/dist/plus/integrations/reads/counts.js +39 -15
  95. package/dist/plus/integrations/reads/counts.js.map +1 -1
  96. package/dist/plus/integrations/reads/filters.d.ts +33 -11
  97. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  98. package/dist/plus/integrations/reads/filters.js +114 -25
  99. package/dist/plus/integrations/reads/filters.js.map +1 -1
  100. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  101. package/dist/plus/integrations/reads/issueTracker.js +23 -17
  102. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  103. package/dist/plus/integrations/reads/searchIssues.d.ts +1 -5
  104. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  105. package/dist/plus/integrations/reads/searchIssues.js +12 -6
  106. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  107. package/dist/plus/integrations/reads/searchPullRequests.d.ts +1 -0
  108. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
  109. package/dist/plus/integrations/reads/searchPullRequests.js +8 -3
  110. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  111. package/dist/plus/integrations/reads/trackerIssue.d.ts +21 -0
  112. package/dist/plus/integrations/reads/trackerIssue.d.ts.map +1 -0
  113. package/dist/plus/integrations/reads/trackerIssue.js +51 -0
  114. package/dist/plus/integrations/reads/trackerIssue.js.map +1 -0
  115. package/dist/plus/integrations/reads/warnings.d.ts +18 -15
  116. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  117. package/dist/plus/integrations/reads/warnings.js +37 -1
  118. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  119. package/dist/plus/integrations/results.d.ts +4 -20
  120. package/dist/plus/integrations/results.d.ts.map +1 -1
  121. package/dist/plus/integrations/results.js.map +1 -1
  122. package/dist/utils/promise.d.ts +2 -0
  123. package/dist/utils/promise.d.ts.map +1 -1
  124. package/dist/utils/promise.js +11 -0
  125. package/dist/utils/promise.js.map +1 -1
  126. package/docs/integrations.md +63 -28
  127. package/docs/kepler-read-api-parity.md +13 -0
  128. package/package.json +2 -2
  129. package/src/plus/git-github/api/github.ts +54 -60
  130. package/src/plus/integrations/collectionMetadata.ts +23 -1
  131. package/src/plus/integrations/context.ts +1 -1
  132. package/src/plus/integrations/index.ts +1 -0
  133. package/src/plus/integrations/integrationService.ts +14 -4
  134. package/src/plus/integrations/manager.ts +65 -6
  135. package/src/plus/integrations/models/gitHostIntegration.ts +117 -129
  136. package/src/plus/integrations/models/integration.ts +56 -22
  137. package/src/plus/integrations/models/issueCache.ts +29 -0
  138. package/src/plus/integrations/models/issueReads.ts +15 -0
  139. package/src/plus/integrations/models/issuesIntegration.ts +38 -8
  140. package/src/plus/integrations/providers/azureDevOps.ts +16 -13
  141. package/src/plus/integrations/providers/bitbucket-server.ts +4 -6
  142. package/src/plus/integrations/providers/bitbucket.ts +3 -4
  143. package/src/plus/integrations/providers/github.ts +5 -7
  144. package/src/plus/integrations/providers/gitlab.ts +4 -6
  145. package/src/plus/integrations/providers/jira.ts +52 -26
  146. package/src/plus/integrations/providers/jiraIssueByKey.ts +155 -0
  147. package/src/plus/integrations/providers/linear.ts +38 -25
  148. package/src/plus/integrations/providers/models.ts +17 -5
  149. package/src/plus/integrations/providers/providerErrors.ts +115 -0
  150. package/src/plus/integrations/providers/providersApi.ts +26 -68
  151. package/src/plus/integrations/providers/trello.ts +5 -4
  152. package/src/plus/integrations/providers/utils/providerPaging.ts +14 -7
  153. package/src/plus/integrations/reads/broaden.ts +35 -16
  154. package/src/plus/integrations/reads/counts.ts +62 -15
  155. package/src/plus/integrations/reads/filters.ts +131 -29
  156. package/src/plus/integrations/reads/issueTracker.ts +26 -24
  157. package/src/plus/integrations/reads/searchIssues.ts +23 -13
  158. package/src/plus/integrations/reads/searchPullRequests.ts +16 -2
  159. package/src/plus/integrations/reads/trackerIssue.ts +105 -0
  160. package/src/plus/integrations/reads/warnings.ts +42 -17
  161. package/src/plus/integrations/results.ts +4 -20
  162. package/src/utils/promise.ts +15 -0
@@ -17,6 +17,7 @@ import type {
17
17
  } from './reads/counts.js';
18
18
  import type { SupportedFilters } from './reads/filters.js';
19
19
  import type { IssueBatchResult, IssueBatchTarget } from './reads/issueBatch.js';
20
+ import type { TrackerIssueResult } from './reads/trackerIssue.js';
20
21
  import type {
21
22
  ConnectionStateChangeEvent,
22
23
  ProviderBroadenResult,
@@ -403,15 +404,36 @@ export interface IntegrationManager {
403
404
  */
404
405
  searchPullRequestsPage(options: {
405
406
  providerId: IntegrationIds;
406
- /** Repository descriptors that bound the search; ids cannot name provider search qualifiers. */
407
+ /**
408
+ * Repository descriptors that bound the search; ids cannot name provider search qualifiers.
409
+ *
410
+ * Both halves of a descriptor must reach the provider unchanged — see `org` — since a search names a
411
+ * repository by its `namespace/name` path.
412
+ */
407
413
  repos?: ProviderRepositoriesInput;
408
- /** Organization/account that bounds the search. */
414
+ /**
415
+ * Organization/account that bounds the search.
416
+ *
417
+ * Held to a STRICTER rule than the free-form `criteria.text`, which is sanitized: a scope name carrying a
418
+ * quote, or an inner space or control character, is REFUSED (warning + `fetchFailed`), and the refusal
419
+ * names the value. Leading and trailing whitespace and control characters are stripped and accepted,
420
+ * since removing them cannot change which scope the query names. Sanitizing a scope would answer the wrong question — the sanitized value may name a real but
421
+ * DIFFERENT organization, whose result looks entirely normal. Pass the name exactly as the provider spells
422
+ * it; `''` means "no org supplied" and falls through to the other scopes.
423
+ */
409
424
  org?: string;
410
425
  criteria?: PullRequestSearchCriteria;
411
426
  /** Cursor-only: without a cursor, reaching page N costs O(N) upstream requests. */
412
427
  page?: number;
413
428
  cursor?: string;
414
- /** Page size per relationship × state facet; the deduped union can contain more rows. */
429
+ /**
430
+ * Page size PER RELATIONSHIP × STATE facet, not per page. Each facet is its own aliased provider query —
431
+ * one axis more than {@link searchIssuesPage}'s per-relationship fan-out — so a page of a 3-relationship,
432
+ * 2-state search returns up to `6 × itemsPerPage` items before deduplication, and fewer than that where
433
+ * the facets overlap. Deduplication does NOT bring the page back to this size: it removes only the rows the
434
+ * facets share. `page.itemsPerPage` reports what actually came back, so size the UI off that rather than
435
+ * off this. A provider may also cap it below what is asked for.
436
+ */
415
437
  itemsPerPage?: number;
416
438
  forceSync?: boolean;
417
439
  connectionId?: string;
@@ -419,7 +441,9 @@ export interface IntegrationManager {
419
441
  domain?: string;
420
442
  /**
421
443
  * Requests the lightweight row shape: identity, body, author, repository, branch refs and stack info,
422
- * without review, check or diff statistics. It also raises the default page size.
444
+ * without review, check or diff statistics. It also replaces the flat per-facet default page size with a
445
+ * fixed budget shared across the search's ACTIVE facets, so with up to three of them it raises the page
446
+ * and from four on it LOWERS it — measured against GitHub, a 100-row budget versus a flat 30 each.
423
447
  *
424
448
  * A provider without a lightweight projection ignores it and returns its usual shape, so this is a hint
425
449
  * rather than a contract about which fields are present.
@@ -512,9 +536,23 @@ export interface IntegrationManager {
512
536
  */
513
537
  searchIssuesPage(options: {
514
538
  providerId: IntegrationIds;
515
- /** Repositories to search. Combines with `org`; both constrain the same query. */
539
+ /**
540
+ * Repositories to search. Combines with `org`; both constrain the same query.
541
+ *
542
+ * Both halves of a descriptor must reach the provider unchanged — see `org` — since a search names a
543
+ * repository by its `namespace/name` path.
544
+ */
516
545
  repos?: ProviderRepositoriesInput;
517
- /** Organization/account to search. Combines with `repos`. */
546
+ /**
547
+ * Organization/account to search. Combines with `repos`.
548
+ *
549
+ * Held to a STRICTER rule than the free-form criteria, which are sanitized: a scope name carrying a quote,
550
+ * or an inner space or control character, is REFUSED (warning + `fetchFailed`), and the refusal names the
551
+ * value. Leading and trailing whitespace and control characters are stripped and accepted, since removing
552
+ * them cannot change which scope the query names.
553
+ * The sanitized value may name a real but DIFFERENT org, whose result looks entirely normal. `''` means
554
+ * "no org supplied" and falls through to the other scopes.
555
+ */
518
556
  org?: string;
519
557
  criteria?: IssueSearchCriteria;
520
558
  /**
@@ -589,6 +627,27 @@ export interface IntegrationManager {
589
627
  /** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
590
628
  domain?: string;
591
629
  }): Promise<ProviderResult<IssueBatchResult>>;
630
+ /**
631
+ * Resolves one issue-tracker issue by key within a resource — the tracker counterpart of
632
+ * {@link getIssuesBatch}, which cannot serve one.
633
+ *
634
+ * `issue: undefined` is a proven absence and may be cached. A failed read returns no item and sets
635
+ * `fetchFailed`. `resourceId` is required and trusted; Jira also requires the resource's site URL so the result
636
+ * retains its browser link. The read performs no resource discovery.
637
+ *
638
+ * Jira and Linear only. Trello refuses: its single-issue read falls back to a capped board scan for a numeric
639
+ * identifier, so it cannot prove absence.
640
+ */
641
+ getTrackerIssue(options: {
642
+ providerId: IntegrationIds;
643
+ /** Provider resource ID for the Atlassian site or Linear workspace. */
644
+ resourceId: string;
645
+ /** Jira site URL from resource discovery. Required for Jira so the result retains a browser link. */
646
+ resourceUrl?: string;
647
+ /** The provider's own key, e.g. `ABC-123`. Not a number. */
648
+ key: string;
649
+ connectionId?: string;
650
+ }): Promise<ProviderResult<TrackerIssueResult>>;
592
651
  /**
593
652
  * How many pull requests match each scope, fetching none of them — the PR twin of {@link countIssues}, behind a
594
653
  * "this will fetch ~N pull requests" preview and a live count next to an unapplied filter. Same cost model,
@@ -13,6 +13,7 @@ import type {
13
13
  import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata.js';
14
14
  import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
15
15
  import type { PullRequestUrlIdentity } from '../../../git/utils/pullRequest.utils.js';
16
+ import { ensureArray } from '../../../utils/array.js';
16
17
  import { isCancellationError } from '../../../utils/cancellation.js';
17
18
  import { gate } from '../../../utils/decorators/gate.js';
18
19
  import { trace } from '../../../utils/decorators/log.js';
@@ -22,7 +23,11 @@ import { getScopedLogger } from '../../../utils/logger.scoped.js';
22
23
  import type { PagedResult } from '../../../utils/paging.js';
23
24
  import type { ProviderAuthenticationSession } from '../authentication/models.js';
24
25
  import { toTokenWithInfo } from '../authentication/models.js';
25
- import { throwIfCallerContractError, toCollectionScopeFailure } from '../collectionMetadata.js';
26
+ import {
27
+ throwIfCallerContractError,
28
+ toCollectionFailureError,
29
+ toCollectionScopeFailure,
30
+ } from '../collectionMetadata.js';
26
31
  import type { IntegrationIds } from '../constants.js';
27
32
  import { GitCloudHostIntegrationId, GitSelfManagedHostIntegrationId } from '../constants.js';
28
33
  import { toError } from '../errors.js';
@@ -50,7 +55,7 @@ import {
50
55
  toProviderPullRequestStates,
51
56
  } from '../providers/models.js';
52
57
  import type { ProvidersApi } from '../providers/providersApi.js';
53
- import { mergeCollectionMetadata } from '../providers/utils/providerPaging.js';
58
+ import { mergeCollectionMetadata, throwIfAllSettledFailed } from '../providers/utils/providerPaging.js';
54
59
  import type {
55
60
  IntegrationResult,
56
61
  IntegrationType,
@@ -113,10 +118,24 @@ function getSelfManagedApiBaseUrl(
113
118
  }
114
119
  }
115
120
 
121
+ /** Read options for {@link GitHostIntegration.searchMyPullRequests} and the provider hook behind it. */
116
122
  export type SearchMyPullRequestsOptions = {
123
+ /** Suppresses the user-facing notification when the read fails. */
124
+ silent?: boolean;
125
+ /**
126
+ * PR states to include (open/closed/merged/all). Providers that cannot express it in a single query
127
+ * filter the normalized results; omitted preserves the historical open-only behavior.
128
+ */
129
+ state?: PullRequestStateFilter;
117
130
  includeReviewRequested?: boolean;
118
131
  };
119
132
 
133
+ /** Read options for {@link GitHostIntegration.searchPullRequests} and the provider hook behind it. */
134
+ export type SearchPullRequestsOptions = {
135
+ /** PR states to include; omitted preserves the provider's default. */
136
+ include?: PullRequestState[];
137
+ };
138
+
120
139
  type MyPullRequestsForReposOptions = {
121
140
  filters?: PullRequestFilter[];
122
141
  cursor?: string;
@@ -747,10 +766,17 @@ export abstract class GitHostIntegration<
747
766
  const customUrl =
748
767
  options?.customUrl ?? getSelfManagedApiBaseUrl(providerId, session.domain || this.domain, session.protocol);
749
768
 
750
- const api = await this.getProvidersApi();
769
+ let api: ProvidersApi;
770
+ try {
771
+ api = await this.getProvidersApi();
772
+ } catch (ex) {
773
+ this.handleProviderException('getIssuesForRepos', ex, { scope: scope, connectionId: connectionId });
774
+ return { error: toError(ex), duration: performance.now() - start };
775
+ }
776
+ const repoIdsInput = api.isRepoIdsInput(reposOrRepoIds);
751
777
  if (
752
778
  providerId !== GitCloudHostIntegrationId.GitLab &&
753
- (api.isRepoIdsInput(reposOrRepoIds) ||
779
+ (repoIdsInput ||
754
780
  (isAzureDevOpsProvider(providerId) &&
755
781
  !reposOrRepoIds.every(repo => repo.project != null && repo.namespace != null)))
756
782
  ) {
@@ -791,7 +817,10 @@ export abstract class GitHostIntegration<
791
817
  try {
792
818
  userAccount = await this.getFilterAccount(api, session, customUrl, organization);
793
819
  } catch (ex) {
794
- Logger.error(ex, 'getIssuesForRepos');
820
+ this.handleProviderException('getIssuesForRepos', ex, {
821
+ scope: scope,
822
+ connectionId: connectionId,
823
+ });
795
824
  return { error: toError(ex), duration: performance.now() - start };
796
825
  }
797
826
 
@@ -861,6 +890,7 @@ export abstract class GitHostIntegration<
861
890
  return { projectInput: projectInput, results: results };
862
891
  }),
863
892
  );
893
+ throwIfAllSettledFailed(settled);
864
894
 
865
895
  for (let i = 0; i < settled.length; i++) {
866
896
  const outcome = settled[i];
@@ -904,6 +934,7 @@ export abstract class GitHostIntegration<
904
934
  cursor.page = options.page;
905
935
  }
906
936
 
937
+ this.resetRequestExceptionCount('getIssuesForRepos');
907
938
  return {
908
939
  value: {
909
940
  values: data,
@@ -921,7 +952,10 @@ export abstract class GitHostIntegration<
921
952
  duration: performance.now() - start,
922
953
  };
923
954
  } catch (ex) {
924
- Logger.error(ex, 'getIssuesForRepos');
955
+ this.handleProviderException('getIssuesForRepos', ex, {
956
+ scope: scope,
957
+ connectionId: connectionId,
958
+ });
925
959
  return { error: toError(ex), duration: performance.now() - start };
926
960
  }
927
961
  }
@@ -937,7 +971,10 @@ export abstract class GitHostIntegration<
937
971
  try {
938
972
  userAccount = await this.getFilterAccount(api, session, customUrl);
939
973
  } catch (ex) {
940
- Logger.error(ex, 'getIssuesForRepos');
974
+ this.handleProviderException('getIssuesForRepos', ex, {
975
+ scope: scope,
976
+ connectionId: connectionId,
977
+ });
941
978
  return { error: toError(ex), duration: performance.now() - start };
942
979
  }
943
980
 
@@ -964,7 +1001,7 @@ export abstract class GitHostIntegration<
964
1001
  };
965
1002
  }
966
1003
 
967
- if (api.getProviderIssuesPagingMode(providerId) === PagingMode.Repo && !api.isRepoIdsInput(reposOrRepoIds)) {
1004
+ if (api.getProviderIssuesPagingMode(providerId) === PagingMode.Repo && !repoIdsInput) {
968
1005
  const cursorInfo = this.parseCursorInfo<PagedRepoInput>(options?.cursor);
969
1006
  const cursors: PagedRepoInput[] = cursorInfo.cursors ?? [];
970
1007
  let repoInputs: PagedRepoInput[] = reposOrRepoIds.map(repo => ({ repo: repo, cursor: undefined }));
@@ -1001,6 +1038,7 @@ export abstract class GitHostIntegration<
1001
1038
  return { repoInput: repoInput, results: results };
1002
1039
  }),
1003
1040
  );
1041
+ throwIfAllSettledFailed(settled);
1004
1042
 
1005
1043
  for (let i = 0; i < settled.length; i++) {
1006
1044
  const outcome = settled[i];
@@ -1038,6 +1076,7 @@ export abstract class GitHostIntegration<
1038
1076
  cursor.page = options.page;
1039
1077
  }
1040
1078
 
1079
+ this.resetRequestExceptionCount('getIssuesForRepos');
1041
1080
  return {
1042
1081
  value: {
1043
1082
  values: data,
@@ -1055,13 +1094,17 @@ export abstract class GitHostIntegration<
1055
1094
  duration: performance.now() - start,
1056
1095
  };
1057
1096
  } catch (ex) {
1058
- Logger.error(ex, 'getIssuesForRepos');
1097
+ this.handleProviderException('getIssuesForRepos', ex, {
1098
+ scope: scope,
1099
+ connectionId: connectionId,
1100
+ });
1059
1101
  return { error: toError(ex), duration: performance.now() - start };
1060
1102
  }
1061
1103
  }
1062
1104
 
1063
1105
  try {
1064
- const result = await api.getIssuesForRepos(toTokenWithInfo(providerId, session), reposOrRepoIds, {
1106
+ const tokenWithInfo = toTokenWithInfo(providerId, session);
1107
+ const result = await api.getIssuesForRepos(tokenWithInfo, reposOrRepoIds, {
1065
1108
  ...getIssuesOptions,
1066
1109
  cursor: options?.cursor,
1067
1110
  baseUrl: customUrl,
@@ -1070,9 +1113,28 @@ export abstract class GitHostIntegration<
1070
1113
  states: states,
1071
1114
  sort: options?.sort,
1072
1115
  });
1116
+ const failures = result.metadata?.failures;
1117
+ if (
1118
+ repoIdsInput &&
1119
+ reposOrRepoIds.length > 0 &&
1120
+ result.values.length === 0 &&
1121
+ failures?.length === reposOrRepoIds.length
1122
+ ) {
1123
+ const ex = toCollectionFailureError(failures[0], tokenWithInfo);
1124
+ this.handleProviderException('getIssuesForRepos', ex, {
1125
+ scope: scope,
1126
+ connectionId: connectionId,
1127
+ });
1128
+ return { error: ex, duration: performance.now() - start };
1129
+ }
1130
+
1131
+ this.resetRequestExceptionCount('getIssuesForRepos');
1073
1132
  return { value: result, duration: performance.now() - start };
1074
1133
  } catch (ex) {
1075
- Logger.error(ex, 'getIssuesForRepos');
1134
+ this.handleProviderException('getIssuesForRepos', ex, {
1135
+ scope: scope,
1136
+ connectionId: connectionId,
1137
+ });
1076
1138
  return { error: toError(ex), duration: performance.now() - start };
1077
1139
  }
1078
1140
  }
@@ -1134,7 +1196,16 @@ export abstract class GitHostIntegration<
1134
1196
  const customUrl =
1135
1197
  options?.customUrl ?? getSelfManagedApiBaseUrl(providerId, session.domain || this.domain, session.protocol);
1136
1198
 
1137
- const api = await this.getProvidersApi();
1199
+ let api: ProvidersApi;
1200
+ try {
1201
+ api = await this.getProvidersApi();
1202
+ } catch (ex) {
1203
+ this.handleProviderException('getPullRequestsForRepos', ex, {
1204
+ scope: scope,
1205
+ connectionId: connectionId,
1206
+ });
1207
+ return { error: toError(ex), duration: performance.now() - start };
1208
+ }
1138
1209
  if (
1139
1210
  providerId !== GitCloudHostIntegrationId.GitLab &&
1140
1211
  (api.isRepoIdsInput(reposOrRepoIds) ||
@@ -1182,14 +1253,20 @@ export abstract class GitHostIntegration<
1182
1253
  try {
1183
1254
  userAccount = await this.getFilterAccount(api, session, customUrl, organization);
1184
1255
  } catch (ex) {
1185
- Logger.error(ex, 'getPullRequestsForRepos');
1256
+ this.handleProviderException('getPullRequestsForRepos', ex, {
1257
+ scope: scope,
1258
+ connectionId: connectionId,
1259
+ });
1186
1260
  return { error: toError(ex), duration: performance.now() - start };
1187
1261
  }
1188
1262
  } else {
1189
1263
  try {
1190
1264
  userAccount = await this.getFilterAccount(api, session, customUrl);
1191
1265
  } catch (ex) {
1192
- Logger.error(ex, 'getPullRequestsForRepos');
1266
+ this.handleProviderException('getPullRequestsForRepos', ex, {
1267
+ scope: scope,
1268
+ connectionId: connectionId,
1269
+ });
1193
1270
  return { error: toError(ex), duration: performance.now() - start };
1194
1271
  }
1195
1272
  }
@@ -1298,6 +1375,7 @@ export abstract class GitHostIntegration<
1298
1375
  return { repoInput: repoInput, results: results };
1299
1376
  }),
1300
1377
  );
1378
+ throwIfAllSettledFailed(settled);
1301
1379
 
1302
1380
  // `allSettled` preserves order, so `settled[i]` is `repoInputs[i]`.
1303
1381
  settled.forEach((outcome, i) => {
@@ -1338,6 +1416,7 @@ export abstract class GitHostIntegration<
1338
1416
  cursor.page = options.page;
1339
1417
  }
1340
1418
 
1419
+ this.resetRequestExceptionCount('getPullRequestsForRepos');
1341
1420
  return {
1342
1421
  value: {
1343
1422
  values: data,
@@ -1355,7 +1434,10 @@ export abstract class GitHostIntegration<
1355
1434
  duration: performance.now() - start,
1356
1435
  };
1357
1436
  } catch (ex) {
1358
- Logger.error(ex, 'getPullRequestsForRepos');
1437
+ this.handleProviderException('getPullRequestsForRepos', ex, {
1438
+ scope: scope,
1439
+ connectionId: connectionId,
1440
+ });
1359
1441
  return { error: toError(ex), duration: performance.now() - start };
1360
1442
  }
1361
1443
  }
@@ -1372,80 +1454,40 @@ export abstract class GitHostIntegration<
1372
1454
  includeRemoteInfo: isAzureDevOpsProvider(providerId) ? true : undefined,
1373
1455
  fields: options?.summary ? summaryPullRequestFields : undefined,
1374
1456
  });
1457
+ this.resetRequestExceptionCount('getPullRequestsForRepos');
1375
1458
  return { value: result, duration: performance.now() - start };
1376
1459
  } catch (ex) {
1377
- Logger.error(ex, 'getPullRequestsForRepos');
1460
+ this.handleProviderException('getPullRequestsForRepos', ex, {
1461
+ scope: scope,
1462
+ connectionId: connectionId,
1463
+ });
1378
1464
  return { error: toError(ex), duration: performance.now() - start };
1379
1465
  }
1380
1466
  }
1381
1467
 
1382
- async searchMyPullRequests(
1383
- repo?: T,
1384
- cancellation?: AbortSignal,
1385
- silent?: boolean,
1386
- connectionId?: string,
1387
- state?: PullRequestStateFilter,
1388
- options?: SearchMyPullRequestsOptions,
1389
- ): Promise<IntegrationResult<PullRequest[] | undefined>>;
1390
- async searchMyPullRequests(
1391
- repos?: T[],
1392
- cancellation?: AbortSignal,
1393
- silent?: boolean,
1394
- connectionId?: string,
1395
- state?: PullRequestStateFilter,
1396
- options?: SearchMyPullRequestsOptions,
1397
- ): Promise<IntegrationResult<PullRequest[] | undefined>>;
1468
+ /** Account-wide (or repo-scoped) search for the current user's pull requests. */
1398
1469
  @trace()
1399
1470
  async searchMyPullRequests(
1400
1471
  repos?: T | T[],
1401
1472
  cancellation?: AbortSignal,
1402
- silent?: boolean,
1403
- connectionId?: string,
1404
- state?: PullRequestStateFilter,
1405
- options?: SearchMyPullRequestsOptions,
1473
+ options?: SearchMyPullRequestsOptions & { connectionId?: string },
1406
1474
  ): Promise<IntegrationResult<PullRequest[] | undefined>> {
1407
1475
  const scope = getScopedLogger();
1476
+ const { connectionId, ...searchOptions } = options ?? {};
1408
1477
  // `connectionId` targets a specific account (multi-account); omitted reads the primary.
1409
1478
  const session = await this.resolveReadSession(connectionId, scope);
1410
1479
  if (session == null) return undefined;
1411
1480
 
1412
1481
  const start = performance.now();
1413
1482
  try {
1414
- // Prefer the optional metadata-aware path for account-wide reads so partial failures (e.g. one Azure
1415
- // org rejecting) are surfaced as a soft `{ value, error }` instead of being lost. Repo-scoped reads and
1416
- // providers without that override keep using the legacy array path.
1417
- let result: IntegrationResult<PullRequest[] | undefined>;
1418
- if (this.searchProviderMyPullRequestsResult != null && repos == null) {
1419
- result = await this.searchProviderMyPullRequestsResult(
1420
- session,
1421
- repos != null ? (Array.isArray(repos) ? repos : [repos]) : undefined,
1422
- cancellation,
1423
- silent,
1424
- state,
1425
- options,
1426
- );
1427
- } else {
1428
- result = {
1429
- value: await this.searchProviderMyPullRequests(
1430
- session,
1431
- repos != null ? (Array.isArray(repos) ? repos : [repos]) : undefined,
1432
- cancellation,
1433
- silent,
1434
- state,
1435
- options,
1436
- ),
1437
- };
1438
- }
1483
+ const prs = await this.searchProviderMyPullRequests(
1484
+ session,
1485
+ ensureArray(repos),
1486
+ cancellation,
1487
+ searchOptions,
1488
+ );
1439
1489
  this.resetRequestExceptionCount('searchMyPullRequests');
1440
- // `IntegrationResult` is a strict union of value-only or error-only (and may be `undefined`). Return the
1441
- // matching branch explicitly; a missing result is treated as a successful empty read.
1442
- if (result == null) {
1443
- return { value: undefined, duration: performance.now() - start };
1444
- }
1445
- if (result.error != null) {
1446
- return { error: result.error, duration: performance.now() - start };
1447
- }
1448
- return { value: result.value, duration: performance.now() - start };
1490
+ return { value: prs, duration: performance.now() - start };
1449
1491
  } catch (ex) {
1450
1492
  this.handleProviderException('searchMyPullRequests', ex, {
1451
1493
  scope: scope,
@@ -1542,34 +1584,13 @@ export abstract class GitHostIntegration<
1542
1584
  }
1543
1585
  }
1544
1586
 
1545
- // `state` selects which PR states to include (open/closed/merged/all). Providers that cannot express it
1546
- // in a single query filter the normalized results; omitted preserves the historical open-only behavior.
1547
1587
  protected abstract searchProviderMyPullRequests(
1548
1588
  session: ProviderAuthenticationSession,
1549
1589
  repos?: T[],
1550
1590
  cancellation?: AbortSignal,
1551
- silent?: boolean,
1552
- state?: PullRequestStateFilter,
1553
1591
  options?: SearchMyPullRequestsOptions,
1554
1592
  ): Promise<PullRequest[] | undefined>;
1555
1593
 
1556
- /**
1557
- * Optional metadata-aware counterpart of {@link searchProviderMyPullRequests}. Providers whose account-wide
1558
- * "my PRs" read already produces {@link ProviderApiPagedResult} with completeness/failures can override
1559
- * this to return a soft `{ value, error }` result so `searchMyPullRequests` surfaces partial data and a
1560
- * warning instead of silently discarding the failure signal. The wrapper prefers this when present; the
1561
- * abstract {@link searchProviderMyPullRequests} remains the required fallback for repo-scoped and
1562
- * metadata-oblivious paths.
1563
- */
1564
- protected searchProviderMyPullRequestsResult?(
1565
- session: ProviderAuthenticationSession,
1566
- repos?: T[],
1567
- cancellation?: AbortSignal,
1568
- silent?: boolean,
1569
- state?: PullRequestStateFilter,
1570
- options?: SearchMyPullRequestsOptions,
1571
- ): Promise<IntegrationResult<PullRequest[] | undefined>>;
1572
-
1573
1594
  /**
1574
1595
  * Result-returning wrapper for the filtered pull-request search. Errors become the soft
1575
1596
  * `{ error }` branch so the public facade can preserve an empty account and a failed request as distinct
@@ -1618,49 +1639,16 @@ export abstract class GitHostIntegration<
1618
1639
  cancellation?: AbortSignal,
1619
1640
  ): Promise<ProviderPullRequestSearchPage | undefined>;
1620
1641
 
1621
- async searchPullRequests(
1622
- searchQuery: string,
1623
- repo?: T,
1624
- cancellation?: AbortSignal,
1625
- options?: { include?: PullRequestState[] },
1626
- ): Promise<PullRequest[] | undefined>;
1627
- async searchPullRequests(
1628
- searchQuery: string,
1629
- repos?: T[],
1630
- cancellation?: AbortSignal,
1631
- options?: { include?: PullRequestState[] },
1632
- ): Promise<PullRequest[] | undefined>;
1633
- async searchPullRequests(
1634
- searchQuery: string,
1635
- repo?: T,
1636
- cancellation?: AbortSignal,
1637
- connectionId?: string,
1638
- options?: { include?: PullRequestState[] },
1639
- ): Promise<PullRequest[] | undefined>;
1640
- async searchPullRequests(
1641
- searchQuery: string,
1642
- repos?: T[],
1643
- cancellation?: AbortSignal,
1644
- connectionId?: string,
1645
- options?: { include?: PullRequestState[] },
1646
- ): Promise<PullRequest[] | undefined>;
1642
+ /** Free-text search across pull requests, optionally narrowed to `repos`. */
1647
1643
  @trace()
1648
1644
  async searchPullRequests(
1649
1645
  searchQuery: string,
1650
1646
  repos?: T | T[],
1651
1647
  cancellation?: AbortSignal,
1652
- connectionIdOrOptions?: string | { include?: PullRequestState[] },
1653
- options?: { include?: PullRequestState[] },
1648
+ options?: SearchPullRequestsOptions & { connectionId?: string },
1654
1649
  ): Promise<PullRequest[] | undefined> {
1655
- // `connectionId` (string) can be omitted when passing `options`; split the overlapping 4th arg.
1656
- // When the 4th arg isn't the options object (a string `connectionId` or `undefined`), fall back to
1657
- // the 5th-arg `options` so an explicit `undefined` connectionId still honors state filtering.
1658
- const connectionId = typeof connectionIdOrOptions === 'string' ? connectionIdOrOptions : undefined;
1659
- const searchOptions =
1660
- connectionIdOrOptions != null && typeof connectionIdOrOptions !== 'string'
1661
- ? connectionIdOrOptions
1662
- : options;
1663
1650
  const scope = getScopedLogger();
1651
+ const { connectionId, ...searchOptions } = options ?? {};
1664
1652
  // `connectionId` targets a specific account (multi-account); omitted reads the primary.
1665
1653
  const session = await this.resolveReadSession(connectionId, scope);
1666
1654
  if (session == null) return undefined;
@@ -1669,7 +1657,7 @@ export abstract class GitHostIntegration<
1669
1657
  const prs = await this.searchProviderPullRequests?.(
1670
1658
  session,
1671
1659
  searchQuery,
1672
- repos != null ? (Array.isArray(repos) ? repos : [repos]) : undefined,
1660
+ ensureArray(repos),
1673
1661
  cancellation,
1674
1662
  searchOptions,
1675
1663
  );
@@ -1686,7 +1674,7 @@ export abstract class GitHostIntegration<
1686
1674
  searchQuery: string,
1687
1675
  repos?: T[],
1688
1676
  cancellation?: AbortSignal,
1689
- options?: { include?: PullRequestState[] },
1677
+ options?: SearchPullRequestsOptions,
1690
1678
  ): Promise<PullRequest[] | undefined>;
1691
1679
 
1692
1680
  /**