paperclip-github-plugin 0.5.1 → 0.5.2

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/dist/worker.js CHANGED
@@ -728,6 +728,17 @@ var PENDING_STATUS_CONTEXT_STATES = /* @__PURE__ */ new Set(["EXPECTED", "PENDIN
728
728
  var GITHUB_REPOSITORY_MAINTAINER_WARMUP_CONCURRENCY = 4;
729
729
  var GITHUB_REPOSITORY_MAINTAINER_ROLE_NAMES = /* @__PURE__ */ new Set(["admin", "maintain"]);
730
730
  var GITHUB_REPOSITORY_TRUSTED_AUTHOR_ASSOCIATIONS = /* @__PURE__ */ new Set(["collaborator", "member", "owner"]);
731
+ var ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES = /* @__PURE__ */ new Set([
732
+ "behind",
733
+ "blocked",
734
+ "dirty",
735
+ "draft",
736
+ "unstable"
737
+ ]);
738
+ var REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES = /* @__PURE__ */ new Set([
739
+ "clean",
740
+ "has_hooks"
741
+ ]);
731
742
  var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
732
743
  query GitHubIssueStatusSnapshot($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
733
744
  repository(owner: $owner, name: $repo) {
@@ -762,6 +773,8 @@ var GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY = `
762
773
  query GitHubPullRequestCiContexts($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
763
774
  repository(owner: $owner, name: $repo) {
764
775
  pullRequest(number: $pullRequestNumber) {
776
+ mergeable
777
+ mergeStateStatus
765
778
  statusCheckRollup {
766
779
  contexts(first: 100, after: $after) {
767
780
  pageInfo {
@@ -825,6 +838,8 @@ var GITHUB_REPOSITORY_OPEN_PULL_REQUEST_STATUSES_QUERY = `
825
838
  }
826
839
  nodes {
827
840
  number
841
+ mergeable
842
+ mergeStateStatus
828
843
  reviewThreads(first: 100) {
829
844
  pageInfo {
830
845
  hasNextPage
@@ -950,6 +965,8 @@ var GITHUB_PROJECT_PULL_REQUEST_SUMMARY_FIELDS = `${GITHUB_PROJECT_PULL_REQUEST_
950
965
  var GITHUB_PROJECT_PULL_REQUEST_METRICS_FIELDS = `
951
966
  number
952
967
  mergeable
968
+ mergeStateStatus
969
+ baseRefName
953
970
  reviews(first: 100) {
954
971
  pageInfo {
955
972
  hasNextPage
@@ -2219,6 +2236,7 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2219
2236
  githubIssueNumber: entityMatch.data.githubIssueNumber,
2220
2237
  githubIssueUrl: entityMatch.data.githubIssueUrl,
2221
2238
  linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers,
2239
+ linkedPullRequests: entityMatch.data.linkedPullRequests,
2222
2240
  entityRecord: entityMatch
2223
2241
  };
2224
2242
  }
@@ -2240,7 +2258,8 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2240
2258
  githubIssueId: registryMatch.githubIssueId,
2241
2259
  githubIssueNumber: registryMatch.githubIssueNumber,
2242
2260
  githubIssueUrl: githubIssueUrl2,
2243
- linkedPullRequestNumbers: []
2261
+ linkedPullRequestNumbers: [],
2262
+ linkedPullRequests: []
2244
2263
  };
2245
2264
  return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink2) ?? fallbackLink2;
2246
2265
  }
@@ -2258,7 +2277,8 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2258
2277
  repositoryUrl: githubIssueReference.repositoryUrl,
2259
2278
  githubIssueNumber: githubIssueReference.issueNumber,
2260
2279
  githubIssueUrl: githubIssueReference.issueUrl,
2261
- linkedPullRequestNumbers: []
2280
+ linkedPullRequestNumbers: [],
2281
+ linkedPullRequests: []
2262
2282
  };
2263
2283
  return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) ?? fallbackLink;
2264
2284
  }
@@ -2293,7 +2313,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2293
2313
  },
2294
2314
  issueId,
2295
2315
  githubIssue,
2296
- linkedPullRequestNumbers
2316
+ linkedPullRequests
2297
2317
  );
2298
2318
  await upsertGitHubIssueLinkRecord(
2299
2319
  ctx,
@@ -2304,7 +2324,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2304
2324
  },
2305
2325
  issueId,
2306
2326
  githubIssue,
2307
- linkedPullRequestNumbers
2327
+ linkedPullRequests
2308
2328
  );
2309
2329
  return {
2310
2330
  source: "entity",
@@ -2315,6 +2335,7 @@ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLi
2315
2335
  githubIssueNumber: githubIssue.number,
2316
2336
  githubIssueUrl: normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl,
2317
2337
  linkedPullRequestNumbers,
2338
+ linkedPullRequests: normalizeLinkedPullRequestReferences(linkedPullRequests, fallbackLink.repositoryUrl),
2318
2339
  entityRecord
2319
2340
  };
2320
2341
  } catch (error) {
@@ -2418,11 +2439,12 @@ function buildCommentAnnotationLinksFromStoredData(annotation) {
2418
2439
  href: annotation.githubIssueUrl
2419
2440
  }
2420
2441
  ];
2421
- for (const pullRequestNumber of annotation.linkedPullRequestNumbers) {
2442
+ const linkedPullRequests = annotation.linkedPullRequests.length > 0 ? annotation.linkedPullRequests : normalizeLinkedPullRequestReferences(annotation.linkedPullRequestNumbers, annotation.repositoryUrl);
2443
+ for (const pullRequest of linkedPullRequests) {
2422
2444
  links.push({
2423
2445
  type: "pull_request",
2424
- label: `PR #${pullRequestNumber}`,
2425
- href: `${annotation.repositoryUrl}/pull/${pullRequestNumber}`
2446
+ label: formatLinkedPullRequestReferenceLabel(pullRequest, annotation.repositoryUrl),
2447
+ href: `${pullRequest.repositoryUrl}/pull/${pullRequest.number}`
2426
2448
  });
2427
2449
  }
2428
2450
  return links;
@@ -2540,6 +2562,7 @@ async function buildIssueGitHubDetails(ctx, input) {
2540
2562
  githubIssueStateReason: entityMatch.data.githubIssueStateReason,
2541
2563
  commentsCount: entityMatch.data.commentsCount,
2542
2564
  linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers,
2565
+ linkedPullRequests: entityMatch.data.linkedPullRequests,
2543
2566
  labels: entityMatch.data.labels,
2544
2567
  syncedAt: entityMatch.data.syncedAt
2545
2568
  };
@@ -2550,7 +2573,8 @@ async function buildIssueGitHubDetails(ctx, input) {
2550
2573
  githubIssueNumber: link.githubIssueNumber,
2551
2574
  githubIssueUrl: link.githubIssueUrl,
2552
2575
  repositoryUrl: link.repositoryUrl,
2553
- linkedPullRequestNumbers: link.linkedPullRequestNumbers
2576
+ linkedPullRequestNumbers: link.linkedPullRequestNumbers,
2577
+ linkedPullRequests: link.linkedPullRequests
2554
2578
  };
2555
2579
  }
2556
2580
  async function resolveIssueByIdentifier(ctx, input) {
@@ -4010,8 +4034,13 @@ function normalizeProjectPullRequestClosingIssues(repository, nodes) {
4010
4034
  function resolveProjectPullRequestReviewable(record) {
4011
4035
  return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.copilotUnresolvedReviewThreads === "number" && record.copilotUnresolvedReviewThreads === 0;
4012
4036
  }
4037
+ function resolveProjectPullRequestTargetsDefaultBranch(record) {
4038
+ const baseBranch = normalizeOptionalString2(record.baseBranch);
4039
+ const defaultBranchName = normalizeOptionalString2(record.defaultBranchName);
4040
+ return Boolean(baseBranch && defaultBranchName && baseBranch === defaultBranchName);
4041
+ }
4013
4042
  function resolveProjectPullRequestMergeable(record) {
4014
- return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0;
4043
+ return record.githubMergeable === true && record.checksStatus === "passed" && typeof record.reviewApprovals === "number" && record.reviewApprovals > 0 && typeof record.reviewChangesRequested === "number" && record.reviewChangesRequested === 0 && typeof record.unresolvedReviewThreads === "number" && record.unresolvedReviewThreads === 0 && resolveProjectPullRequestTargetsDefaultBranch(record);
4015
4044
  }
4016
4045
  function resolveProjectPullRequestUpToDateStatus(record) {
4017
4046
  const mergeStateStatus = typeof record.mergeStateStatus === "string" ? record.mergeStateStatus : null;
@@ -4194,12 +4223,12 @@ function classifyGitHubPullRequestCiState(contexts) {
4194
4223
  return hasPendingContext ? "unfinished" : "green";
4195
4224
  }
4196
4225
  function resolvePaperclipStatusFromLinkedPullRequests(linkedPullRequests, options) {
4197
- if (linkedPullRequests.some((pullRequest) => pullRequest.hasUnresolvedReviewThreads || pullRequest.ciState === "red")) {
4226
+ if (linkedPullRequests.some(
4227
+ (pullRequest) => pullRequest.hasUnresolvedReviewThreads || pullRequest.ciState === "red" || isGitHubPullRequestActionRequiredForSync(pullRequest)
4228
+ )) {
4198
4229
  return options?.preferInProgress ? "in_progress" : "todo";
4199
4230
  }
4200
- if (linkedPullRequests.length > 0 && linkedPullRequests.every(
4201
- (pullRequest) => pullRequest.ciState === "green" && pullRequest.hasUnresolvedReviewThreads === false
4202
- )) {
4231
+ if (linkedPullRequests.length > 0 && linkedPullRequests.every((pullRequest) => isGitHubPullRequestReviewReadyForSync(pullRequest))) {
4203
4232
  return "in_review";
4204
4233
  }
4205
4234
  return "in_progress";
@@ -4445,15 +4474,19 @@ function resolvePaperclipIssueReviewAssignee(syncContext, advancedSettings) {
4445
4474
  role: nextStage.type === "approval" ? "approver" : "reviewer"
4446
4475
  };
4447
4476
  }
4477
+ const reviewStageType = nextStage?.type ?? currentStageType;
4478
+ if (!reviewStageType && !syncContext.executionPolicy) {
4479
+ return null;
4480
+ }
4448
4481
  const approverOverride = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "approver");
4449
- if (nextStage?.type === "approval" && approverOverride) {
4482
+ if (reviewStageType === "approval" && approverOverride) {
4450
4483
  return {
4451
4484
  principal: approverOverride,
4452
4485
  role: "approver"
4453
4486
  };
4454
4487
  }
4455
4488
  const reviewerOverride = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "reviewer");
4456
- if (reviewerOverride) {
4489
+ if (reviewStageType !== "approval" && reviewerOverride) {
4457
4490
  return {
4458
4491
  principal: reviewerOverride,
4459
4492
  role: "reviewer"
@@ -4541,6 +4574,9 @@ function buildSyncFallbackExecutionStatePatch(params) {
4541
4574
  if ((currentStatus === "done" || currentStatus === "cancelled") && nextStatus !== "done" && nextStatus !== "cancelled") {
4542
4575
  return previousState ? null : void 0;
4543
4576
  }
4577
+ if (nextStatus === "done" || nextStatus === "cancelled") {
4578
+ return previousState ? null : void 0;
4579
+ }
4544
4580
  if (nextStatus === "in_review" && syncContext.executionPolicy) {
4545
4581
  const nextStageMatch = findPaperclipIssueExecutionStageMatch(syncContext.executionPolicy, previousState);
4546
4582
  if (!nextStageMatch) {
@@ -4599,32 +4635,33 @@ function describeGitHubStatusTransitionReason(params) {
4599
4635
  }
4600
4636
  const linkedPullRequestSubject = snapshot.linkedPullRequests.length === 1 ? "the linked pull request" : "linked pull requests";
4601
4637
  const linkedPullRequestVerb = snapshot.linkedPullRequests.length === 1 ? "has" : "have";
4602
- const hasRedCi = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.ciState === "red");
4603
- const hasUnresolvedReviewThreads = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.hasUnresolvedReviewThreads);
4638
+ const blockingConditions = [...new Set(
4639
+ snapshot.linkedPullRequests.flatMap((pullRequest) => listGitHubPullRequestSyncBlockingConditions(pullRequest))
4640
+ )];
4604
4641
  const hasUnfinishedCi = snapshot.linkedPullRequests.some((pullRequest) => pullRequest.ciState === "unfinished");
4605
- if (hasRedCi && hasUnresolvedReviewThreads) {
4606
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} failing CI with unresolved review threads`;
4607
- }
4608
- if (hasRedCi) {
4609
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} failing CI`;
4610
- }
4611
- if (hasUnresolvedReviewThreads) {
4612
- return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unresolved review threads`;
4642
+ const hasUnknownMergeability = snapshot.linkedPullRequests.some(
4643
+ (pullRequest) => pullRequest.mergeStateStatus === "unknown"
4644
+ );
4645
+ if (blockingConditions.length > 0) {
4646
+ return `${linkedPullRequestSubject} ${linkedPullRequestVerb} ${formatPlainTextList(blockingConditions)}`;
4613
4647
  }
4614
4648
  if (hasUnfinishedCi) {
4615
4649
  return `${linkedPullRequestSubject} still ${linkedPullRequestVerb} unfinished CI jobs`;
4616
4650
  }
4651
+ if (hasUnknownMergeability) {
4652
+ return `${linkedPullRequestSubject} ${linkedPullRequestVerb} unknown mergeability`;
4653
+ }
4617
4654
  return `${linkedPullRequestSubject} ${linkedPullRequestVerb} green CI with all review threads resolved`;
4618
4655
  }
4619
4656
  function buildStatusTransitionCommentAnnotation(params) {
4620
4657
  const { repository, snapshot, previousStatus, nextStatus, reason } = params;
4658
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(snapshot.linkedPullRequests);
4621
4659
  return {
4622
4660
  repositoryUrl: repository.url,
4623
4661
  githubIssueNumber: snapshot.issueNumber,
4624
4662
  githubIssueUrl: `${repository.url}/issues/${snapshot.issueNumber}`,
4625
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
4626
- snapshot.linkedPullRequests.map((pullRequest) => pullRequest.number)
4627
- ),
4663
+ linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(linkedPullRequests.map((pullRequest) => pullRequest.number)),
4664
+ linkedPullRequests,
4628
4665
  previousStatus,
4629
4666
  nextStatus,
4630
4667
  reason,
@@ -4695,7 +4732,7 @@ function resolvePaperclipIssueStatus(params) {
4695
4732
  }
4696
4733
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
4697
4734
  const linkedPullRequests = [];
4698
- const seenPullRequestNumbers = /* @__PURE__ */ new Set();
4735
+ const seenPullRequestKeys = /* @__PURE__ */ new Set();
4699
4736
  let after;
4700
4737
  do {
4701
4738
  const response = await octokit.graphql(GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY, {
@@ -4707,31 +4744,37 @@ async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber)
4707
4744
  const nextLinkedPullRequests = collectGitHubLinkedPullRequests(
4708
4745
  response.repository?.issue?.closedByPullRequestsReferences?.nodes ?? [],
4709
4746
  repository,
4710
- seenPullRequestNumbers
4747
+ seenPullRequestKeys
4711
4748
  );
4712
4749
  linkedPullRequests.push(...nextLinkedPullRequests);
4713
4750
  after = getPageCursor(response.repository?.issue?.closedByPullRequestsReferences?.pageInfo);
4714
4751
  } while (after);
4715
4752
  return linkedPullRequests;
4716
4753
  }
4717
- function collectGitHubLinkedPullRequests(nodes, repository, seenPullRequestNumbers = /* @__PURE__ */ new Set()) {
4754
+ function collectGitHubLinkedPullRequests(nodes, repository, seenPullRequestKeys = /* @__PURE__ */ new Set()) {
4718
4755
  const linkedPullRequests = [];
4719
4756
  for (const node of nodes) {
4720
- if (!node || typeof node.number !== "number" || !node.state || seenPullRequestNumbers.has(node.number)) {
4757
+ if (!node || typeof node.number !== "number" || !node.state) {
4721
4758
  continue;
4722
4759
  }
4723
4760
  const pullRequestOwner = node.repository?.owner?.login?.trim();
4724
4761
  const pullRequestRepo = node.repository?.name?.trim();
4725
- if (pullRequestOwner && pullRequestRepo && !areRepositoriesEqual(repository, {
4726
- owner: pullRequestOwner,
4727
- repo: pullRequestRepo
4728
- })) {
4762
+ const pullRequestRepository = pullRequestOwner && pullRequestRepo ? parseRepositoryReference(`${pullRequestOwner}/${pullRequestRepo}`) : repository;
4763
+ if (!pullRequestRepository) {
4764
+ continue;
4765
+ }
4766
+ const pullRequestKey = buildGitHubPullRequestReferenceKey({
4767
+ number: node.number,
4768
+ repositoryUrl: pullRequestRepository.url
4769
+ });
4770
+ if (seenPullRequestKeys.has(pullRequestKey)) {
4729
4771
  continue;
4730
4772
  }
4731
- seenPullRequestNumbers.add(node.number);
4773
+ seenPullRequestKeys.add(pullRequestKey);
4732
4774
  linkedPullRequests.push({
4733
4775
  number: node.number,
4734
- state: node.state
4776
+ state: node.state,
4777
+ repositoryUrl: pullRequestRepository.url
4735
4778
  });
4736
4779
  }
4737
4780
  return linkedPullRequests;
@@ -4789,7 +4832,75 @@ function extractGitHubCiContextRecords(nodes) {
4789
4832
  }
4790
4833
  return contexts;
4791
4834
  }
4792
- function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
4835
+ function normalizeGitHubPullRequestMergeability(value) {
4836
+ if (value === "MERGEABLE") {
4837
+ return "mergeable";
4838
+ }
4839
+ if (value === "CONFLICTING") {
4840
+ return "conflicting";
4841
+ }
4842
+ return "unknown";
4843
+ }
4844
+ function normalizeGitHubPullRequestMergeStateStatus(value) {
4845
+ switch (typeof value === "string" ? value.trim().toLowerCase() : "") {
4846
+ case "behind":
4847
+ return "behind";
4848
+ case "blocked":
4849
+ return "blocked";
4850
+ case "clean":
4851
+ return "clean";
4852
+ case "dirty":
4853
+ return "dirty";
4854
+ case "draft":
4855
+ return "draft";
4856
+ case "has_hooks":
4857
+ return "has_hooks";
4858
+ case "unstable":
4859
+ return "unstable";
4860
+ default:
4861
+ return "unknown";
4862
+ }
4863
+ }
4864
+ function isGitHubPullRequestActionRequiredForSync(pullRequest) {
4865
+ return pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
4866
+ }
4867
+ function isGitHubPullRequestReviewReadyForSync(pullRequest) {
4868
+ if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads) {
4869
+ return false;
4870
+ }
4871
+ return REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
4872
+ }
4873
+ function listGitHubPullRequestSyncBlockingConditions(pullRequest) {
4874
+ const conditions = [];
4875
+ if (pullRequest.ciState === "red") {
4876
+ conditions.push("failing CI");
4877
+ }
4878
+ if (pullRequest.mergeability === "conflicting" || pullRequest.mergeStateStatus === "dirty") {
4879
+ conditions.push("merge conflicts");
4880
+ } else {
4881
+ switch (pullRequest.mergeStateStatus) {
4882
+ case "behind":
4883
+ conditions.push("out-of-date branch state");
4884
+ break;
4885
+ case "blocked":
4886
+ conditions.push("blocked merge requirements");
4887
+ break;
4888
+ case "draft":
4889
+ conditions.push("draft status");
4890
+ break;
4891
+ case "unstable":
4892
+ conditions.push("unstable merge requirements");
4893
+ break;
4894
+ default:
4895
+ break;
4896
+ }
4897
+ }
4898
+ if (pullRequest.hasUnresolvedReviewThreads) {
4899
+ conditions.push("unresolved review threads");
4900
+ }
4901
+ return conditions;
4902
+ }
4903
+ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository) {
4793
4904
  if (typeof node.number !== "number") {
4794
4905
  return null;
4795
4906
  }
@@ -4800,8 +4911,11 @@ function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node) {
4800
4911
  }
4801
4912
  return {
4802
4913
  number: node.number,
4914
+ repositoryUrl: repository.url,
4803
4915
  hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
4804
- ciState
4916
+ ciState,
4917
+ mergeability: normalizeGitHubPullRequestMergeability(node.mergeable),
4918
+ mergeStateStatus: normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus)
4805
4919
  };
4806
4920
  }
4807
4921
  function tryBuildGitHubPullRequestCiStateFromBatchNode(node) {
@@ -4816,7 +4930,9 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4816
4930
  return;
4817
4931
  }
4818
4932
  const remainingNumbers = new Set(
4819
- [...targetPullRequestNumbers].filter((pullRequestNumber) => !pullRequestStatusCache.has(pullRequestNumber))
4933
+ [...targetPullRequestNumbers].filter(
4934
+ (pullRequestNumber) => !getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber)
4935
+ )
4820
4936
  );
4821
4937
  if (remainingNumbers.size === 0) {
4822
4938
  return;
@@ -4837,9 +4953,9 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4837
4953
  continue;
4838
4954
  }
4839
4955
  remainingNumbers.delete(node.number);
4840
- const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node);
4956
+ const snapshot = tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository);
4841
4957
  if (snapshot) {
4842
- pullRequestStatusCache.set(node.number, snapshot);
4958
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4843
4959
  cacheGitHubPullRequestStatusSnapshot(repository, snapshot);
4844
4960
  }
4845
4961
  }
@@ -4850,7 +4966,12 @@ async function warmGitHubPullRequestStatusCache(octokit, repository, targetPullR
4850
4966
  } while (after);
4851
4967
  }
4852
4968
  async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumber) {
4969
+ return (await getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber)).ciState;
4970
+ }
4971
+ async function getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber) {
4853
4972
  const contexts = [];
4973
+ let mergeability = "unknown";
4974
+ let mergeStateStatus = "unknown";
4854
4975
  let after;
4855
4976
  do {
4856
4977
  const response = await octokit.graphql(GITHUB_PULL_REQUEST_CI_CONTEXTS_QUERY, {
@@ -4859,6 +4980,8 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
4859
4980
  pullRequestNumber,
4860
4981
  after
4861
4982
  });
4983
+ mergeability = normalizeGitHubPullRequestMergeability(response.repository?.pullRequest?.mergeable);
4984
+ mergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(response.repository?.pullRequest?.mergeStateStatus);
4862
4985
  const connection = response.repository?.pullRequest?.statusCheckRollup?.contexts;
4863
4986
  const nodes = connection?.nodes ?? [];
4864
4987
  for (const node of nodes) {
@@ -4882,49 +5005,63 @@ async function getGitHubPullRequestCiState(octokit, repository, pullRequestNumbe
4882
5005
  }
4883
5006
  after = getPageCursor(connection?.pageInfo);
4884
5007
  } while (after);
4885
- return classifyGitHubPullRequestCiState(contexts);
5008
+ return {
5009
+ ciState: classifyGitHubPullRequestCiState(contexts),
5010
+ mergeability,
5011
+ mergeStateStatus
5012
+ };
4886
5013
  }
4887
5014
  async function getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequestNumber, pullRequestStatusCache, options) {
4888
- const cached = pullRequestStatusCache.get(pullRequestNumber);
5015
+ const cached = getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber);
4889
5016
  if (cached) {
4890
5017
  return cached;
4891
5018
  }
4892
5019
  const cacheKey = buildRepositoryPullRequestRecordCacheKey(repository, pullRequestNumber, "status");
4893
5020
  const cachedSnapshot = getFreshCacheValue(activeGitHubPullRequestStatusSnapshotCache, cacheKey);
4894
5021
  if (cachedSnapshot) {
4895
- pullRequestStatusCache.set(pullRequestNumber, cachedSnapshot);
5022
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, cachedSnapshot);
4896
5023
  return cachedSnapshot;
4897
5024
  }
4898
- if (options?.reviewThreadSummary && options.ciState) {
5025
+ if (options?.reviewThreadSummary && options.ciState && options.mergeability !== void 0 && options.mergeStateStatus !== void 0) {
4899
5026
  const snapshot = cacheGitHubPullRequestStatusSnapshot(repository, {
4900
5027
  number: pullRequestNumber,
5028
+ repositoryUrl: repository.url,
4901
5029
  hasUnresolvedReviewThreads: options.reviewThreadSummary.unresolvedReviewThreads > 0,
4902
- ciState: options.ciState
5030
+ ciState: options.ciState,
5031
+ mergeability: options.mergeability,
5032
+ mergeStateStatus: options.mergeStateStatus
4903
5033
  });
4904
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5034
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4905
5035
  return snapshot;
4906
5036
  }
4907
5037
  const inFlightSnapshot = activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey);
4908
5038
  if (inFlightSnapshot) {
4909
5039
  const snapshot = await inFlightSnapshot;
4910
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5040
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4911
5041
  return snapshot;
4912
5042
  }
4913
5043
  const loadSnapshotPromise = (async () => {
4914
- const [reviewThreadSummary, ciState] = await Promise.all([
5044
+ const [reviewThreadSummary, ciSnapshot] = await Promise.all([
4915
5045
  options?.reviewThreadSummary ?? getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, repository, pullRequestNumber),
4916
- options?.ciState ?? getGitHubPullRequestCiState(octokit, repository, pullRequestNumber)
5046
+ options?.ciState && options.mergeability !== void 0 && options.mergeStateStatus !== void 0 ? {
5047
+ ciState: options.ciState,
5048
+ mergeability: options.mergeability,
5049
+ mergeStateStatus: options.mergeStateStatus
5050
+ } : getGitHubPullRequestCiSnapshot(octokit, repository, pullRequestNumber)
4917
5051
  ]);
4918
5052
  return cacheGitHubPullRequestStatusSnapshot(repository, {
4919
5053
  number: pullRequestNumber,
5054
+ repositoryUrl: repository.url,
4920
5055
  hasUnresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads > 0,
4921
- ciState
5056
+ ciState: ciSnapshot.ciState,
5057
+ mergeability: ciSnapshot.mergeability,
5058
+ mergeStateStatus: ciSnapshot.mergeStateStatus
4922
5059
  });
4923
5060
  })();
4924
5061
  activeGitHubPullRequestStatusSnapshotPromiseCache.set(cacheKey, loadSnapshotPromise);
4925
5062
  try {
4926
5063
  const snapshot = await loadSnapshotPromise;
4927
- pullRequestStatusCache.set(pullRequestNumber, snapshot);
5064
+ setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot);
4928
5065
  return snapshot;
4929
5066
  } finally {
4930
5067
  if (activeGitHubPullRequestStatusSnapshotPromiseCache.get(cacheKey) === loadSnapshotPromise) {
@@ -4957,8 +5094,14 @@ async function getGitHubIssueStatusSnapshot(octokit, repository, issueNumber, gi
4957
5094
  if (pullRequest.state !== "OPEN") {
4958
5095
  continue;
4959
5096
  }
5097
+ const linkedPullRequestRepository = requireRepositoryReference(pullRequest.repositoryUrl);
4960
5098
  linkedPullRequestSnapshots.push(
4961
- await getGitHubPullRequestStatusSnapshot(octokit, repository, pullRequest.number, pullRequestStatusCache)
5099
+ await getGitHubPullRequestStatusSnapshot(
5100
+ octokit,
5101
+ linkedPullRequestRepository,
5102
+ pullRequest.number,
5103
+ pullRequestStatusCache
5104
+ )
4962
5105
  );
4963
5106
  }
4964
5107
  const snapshot = {
@@ -5212,11 +5355,86 @@ function getHiddenGitHubImportMarkerPattern() {
5212
5355
  "i"
5213
5356
  );
5214
5357
  }
5358
+ function formatPlainTextList(items) {
5359
+ if (items.length === 0) {
5360
+ return "";
5361
+ }
5362
+ if (items.length === 1) {
5363
+ return items[0];
5364
+ }
5365
+ if (items.length === 2) {
5366
+ return `${items[0]} and ${items[1]}`;
5367
+ }
5368
+ return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
5369
+ }
5215
5370
  function normalizeLinkedPullRequestNumbers(values) {
5216
5371
  return [...new Set(
5217
5372
  values.filter((pullRequestNumber) => Number.isInteger(pullRequestNumber) && pullRequestNumber > 0)
5218
5373
  )].sort((left, right) => left - right);
5219
5374
  }
5375
+ function buildGitHubPullRequestReferenceKey(pullRequest) {
5376
+ return `${getNormalizedMappingRepositoryUrl({ repositoryUrl: pullRequest.repositoryUrl }).toLowerCase()}#${Math.max(1, Math.floor(pullRequest.number))}`;
5377
+ }
5378
+ function normalizeLinkedPullRequestReferences(values, fallbackRepositoryUrl) {
5379
+ const references = [];
5380
+ const seenKeys = /* @__PURE__ */ new Set();
5381
+ const normalizedFallbackRepositoryUrl = typeof fallbackRepositoryUrl === "string" && fallbackRepositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
5382
+ repositoryUrl: fallbackRepositoryUrl
5383
+ }) : void 0;
5384
+ for (const value of values) {
5385
+ const number = typeof value === "number" ? Math.floor(value) : value && typeof value === "object" && typeof value.number === "number" ? Math.floor(value.number) : void 0;
5386
+ const repositoryUrl = value && typeof value === "object" && typeof value.repositoryUrl === "string" && value.repositoryUrl.trim() ? getNormalizedMappingRepositoryUrl({
5387
+ repositoryUrl: value.repositoryUrl
5388
+ }) : normalizedFallbackRepositoryUrl;
5389
+ if (!number || number < 1 || !repositoryUrl) {
5390
+ continue;
5391
+ }
5392
+ const referenceKey = buildGitHubPullRequestReferenceKey({
5393
+ number,
5394
+ repositoryUrl
5395
+ });
5396
+ if (seenKeys.has(referenceKey)) {
5397
+ continue;
5398
+ }
5399
+ seenKeys.add(referenceKey);
5400
+ references.push({
5401
+ number,
5402
+ repositoryUrl
5403
+ });
5404
+ }
5405
+ references.sort((left, right) => {
5406
+ const repositoryUrlComparison = left.repositoryUrl.toLowerCase().localeCompare(right.repositoryUrl.toLowerCase());
5407
+ if (repositoryUrlComparison !== 0) {
5408
+ return repositoryUrlComparison;
5409
+ }
5410
+ return left.number - right.number;
5411
+ });
5412
+ return references;
5413
+ }
5414
+ function formatLinkedPullRequestReferenceLabel(pullRequest, issueRepositoryUrl) {
5415
+ const pullRequestRepository = parseRepositoryReference(pullRequest.repositoryUrl);
5416
+ if (!pullRequestRepository) {
5417
+ return `PR #${pullRequest.number}`;
5418
+ }
5419
+ if (issueRepositoryUrl) {
5420
+ const issueRepository = parseRepositoryReference(issueRepositoryUrl);
5421
+ if (issueRepository && areRepositoriesEqual(issueRepository, pullRequestRepository)) {
5422
+ return `PR #${pullRequest.number}`;
5423
+ }
5424
+ }
5425
+ return `${formatRepositoryLabel(pullRequestRepository)}#${pullRequest.number}`;
5426
+ }
5427
+ function getCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, repository, pullRequestNumber) {
5428
+ return pullRequestStatusCache.get(
5429
+ buildGitHubPullRequestReferenceKey({
5430
+ number: pullRequestNumber,
5431
+ repositoryUrl: repository.url
5432
+ })
5433
+ );
5434
+ }
5435
+ function setCachedGitHubPullRequestStatusSnapshot(pullRequestStatusCache, snapshot) {
5436
+ pullRequestStatusCache.set(buildGitHubPullRequestReferenceKey(snapshot), snapshot);
5437
+ }
5220
5438
  function extractImportedGitHubIssueUrlFromDescription(description) {
5221
5439
  if (typeof description !== "string") {
5222
5440
  return void 0;
@@ -5442,6 +5660,14 @@ function normalizeGitHubIssueLinkEntityData(value) {
5442
5660
  if (!repositoryUrl || githubIssueId === void 0 || githubIssueNumber === void 0 || !githubIssueUrl || !githubIssueState || !syncedAt) {
5443
5661
  return null;
5444
5662
  }
5663
+ const linkedPullRequestNumbers = normalizeLinkedPullRequestNumbers(
5664
+ Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5665
+ );
5666
+ const rawLinkedPullRequests = Array.isArray(record.linkedPullRequests) ? record.linkedPullRequests : [];
5667
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(
5668
+ rawLinkedPullRequests.length > 0 ? rawLinkedPullRequests : linkedPullRequestNumbers,
5669
+ repositoryUrl
5670
+ );
5445
5671
  return {
5446
5672
  ...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
5447
5673
  ...typeof record.paperclipProjectId === "string" && record.paperclipProjectId.trim() ? { paperclipProjectId: record.paperclipProjectId.trim() } : {},
@@ -5455,9 +5681,8 @@ function normalizeGitHubIssueLinkEntityData(value) {
5455
5681
  githubIssueState,
5456
5682
  ...githubIssueStateReason ? { githubIssueStateReason } : {},
5457
5683
  commentsCount,
5458
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
5459
- Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5460
- ),
5684
+ linkedPullRequestNumbers,
5685
+ linkedPullRequests,
5461
5686
  labels: normalizeStoredGitHubIssueLabels(record.labels),
5462
5687
  syncedAt
5463
5688
  };
@@ -5507,15 +5732,22 @@ function normalizeStoredStatusTransitionCommentAnnotation(value) {
5507
5732
  if (!repositoryUrl || githubIssueNumber === void 0 || !githubIssueUrl || !previousStatus || !nextStatus || !reason || !createdAt || !paperclipIssueId) {
5508
5733
  return null;
5509
5734
  }
5735
+ const linkedPullRequestNumbers = normalizeLinkedPullRequestNumbers(
5736
+ Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5737
+ );
5738
+ const rawLinkedPullRequests = Array.isArray(record.linkedPullRequests) ? record.linkedPullRequests : [];
5739
+ const linkedPullRequests = normalizeLinkedPullRequestReferences(
5740
+ rawLinkedPullRequests.length > 0 ? rawLinkedPullRequests : linkedPullRequestNumbers,
5741
+ repositoryUrl
5742
+ );
5510
5743
  return {
5511
5744
  ...typeof record.companyId === "string" && record.companyId.trim() ? { companyId: record.companyId.trim() } : {},
5512
5745
  paperclipIssueId,
5513
5746
  repositoryUrl,
5514
5747
  githubIssueNumber,
5515
5748
  githubIssueUrl,
5516
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
5517
- Array.isArray(record.linkedPullRequestNumbers) ? record.linkedPullRequestNumbers.filter((entry) => typeof entry === "number") : []
5518
- ),
5749
+ linkedPullRequestNumbers,
5750
+ linkedPullRequests,
5519
5751
  previousStatus,
5520
5752
  nextStatus,
5521
5753
  reason,
@@ -5642,9 +5874,10 @@ async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
5642
5874
  }
5643
5875
  return null;
5644
5876
  }
5645
- function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers) {
5877
+ function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequests) {
5646
5878
  const githubIssueUrl = normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl;
5647
5879
  const repositoryUrl = parseRepositoryReference(target.repositoryUrl)?.url ?? target.repositoryUrl.trim();
5880
+ const normalizedLinkedPullRequests = normalizeLinkedPullRequestReferences(linkedPullRequests, repositoryUrl);
5648
5881
  return {
5649
5882
  paperclipIssueId: issueId,
5650
5883
  title: `GitHub issue #${githubIssue.number}`,
@@ -5662,14 +5895,17 @@ function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequ
5662
5895
  githubIssueState: githubIssue.state,
5663
5896
  ...githubIssue.stateReason ? { githubIssueStateReason: githubIssue.stateReason } : {},
5664
5897
  commentsCount: githubIssue.commentsCount,
5665
- linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(linkedPullRequestNumbers),
5898
+ linkedPullRequestNumbers: normalizeLinkedPullRequestNumbers(
5899
+ normalizedLinkedPullRequests.map((pullRequest) => pullRequest.number)
5900
+ ),
5901
+ linkedPullRequests: normalizedLinkedPullRequests,
5666
5902
  labels: githubIssue.labels,
5667
5903
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
5668
5904
  }
5669
5905
  };
5670
5906
  }
5671
- async function upsertGitHubIssueLinkRecord(ctx, target, issueId, githubIssue, linkedPullRequestNumbers) {
5672
- const record = buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers);
5907
+ async function upsertGitHubIssueLinkRecord(ctx, target, issueId, githubIssue, linkedPullRequests) {
5908
+ const record = buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequests);
5673
5909
  await ctx.entities.upsert({
5674
5910
  entityType: ISSUE_LINK_ENTITY_TYPE,
5675
5911
  scopeKind: "issue",
@@ -6580,14 +6816,22 @@ async function updatePaperclipIssueState(ctx, params) {
6580
6816
  syncContext,
6581
6817
  nextStatus,
6582
6818
  nextAssignee,
6819
+ clearAssignee,
6583
6820
  transitionComment,
6584
6821
  transitionCommentAnnotation,
6585
6822
  paperclipApiBaseUrl
6586
6823
  } = params;
6587
6824
  const trimmedTransitionComment = transitionComment.trim();
6588
6825
  let issueUpdated = false;
6826
+ const syncExecutionStatePatch = buildSyncFallbackExecutionStatePatch({
6827
+ currentStatus,
6828
+ nextStatus,
6829
+ syncContext,
6830
+ nextAssignee
6831
+ });
6589
6832
  const issuePatch = {
6590
- status: nextStatus
6833
+ status: nextStatus,
6834
+ ...syncExecutionStatePatch === null ? { executionState: null } : {}
6591
6835
  };
6592
6836
  if (nextAssignee) {
6593
6837
  if (nextAssignee.kind === "agent") {
@@ -6597,6 +6841,9 @@ async function updatePaperclipIssueState(ctx, params) {
6597
6841
  issuePatch.assigneeAgentId = null;
6598
6842
  issuePatch.assigneeUserId = nextAssignee.id;
6599
6843
  }
6844
+ } else if (clearAssignee) {
6845
+ issuePatch.assigneeAgentId = null;
6846
+ issuePatch.assigneeUserId = null;
6600
6847
  }
6601
6848
  if (paperclipApiBaseUrl) {
6602
6849
  try {
@@ -6642,16 +6889,10 @@ async function updatePaperclipIssueState(ctx, params) {
6642
6889
  }
6643
6890
  }
6644
6891
  if (!issueUpdated) {
6645
- const fallbackExecutionStatePatch = buildSyncFallbackExecutionStatePatch({
6646
- currentStatus,
6647
- nextStatus,
6648
- syncContext,
6649
- nextAssignee
6650
- });
6651
6892
  const preserveExistingUserAssigneeWithoutLocalApi = nextAssignee?.kind === "user" && !paperclipApiBaseUrl;
6652
6893
  const sdkIssuePatch = {
6653
6894
  ...issuePatch,
6654
- ...fallbackExecutionStatePatch !== void 0 ? { executionState: fallbackExecutionStatePatch } : {}
6895
+ ...syncExecutionStatePatch !== void 0 ? { executionState: syncExecutionStatePatch } : {}
6655
6896
  };
6656
6897
  if (preserveExistingUserAssigneeWithoutLocalApi) {
6657
6898
  delete sdkIssuePatch.assigneeAgentId;
@@ -7034,7 +7275,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7034
7275
  stateReason: snapshot.stateReason,
7035
7276
  commentsCount: snapshot.commentCount
7036
7277
  },
7037
- snapshotLinkedPullRequestNumbers
7278
+ snapshot.linkedPullRequests
7038
7279
  );
7039
7280
  const previousCommentCount = importedIssue.lastSeenCommentCount;
7040
7281
  const hasNewComments = snapshot.commentCount > (previousCommentCount ?? snapshot.commentCount);
@@ -7073,12 +7314,30 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7073
7314
  syncContext: paperclipIssueSyncContext,
7074
7315
  advancedSettings
7075
7316
  });
7317
+ const shouldClearTransitionAssignee = nextStatus === "in_review" && nextTransitionAssignee === null && paperclipIssueSyncContext.assignee !== null;
7076
7318
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
7077
7319
  const shouldWakeImportedAssignee = wasImportedThisRun && paperclipIssue.status === nextStatus && nextStatus === "todo" && paperclipIssueSyncContext.assignee?.kind === "agent";
7078
7320
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
7079
7321
  importedIssue.githubIssueNumber = githubIssue.number;
7080
7322
  importedIssue.lastSeenCommentCount = snapshot.commentCount;
7081
7323
  if (paperclipIssue.status === nextStatus) {
7324
+ if (shouldClearTransitionAssignee) {
7325
+ updateSyncFailureContext(syncFailureContext, {
7326
+ phase: "updating_paperclip_status",
7327
+ repositoryUrl: repository.url,
7328
+ githubIssueNumber: githubIssue.number
7329
+ });
7330
+ await updatePaperclipIssueState(ctx, {
7331
+ companyId: mapping.companyId,
7332
+ issueId: importedIssue.paperclipIssueId,
7333
+ currentStatus: paperclipIssue.status,
7334
+ syncContext: paperclipIssueSyncContext,
7335
+ nextStatus,
7336
+ clearAssignee: true,
7337
+ transitionComment: "",
7338
+ paperclipApiBaseUrl
7339
+ });
7340
+ }
7082
7341
  if (shouldWakeImportedAssignee) {
7083
7342
  queuedIssueWakeups.push({
7084
7343
  assigneeAgentId: paperclipIssueSyncContext.assignee?.kind === "agent" ? paperclipIssueSyncContext.assignee.id : null,
@@ -7110,6 +7369,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
7110
7369
  syncContext: paperclipIssueSyncContext,
7111
7370
  nextStatus,
7112
7371
  ...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
7372
+ ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
7113
7373
  transitionComment: transitionComment.body,
7114
7374
  transitionCommentAnnotation: transitionComment.annotation,
7115
7375
  paperclipApiBaseUrl
@@ -7672,23 +7932,73 @@ async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
7672
7932
  if (!link) {
7673
7933
  throw new Error("This Paperclip issue is not linked to GitHub yet.");
7674
7934
  }
7675
- const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
7676
- input.repository,
7677
- link.repositoryUrl,
7678
- "repository must match the GitHub repository linked to the provided Paperclip issue."
7679
- );
7680
7935
  const explicitPullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
7936
+ const linkedPullRequests = link.linkedPullRequests.length > 0 ? link.linkedPullRequests : normalizeLinkedPullRequestReferences(link.linkedPullRequestNumbers, link.repositoryUrl);
7681
7937
  if (explicitPullRequestNumber !== void 0) {
7938
+ const explicitRepository = normalizeOptionalToolString(input.repository);
7939
+ const matchingLinkedPullRequests = linkedPullRequests.filter(
7940
+ (pullRequest) => pullRequest.number === explicitPullRequestNumber
7941
+ );
7942
+ if (explicitRepository) {
7943
+ const requestedRepository = requireRepositoryReference(explicitRepository);
7944
+ if (matchingLinkedPullRequests.length > 0) {
7945
+ const matchingLinkedPullRequest = matchingLinkedPullRequests.find(
7946
+ (pullRequest) => areRepositoriesEqual(requestedRepository, requireRepositoryReference(pullRequest.repositoryUrl))
7947
+ );
7948
+ if (!matchingLinkedPullRequest) {
7949
+ const linkedIssueRepository = requireRepositoryReference(link.repositoryUrl);
7950
+ const allMatchingPullRequestsUseIssueRepository = matchingLinkedPullRequests.every(
7951
+ (pullRequest) => areRepositoriesEqual(linkedIssueRepository, requireRepositoryReference(pullRequest.repositoryUrl))
7952
+ );
7953
+ throw new Error(
7954
+ allMatchingPullRequestsUseIssueRepository ? "repository must match the GitHub repository linked to the provided Paperclip issue." : "repository must match the GitHub repository for the selected linked pull request."
7955
+ );
7956
+ }
7957
+ return {
7958
+ repository: requestedRepository,
7959
+ pullRequestNumber: explicitPullRequestNumber,
7960
+ paperclipIssueId
7961
+ };
7962
+ }
7963
+ const repository2 = assertExplicitRepositoryMatchesLinkedRepository(
7964
+ input.repository,
7965
+ link.repositoryUrl,
7966
+ "repository must match the GitHub repository linked to the provided Paperclip issue."
7967
+ );
7968
+ return {
7969
+ repository: repository2,
7970
+ pullRequestNumber: explicitPullRequestNumber,
7971
+ paperclipIssueId
7972
+ };
7973
+ }
7974
+ if (matchingLinkedPullRequests.length === 1) {
7975
+ return {
7976
+ repository: requireRepositoryReference(matchingLinkedPullRequests[0].repositoryUrl),
7977
+ pullRequestNumber: explicitPullRequestNumber,
7978
+ paperclipIssueId
7979
+ };
7980
+ }
7981
+ if (matchingLinkedPullRequests.length > 1) {
7982
+ throw new Error("repository is required because the linked Paperclip issue has matching pull request numbers in multiple repositories.");
7983
+ }
7682
7984
  return {
7683
- repository: repository2,
7985
+ repository: requireRepositoryReference(link.repositoryUrl),
7684
7986
  pullRequestNumber: explicitPullRequestNumber,
7685
7987
  paperclipIssueId
7686
7988
  };
7687
7989
  }
7688
- if (link.linkedPullRequestNumbers.length === 1) {
7990
+ if (linkedPullRequests.length === 1) {
7991
+ const inferredPullRequest = linkedPullRequests[0];
7992
+ const explicitRepository = normalizeOptionalToolString(input.repository);
7993
+ if (explicitRepository) {
7994
+ const requestedRepository = requireRepositoryReference(explicitRepository);
7995
+ if (!areRepositoriesEqual(requestedRepository, requireRepositoryReference(inferredPullRequest.repositoryUrl))) {
7996
+ throw new Error("repository must match the GitHub repository for the selected linked pull request.");
7997
+ }
7998
+ }
7689
7999
  return {
7690
- repository: repository2,
7691
- pullRequestNumber: link.linkedPullRequestNumbers[0],
8000
+ repository: requireRepositoryReference(inferredPullRequest.repositoryUrl),
8001
+ pullRequestNumber: inferredPullRequest.number,
7692
8002
  paperclipIssueId
7693
8003
  };
7694
8004
  }
@@ -8437,7 +8747,7 @@ function getProjectPullRequestStatus(state) {
8437
8747
  return "open";
8438
8748
  }
8439
8749
  }
8440
- async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache) {
8750
+ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, issueLookup, pullRequestStatusCache, defaultBranchName) {
8441
8751
  if (!node || typeof node.number !== "number" || !node.url || !node.title?.trim()) {
8442
8752
  return null;
8443
8753
  }
@@ -8446,6 +8756,8 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8446
8756
  const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
8447
8757
  statusCheckRollup: node.statusCheckRollup
8448
8758
  });
8759
+ const inlineMergeability = normalizeGitHubPullRequestMergeability(node.mergeable);
8760
+ const inlineMergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus);
8449
8761
  const [reviewThreadSummary, reviewSummary, statusSnapshot, behindBy] = await Promise.all([
8450
8762
  getOrLoadCachedGitHubPullRequestReviewThreadSummary(
8451
8763
  octokit,
@@ -8461,7 +8773,9 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8461
8773
  ),
8462
8774
  getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
8463
8775
  reviewThreadSummary: inlineReviewThreadSummary,
8464
- ciState: inlineCiState
8776
+ ciState: inlineCiState,
8777
+ mergeability: inlineMergeability,
8778
+ mergeStateStatus: inlineMergeStateStatus
8465
8779
  }),
8466
8780
  getGitHubPullRequestBehindCount(octokit, repository, {
8467
8781
  baseBranch: node.baseRefName,
@@ -8486,8 +8800,11 @@ async function buildProjectPullRequestSummaryRecord(octokit, repository, node, i
8486
8800
  const mergeable = resolveProjectPullRequestMergeable({
8487
8801
  checksStatus,
8488
8802
  reviewApprovals: reviewSummary.approvals,
8803
+ reviewChangesRequested: reviewSummary.changesRequested,
8489
8804
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
8490
- githubMergeable
8805
+ githubMergeable,
8806
+ baseBranch: node.baseRefName,
8807
+ defaultBranchName
8491
8808
  });
8492
8809
  const upToDateStatus = resolveProjectPullRequestUpToDateStatus({
8493
8810
  mergeStateStatus: node.mergeStateStatus,
@@ -8562,7 +8879,8 @@ async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options
8562
8879
  scope.repository,
8563
8880
  node,
8564
8881
  issueLookup,
8565
- pullRequestStatusCache
8882
+ pullRequestStatusCache,
8883
+ defaultBranchName
8566
8884
  )
8567
8885
  );
8568
8886
  pullRequests.push(...pageRecords.filter((record) => Boolean(record)));
@@ -8581,7 +8899,7 @@ async function listProjectPullRequestSummaryRecords(ctx, octokit, scope, options
8581
8899
  ...nextCursor ? { nextCursor } : {}
8582
8900
  };
8583
8901
  }
8584
- async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache) {
8902
+ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pullRequestStatusCache, defaultBranchName) {
8585
8903
  if (!node || typeof node.number !== "number") {
8586
8904
  return {
8587
8905
  pullRequestNumber: null,
@@ -8595,6 +8913,8 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8595
8913
  const inlineCiState = tryBuildGitHubPullRequestCiStateFromBatchNode({
8596
8914
  statusCheckRollup: node.statusCheckRollup
8597
8915
  });
8916
+ const inlineMergeability = normalizeGitHubPullRequestMergeability(node.mergeable);
8917
+ const inlineMergeStateStatus = normalizeGitHubPullRequestMergeStateStatus(node.mergeStateStatus);
8598
8918
  const [reviewThreadSummary, reviewSummary, statusSnapshot] = await Promise.all([
8599
8919
  getOrLoadCachedGitHubPullRequestReviewThreadSummary(
8600
8920
  octokit,
@@ -8610,7 +8930,9 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8610
8930
  ),
8611
8931
  getGitHubPullRequestStatusSnapshot(octokit, repository, node.number, pullRequestStatusCache, {
8612
8932
  reviewThreadSummary: inlineReviewThreadSummary,
8613
- ciState: inlineCiState
8933
+ ciState: inlineCiState,
8934
+ mergeability: inlineMergeability,
8935
+ mergeStateStatus: inlineMergeStateStatus
8614
8936
  })
8615
8937
  ]);
8616
8938
  const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
@@ -8623,8 +8945,11 @@ async function buildProjectPullRequestMetricCounts(octokit, repository, node, pu
8623
8945
  const mergeable = resolveProjectPullRequestMergeable({
8624
8946
  checksStatus,
8625
8947
  reviewApprovals: reviewSummary.approvals,
8948
+ reviewChangesRequested: reviewSummary.changesRequested,
8626
8949
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
8627
- githubMergeable
8950
+ githubMergeable,
8951
+ baseBranch: node.baseRefName,
8952
+ defaultBranchName
8628
8953
  });
8629
8954
  return {
8630
8955
  pullRequestNumber: Math.floor(node.number),
@@ -8674,7 +8999,13 @@ async function listProjectPullRequestMetrics(octokit, scope) {
8674
8999
  const pageMetrics = await mapWithConcurrency(
8675
9000
  pageNodes,
8676
9001
  PROJECT_PULL_REQUEST_SUMMARY_CONCURRENCY,
8677
- async (node) => buildProjectPullRequestMetricCounts(octokit, scope.repository, node, pullRequestStatusCache)
9002
+ async (node) => buildProjectPullRequestMetricCounts(
9003
+ octokit,
9004
+ scope.repository,
9005
+ node,
9006
+ pullRequestStatusCache,
9007
+ defaultBranchName
9008
+ )
8678
9009
  );
8679
9010
  for (const pageMetric of pageMetrics) {
8680
9011
  mergeablePullRequests += pageMetric.mergeablePullRequests;
@@ -9410,6 +9741,20 @@ function getPullRequestApiState(value) {
9410
9741
  }
9411
9742
  return value.state === "closed" ? "closed" : "open";
9412
9743
  }
9744
+ async function getGitHubRepositoryDefaultBranchName(octokit, repository) {
9745
+ try {
9746
+ const response = await octokit.rest.repos.get({
9747
+ owner: repository.owner,
9748
+ repo: repository.repo,
9749
+ headers: {
9750
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
9751
+ }
9752
+ });
9753
+ return normalizeOptionalString2(response.data.default_branch);
9754
+ } catch {
9755
+ return void 0;
9756
+ }
9757
+ }
9413
9758
  async function buildProjectPullRequestDetailData(ctx, input) {
9414
9759
  const pullRequestNumber = normalizeToolPositiveInteger(input.pullRequestNumber);
9415
9760
  if (!pullRequestNumber) {
@@ -9425,6 +9770,15 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9425
9770
  activeProjectPullRequestSummaryRecordCache,
9426
9771
  buildProjectPullRequestSummaryRecordCacheKey(scope, pullRequestNumber)
9427
9772
  );
9773
+ const cachedSummary = getFreshCacheValue(
9774
+ activeProjectPullRequestSummaryCache,
9775
+ buildProjectPullRequestSummaryCacheKey(scope)
9776
+ );
9777
+ const cachedMetrics = getFreshCacheValue(
9778
+ activeProjectPullRequestMetricsCache,
9779
+ buildProjectPullRequestMetricsCacheKey(scope)
9780
+ );
9781
+ const cachedDefaultBranchName = normalizeOptionalString2(cachedSummary?.defaultBranchName) ?? normalizeOptionalString2(cachedMetrics?.defaultBranchName);
9428
9782
  const cachedLinkedIssue = cachedSummaryRecord ? getLinkedPaperclipIssueFromProjectPullRequestRecord(cachedSummaryRecord) : void 0;
9429
9783
  const octokit = await createGitHubToolOctokit(ctx, scope.companyId);
9430
9784
  const response = await octokit.rest.pulls.get({
@@ -9443,7 +9797,8 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9443
9797
  reviewThreadSummary: reviewThreadSummary2
9444
9798
  })
9445
9799
  );
9446
- const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot] = await Promise.all([
9800
+ const defaultBranchNamePromise = cachedDefaultBranchName ? Promise.resolve(cachedDefaultBranchName) : getGitHubRepositoryDefaultBranchName(octokit, scope.repository);
9801
+ const [reviewSummary, reviewThreadSummary, comments, linkedIssue, statusSnapshot, defaultBranchName] = await Promise.all([
9447
9802
  reviewSummaryPromise,
9448
9803
  reviewThreadSummaryPromise,
9449
9804
  listAllGitHubIssueComments(octokit, scope.repository, pullRequestNumber),
@@ -9455,7 +9810,8 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9455
9810
  issueLookup
9456
9811
  );
9457
9812
  })(),
9458
- statusSnapshotPromise
9813
+ statusSnapshotPromise,
9814
+ defaultBranchNamePromise
9459
9815
  ]);
9460
9816
  const author = buildProjectPullRequestPerson({
9461
9817
  login: pullRequest.user?.login,
@@ -9499,8 +9855,11 @@ async function buildProjectPullRequestDetailData(ctx, input) {
9499
9855
  const mergeable = resolveProjectPullRequestMergeable({
9500
9856
  checksStatus,
9501
9857
  reviewApprovals: reviewSummary.approvals,
9858
+ reviewChangesRequested: reviewSummary.changesRequested,
9502
9859
  unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
9503
- githubMergeable
9860
+ githubMergeable,
9861
+ baseBranch: pullRequest.base.ref,
9862
+ defaultBranchName
9504
9863
  });
9505
9864
  return setCacheValue(
9506
9865
  activeProjectPullRequestDetailCache,
@@ -9695,6 +10054,49 @@ async function mergeProjectPullRequest(ctx, input) {
9695
10054
  }
9696
10055
  const scope = await requireProjectPullRequestScope(ctx, input);
9697
10056
  const octokit = await createGitHubToolOctokit(ctx, scope.companyId);
10057
+ const pullRequestResponse = await octokit.rest.pulls.get({
10058
+ owner: scope.repository.owner,
10059
+ repo: scope.repository.repo,
10060
+ pull_number: pullRequestNumber,
10061
+ headers: {
10062
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
10063
+ }
10064
+ });
10065
+ const pullRequest = pullRequestResponse.data;
10066
+ const reviewSummaryPromise = getOrLoadCachedGitHubPullRequestReviewSummary(octokit, scope.repository, pullRequestNumber);
10067
+ const reviewThreadSummaryPromise = getOrLoadCachedGitHubPullRequestReviewThreadSummary(octokit, scope.repository, pullRequestNumber);
10068
+ const statusSnapshotPromise = reviewThreadSummaryPromise.then(
10069
+ (reviewThreadSummary2) => getGitHubPullRequestStatusSnapshot(octokit, scope.repository, pullRequestNumber, /* @__PURE__ */ new Map(), {
10070
+ reviewThreadSummary: reviewThreadSummary2
10071
+ })
10072
+ );
10073
+ const defaultBranchNamePromise = getGitHubRepositoryDefaultBranchName(octokit, scope.repository);
10074
+ const [reviewSummary, reviewThreadSummary, statusSnapshot, defaultBranchName] = await Promise.all([
10075
+ reviewSummaryPromise,
10076
+ reviewThreadSummaryPromise,
10077
+ statusSnapshotPromise,
10078
+ defaultBranchNamePromise
10079
+ ]);
10080
+ if (!defaultBranchName) {
10081
+ throw new Error(
10082
+ "Could not determine the repository default branch before merging this pull request. Retry the merge, and if it keeps failing check GitHub token permissions and connectivity."
10083
+ );
10084
+ }
10085
+ const checksStatus = statusSnapshot.ciState === "green" ? "passed" : statusSnapshot.ciState === "red" ? "failed" : "pending";
10086
+ const mergeable = resolveProjectPullRequestMergeable({
10087
+ checksStatus,
10088
+ reviewApprovals: reviewSummary.approvals,
10089
+ reviewChangesRequested: reviewSummary.changesRequested,
10090
+ unresolvedReviewThreads: reviewThreadSummary.unresolvedReviewThreads,
10091
+ githubMergeable: pullRequest.mergeable === true,
10092
+ baseBranch: pullRequest.base.ref,
10093
+ defaultBranchName
10094
+ });
10095
+ if (!mergeable) {
10096
+ throw new Error(
10097
+ "This pull request is not mergeable yet. It must target the current default branch, have passing checks, at least one approval, no outstanding changes requests, and no unresolved review threads."
10098
+ );
10099
+ }
9698
10100
  const response = await octokit.rest.pulls.merge({
9699
10101
  owner: scope.repository.owner,
9700
10102
  repo: scope.repository.repo,
@@ -10472,20 +10874,28 @@ async function performSync(ctx, trigger, options = {}) {
10472
10874
  for (const [issueNumber, linkedPullRequests] of warmedLinkedPullRequests.entries()) {
10473
10875
  linkedPullRequestsByIssueNumber.set(issueNumber, linkedPullRequests);
10474
10876
  }
10475
- const openLinkedPullRequestNumbers = /* @__PURE__ */ new Set();
10877
+ const openLinkedPullRequestNumbersByRepository = /* @__PURE__ */ new Map();
10476
10878
  for (const linkedPullRequests of warmedLinkedPullRequests.values()) {
10477
10879
  for (const pullRequest of linkedPullRequests) {
10478
10880
  if (pullRequest.state === "OPEN") {
10479
- openLinkedPullRequestNumbers.add(pullRequest.number);
10881
+ const pullRequestRepository = requireRepositoryReference(pullRequest.repositoryUrl);
10882
+ const entry = openLinkedPullRequestNumbersByRepository.get(pullRequestRepository.url) ?? {
10883
+ repository: pullRequestRepository,
10884
+ numbers: /* @__PURE__ */ new Set()
10885
+ };
10886
+ entry.numbers.add(pullRequest.number);
10887
+ openLinkedPullRequestNumbersByRepository.set(pullRequestRepository.url, entry);
10480
10888
  }
10481
10889
  }
10482
10890
  }
10483
- await warmGitHubPullRequestStatusCache(
10484
- octokit,
10485
- repository,
10486
- openLinkedPullRequestNumbers,
10487
- pullRequestStatusCache
10488
- );
10891
+ for (const entry of openLinkedPullRequestNumbersByRepository.values()) {
10892
+ await warmGitHubPullRequestStatusCache(
10893
+ octokit,
10894
+ entry.repository,
10895
+ entry.numbers,
10896
+ pullRequestStatusCache
10897
+ );
10898
+ }
10489
10899
  await throwIfSyncCancelled();
10490
10900
  } catch (error) {
10491
10901
  if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
@@ -11274,6 +11684,7 @@ function registerGitHubAgentTools(ctx) {
11274
11684
  ].filter((warning) => warning !== null);
11275
11685
  const snapshot = snapshotResult.status === "fulfilled" ? snapshotResult.value : {
11276
11686
  number: target.pullRequestNumber,
11687
+ repositoryUrl: target.repository.url,
11277
11688
  hasUnresolvedReviewThreads: false,
11278
11689
  ciState: "unfinished"
11279
11690
  };