paperclip-github-plugin 0.8.2 → 0.8.4

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
@@ -613,6 +613,7 @@ function requiresPaperclipBoardAccess(value) {
613
613
  }
614
614
 
615
615
  // src/worker.ts
616
+ var GITHUB_SYNC_BASE_ORIGIN_KIND = `plugin:${GITHUB_SYNC_PLUGIN_ID}`;
616
617
  var GITHUB_ISSUE_ORIGIN_KIND = `plugin:${GITHUB_SYNC_PLUGIN_ID}:github-issue`;
617
618
  var GITHUB_PULL_REQUEST_ORIGIN_KIND = `plugin:${GITHUB_SYNC_PLUGIN_ID}:github-pull-request`;
618
619
  var SETTINGS_SCOPE = {
@@ -5020,6 +5021,29 @@ function getPaperclipIssueSyncContext(issue) {
5020
5021
  executionState: normalizePaperclipIssueExecutionState(record.executionState)
5021
5022
  };
5022
5023
  }
5024
+ function hasUnresolvedPaperclipIssueBlockerSummary(blockers) {
5025
+ if (!Array.isArray(blockers)) {
5026
+ return false;
5027
+ }
5028
+ return blockers.some((blocker) => {
5029
+ if (!blocker || typeof blocker !== "object") {
5030
+ return true;
5031
+ }
5032
+ const status = blocker.status;
5033
+ return status !== "done" && status !== "cancelled";
5034
+ });
5035
+ }
5036
+ async function hasUnresolvedPaperclipIssueBlocker(ctx, issue, companyId) {
5037
+ const record = issue;
5038
+ if (hasUnresolvedPaperclipIssueBlockerSummary(record.blockedBy)) {
5039
+ return true;
5040
+ }
5041
+ if (typeof ctx.issues.relations?.get !== "function") {
5042
+ return false;
5043
+ }
5044
+ const relations = await ctx.issues.relations.get(issue.id, companyId);
5045
+ return hasUnresolvedPaperclipIssueBlockerSummary(relations.blockedBy);
5046
+ }
5023
5047
  function isSamePaperclipIssueAssigneePrincipal(left, right) {
5024
5048
  if (!left || !right) {
5025
5049
  return !left && !right;
@@ -5201,6 +5225,9 @@ function isHealthyMaintainerWaitTransition(params) {
5201
5225
  const { currentStatus, nextStatus, syncContext } = params;
5202
5226
  return nextStatus === "in_review" && (currentStatus === "done" || currentStatus === "in_review") && syncContext.executionState === null && syncContext.executionPolicy !== null;
5203
5227
  }
5228
+ function shouldPreserveImportedTriageAssignee(params) {
5229
+ return params.wasImportedThisRun && params.maintainerAuthoredImportedIssue === true && params.currentStatus === "backlog" && params.nextStatus === "todo";
5230
+ }
5204
5231
  function doesPaperclipIssueAssigneeMatch(currentAssignee, nextAssignee) {
5205
5232
  return isSamePaperclipIssueAssigneePrincipal(currentAssignee, nextAssignee);
5206
5233
  }
@@ -5552,10 +5579,10 @@ function normalizeGitHubPullRequestReviewDecision(value) {
5552
5579
  }
5553
5580
  }
5554
5581
  function isGitHubPullRequestActionRequiredForSync(pullRequest) {
5555
- return pullRequest.reviewDecision === "changes_requested" || pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
5582
+ return pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
5556
5583
  }
5557
5584
  function isGitHubPullRequestReviewReadyForSync(pullRequest) {
5558
- if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads || pullRequest.reviewDecision === "changes_requested") {
5585
+ if (pullRequest.ciState !== "green" || pullRequest.hasUnresolvedReviewThreads) {
5559
5586
  return false;
5560
5587
  }
5561
5588
  return REVIEW_READY_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
@@ -5588,9 +5615,6 @@ function listGitHubPullRequestSyncBlockingConditions(pullRequest) {
5588
5615
  if (pullRequest.hasUnresolvedReviewThreads) {
5589
5616
  conditions.push("unresolved review threads");
5590
5617
  }
5591
- if (pullRequest.reviewDecision === "changes_requested") {
5592
- conditions.push("requested changes");
5593
- }
5594
5618
  return conditions;
5595
5619
  }
5596
5620
  function tryBuildGitHubPullRequestStatusSnapshotFromBatchNode(node, repository) {
@@ -6365,6 +6389,18 @@ function extractImportedGitHubIssueUrlFromDescription(description) {
6365
6389
  }
6366
6390
  return normalizeGitHubIssueHtmlUrl(legacyMatch[1]);
6367
6391
  }
6392
+ function removeGitHubIssueLinkMetadataFromDescription(description) {
6393
+ if (typeof description !== "string") {
6394
+ return void 0;
6395
+ }
6396
+ let next = description;
6397
+ const hiddenMarkerPattern = getHiddenGitHubImportMarkerPattern();
6398
+ while (hiddenMarkerPattern.test(next)) {
6399
+ next = next.replace(hiddenMarkerPattern, "");
6400
+ }
6401
+ next = next.replace(/^\*\s+GitHub issue:\s+\[[^\]]+\]\([^)]+\)\s*$/gm, "").replace(/^Imported from https:\/\/github\.com\/[^\s)]+\/[^\s)]+\/issues\/\d+\.?\s*$/gim, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
6402
+ return next === description ? void 0 : next;
6403
+ }
6368
6404
  function compareImportedPaperclipIssueCreatedAt(left, right) {
6369
6405
  const leftTime = Date.parse(String(left.createdAt ?? ""));
6370
6406
  const rightTime = Date.parse(String(right.createdAt ?? ""));
@@ -6693,6 +6729,7 @@ async function listGitHubIssueLinkRecords(ctx, query = {}) {
6693
6729
  }
6694
6730
  records.push({
6695
6731
  paperclipIssueId: entry.scopeId,
6732
+ ...typeof entry.externalId === "string" && entry.externalId.trim() ? { externalId: entry.externalId.trim() } : {},
6696
6733
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
6697
6734
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
6698
6735
  ...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
@@ -6739,6 +6776,7 @@ async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
6739
6776
  }
6740
6777
  records.push({
6741
6778
  paperclipIssueId: entry.scopeId,
6779
+ ...typeof entry.externalId === "string" && entry.externalId.trim() ? { externalId: entry.externalId.trim() } : {},
6742
6780
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
6743
6781
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
6744
6782
  ...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
@@ -7147,6 +7185,134 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
7147
7185
  githubPullRequestState: pullRequestState
7148
7186
  };
7149
7187
  }
7188
+ function invalidateGitHubLinkCachesForTarget(params) {
7189
+ if (!params.companyId || !params.projectId || !params.repositoryUrl) {
7190
+ return;
7191
+ }
7192
+ const repository = parseRepositoryReference(params.repositoryUrl);
7193
+ if (!repository) {
7194
+ return;
7195
+ }
7196
+ invalidateProjectPullRequestCaches({
7197
+ companyId: params.companyId,
7198
+ projectId: params.projectId,
7199
+ repository
7200
+ });
7201
+ }
7202
+ async function tombstoneGitHubIssueLinkRecord(ctx, record, unlinkedAt) {
7203
+ await ctx.entities.upsert({
7204
+ entityType: ISSUE_LINK_ENTITY_TYPE,
7205
+ scopeKind: "issue",
7206
+ scopeId: record.paperclipIssueId,
7207
+ externalId: record.externalId ?? record.data.githubIssueUrl,
7208
+ title: record.title ?? `GitHub issue #${record.data.githubIssueNumber}`,
7209
+ status: "unlinked",
7210
+ data: {
7211
+ kind: "issue",
7212
+ paperclipIssueId: record.paperclipIssueId,
7213
+ repositoryUrl: record.data.repositoryUrl,
7214
+ githubIssueNumber: record.data.githubIssueNumber,
7215
+ githubIssueUrl: record.data.githubIssueUrl,
7216
+ ...record.data.companyId ? { companyId: record.data.companyId } : {},
7217
+ ...record.data.paperclipProjectId ? { paperclipProjectId: record.data.paperclipProjectId } : {},
7218
+ unlinkedAt
7219
+ }
7220
+ });
7221
+ }
7222
+ async function tombstoneGitHubPullRequestLinkRecord(ctx, record, unlinkedAt) {
7223
+ await ctx.entities.upsert({
7224
+ entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
7225
+ scopeKind: "issue",
7226
+ scopeId: record.paperclipIssueId,
7227
+ externalId: record.externalId ?? record.data.githubPullRequestUrl,
7228
+ title: record.title ?? `GitHub pull request #${record.data.githubPullRequestNumber}`,
7229
+ status: "unlinked",
7230
+ data: {
7231
+ kind: "pull_request",
7232
+ paperclipIssueId: record.paperclipIssueId,
7233
+ repositoryUrl: record.data.repositoryUrl,
7234
+ githubPullRequestNumber: record.data.githubPullRequestNumber,
7235
+ githubPullRequestUrl: record.data.githubPullRequestUrl,
7236
+ ...record.data.companyId ? { companyId: record.data.companyId } : {},
7237
+ ...record.data.paperclipProjectId ? { paperclipProjectId: record.data.paperclipProjectId } : {},
7238
+ unlinkedAt
7239
+ }
7240
+ });
7241
+ }
7242
+ async function unlinkPaperclipIssueFromGitHub(ctx, params) {
7243
+ const companyId = normalizeCompanyId(params.companyId);
7244
+ const issueId = normalizeOptionalString2(params.issueId);
7245
+ if (!companyId || !issueId) {
7246
+ throw new Error("companyId and issueId are required.");
7247
+ }
7248
+ const issue = await ctx.issues.get(issueId, companyId);
7249
+ if (!issue) {
7250
+ throw new Error("Paperclip issue was not found.");
7251
+ }
7252
+ const issueLinkRecords = (await listGitHubIssueLinkRecords(ctx, {
7253
+ paperclipIssueId: issueId
7254
+ })).filter((record) => !record.data.companyId || record.data.companyId === companyId);
7255
+ const pullRequestLinkRecords = (await listGitHubPullRequestLinkRecords(ctx, {
7256
+ paperclipIssueId: issueId
7257
+ })).filter((record) => !record.data.companyId || record.data.companyId === companyId);
7258
+ const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
7259
+ const removedImportRegistryEntries = importRegistry.filter(
7260
+ (entry) => entry.paperclipIssueId === issueId && (!entry.companyId || entry.companyId === companyId)
7261
+ );
7262
+ const nextImportRegistry = importRegistry.filter(
7263
+ (entry) => !(entry.paperclipIssueId === issueId && (!entry.companyId || entry.companyId === companyId))
7264
+ );
7265
+ const unlinkedAt = (/* @__PURE__ */ new Date()).toISOString();
7266
+ for (const record of issueLinkRecords) {
7267
+ await tombstoneGitHubIssueLinkRecord(ctx, record, unlinkedAt);
7268
+ invalidateGitHubLinkCachesForTarget({
7269
+ companyId: record.data.companyId ?? companyId,
7270
+ projectId: record.data.paperclipProjectId ?? issue.projectId ?? void 0,
7271
+ repositoryUrl: record.data.repositoryUrl
7272
+ });
7273
+ }
7274
+ for (const record of pullRequestLinkRecords) {
7275
+ await tombstoneGitHubPullRequestLinkRecord(ctx, record, unlinkedAt);
7276
+ invalidateGitHubLinkCachesForTarget({
7277
+ companyId: record.data.companyId ?? companyId,
7278
+ projectId: record.data.paperclipProjectId ?? issue.projectId ?? void 0,
7279
+ repositoryUrl: record.data.repositoryUrl
7280
+ });
7281
+ }
7282
+ for (const entry of removedImportRegistryEntries) {
7283
+ invalidateGitHubLinkCachesForTarget({
7284
+ companyId: entry.companyId ?? companyId,
7285
+ projectId: entry.paperclipProjectId ?? issue.projectId ?? void 0,
7286
+ repositoryUrl: entry.repositoryUrl
7287
+ });
7288
+ }
7289
+ if (removedImportRegistryEntries.length > 0) {
7290
+ await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextImportRegistry);
7291
+ }
7292
+ const shouldClearGitHubOrigin = issue.originKind === GITHUB_ISSUE_ORIGIN_KIND || issue.originKind === GITHUB_PULL_REQUEST_ORIGIN_KIND;
7293
+ const nextDescription = removeGitHubIssueLinkMetadataFromDescription(issue.description);
7294
+ const issuePatch = {};
7295
+ if (shouldClearGitHubOrigin) {
7296
+ issuePatch.originKind = GITHUB_SYNC_BASE_ORIGIN_KIND;
7297
+ issuePatch.originId = null;
7298
+ issuePatch.originRunId = null;
7299
+ }
7300
+ if (nextDescription !== void 0) {
7301
+ issuePatch.description = nextDescription;
7302
+ }
7303
+ if (Object.keys(issuePatch).length > 0) {
7304
+ await ctx.issues.update(issueId, issuePatch, companyId);
7305
+ }
7306
+ return {
7307
+ paperclipIssueId: issueId,
7308
+ unlinked: issueLinkRecords.length > 0 || pullRequestLinkRecords.length > 0 || removedImportRegistryEntries.length > 0 || shouldClearGitHubOrigin || nextDescription !== void 0,
7309
+ unlinkedIssueLinksCount: issueLinkRecords.length,
7310
+ unlinkedPullRequestLinksCount: pullRequestLinkRecords.length,
7311
+ removedImportRegistryEntriesCount: removedImportRegistryEntries.length,
7312
+ clearedIssueOrigin: shouldClearGitHubOrigin,
7313
+ removedDescriptionMetadata: nextDescription !== void 0
7314
+ };
7315
+ }
7150
7316
  async function upsertStatusTransitionCommentAnnotation(ctx, params) {
7151
7317
  const { issueId, commentId, annotation } = params;
7152
7318
  await ctx.entities.upsert({
@@ -8450,6 +8616,261 @@ async function ensurePaperclipIssueImported(ctx, mapping, advancedSettings, issu
8450
8616
  }
8451
8617
  return createdIssue.id;
8452
8618
  }
8619
+ function getGitHubIssueRepositoryReference(issue) {
8620
+ return parseGitHubIssueHtmlUrl(issue.htmlUrl);
8621
+ }
8622
+ function findTransferredIssueTargetMapping(mappings, params) {
8623
+ const companyId = normalizeCompanyId(params.companyId);
8624
+ const repository = parseRepositoryReference(params.repositoryUrl);
8625
+ if (!companyId || !repository) {
8626
+ return void 0;
8627
+ }
8628
+ return getSyncableMappings(mappings).find((mapping) => {
8629
+ if (normalizeCompanyId(mapping.companyId) !== companyId) {
8630
+ return false;
8631
+ }
8632
+ const mappingRepository = parseRepositoryReference(mapping.repositoryUrl);
8633
+ return Boolean(mappingRepository && areRepositoriesEqual(mappingRepository, repository));
8634
+ });
8635
+ }
8636
+ async function patchPaperclipIssueForGitHubIssueTransfer(ctx, params) {
8637
+ const nextDescription = buildPaperclipIssueDescription(params.githubIssue, []);
8638
+ const nextOriginId = normalizeGitHubIssueHtmlUrl(params.githubIssue.htmlUrl) ?? params.githubIssue.htmlUrl;
8639
+ const descriptionUpdated = normalizeIssueDescriptionValue(params.currentDescription) !== nextDescription;
8640
+ const patch = {
8641
+ projectId: params.projectId,
8642
+ originKind: GITHUB_ISSUE_ORIGIN_KIND,
8643
+ originId: nextOriginId,
8644
+ ...descriptionUpdated ? { description: nextDescription } : {}
8645
+ };
8646
+ let issueUpdated = false;
8647
+ if (params.paperclipApiBaseUrl) {
8648
+ try {
8649
+ const response = await fetchPaperclipApi(
8650
+ getPaperclipIssueEndpoint(params.paperclipApiBaseUrl, params.issueId),
8651
+ {
8652
+ method: "PATCH",
8653
+ headers: {
8654
+ accept: "application/json",
8655
+ "content-type": "application/json"
8656
+ },
8657
+ body: JSON.stringify(patch)
8658
+ },
8659
+ {
8660
+ companyId: params.companyId
8661
+ }
8662
+ );
8663
+ const payloadResult = await readPaperclipApiJsonResponse(response, {
8664
+ operationLabel: "issue update",
8665
+ bodyRequired: false
8666
+ });
8667
+ issueUpdated = !payloadResult.failure;
8668
+ } catch {
8669
+ issueUpdated = false;
8670
+ }
8671
+ }
8672
+ if (!issueUpdated) {
8673
+ await ctx.issues.update(
8674
+ params.issueId,
8675
+ patch,
8676
+ params.companyId
8677
+ );
8678
+ }
8679
+ return {
8680
+ descriptionUpdated
8681
+ };
8682
+ }
8683
+ async function moveImportedIssueToTransferredGitHubMapping(ctx, params) {
8684
+ const companyId = normalizeCompanyId(params.targetMapping.companyId);
8685
+ const projectId = normalizeOptionalString2(params.targetMapping.paperclipProjectId);
8686
+ if (!companyId || !projectId) {
8687
+ return {
8688
+ updatedDescriptionsCount: 0
8689
+ };
8690
+ }
8691
+ const paperclipIssue = await ctx.issues.get(params.importedIssue.paperclipIssueId, companyId);
8692
+ if (!paperclipIssue) {
8693
+ return {
8694
+ updatedDescriptionsCount: 0
8695
+ };
8696
+ }
8697
+ const transferPatchResult = await patchPaperclipIssueForGitHubIssueTransfer(ctx, {
8698
+ companyId,
8699
+ issueId: params.importedIssue.paperclipIssueId,
8700
+ projectId,
8701
+ githubIssue: params.githubIssue,
8702
+ currentDescription: paperclipIssue.description,
8703
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8704
+ });
8705
+ const nextGitHubIssueUrl = normalizeGitHubIssueHtmlUrl(params.githubIssue.htmlUrl) ?? params.githubIssue.htmlUrl;
8706
+ const existingLinks = await listGitHubIssueLinkRecords(ctx, {
8707
+ paperclipIssueId: params.importedIssue.paperclipIssueId
8708
+ });
8709
+ const unlinkedAt = (/* @__PURE__ */ new Date()).toISOString();
8710
+ for (const link of existingLinks) {
8711
+ if ((!link.data.companyId || link.data.companyId === companyId) && link.data.githubIssueUrl !== nextGitHubIssueUrl) {
8712
+ await tombstoneGitHubIssueLinkRecord(ctx, link, unlinkedAt);
8713
+ }
8714
+ }
8715
+ await upsertGitHubIssueLinkRecord(
8716
+ ctx,
8717
+ params.targetMapping,
8718
+ params.importedIssue.paperclipIssueId,
8719
+ params.githubIssue,
8720
+ []
8721
+ );
8722
+ upsertImportedIssueRecord(
8723
+ params.nextRegistry,
8724
+ buildImportedIssueRecord(
8725
+ params.targetMapping,
8726
+ params.githubIssue,
8727
+ params.importedIssue.paperclipIssueId,
8728
+ params.importedIssue.importedAt
8729
+ )
8730
+ );
8731
+ invalidateGitHubLinkCachesForTarget({
8732
+ companyId,
8733
+ projectId: params.sourceMapping.paperclipProjectId ?? paperclipIssue.projectId ?? void 0,
8734
+ repositoryUrl: params.sourceMapping.repositoryUrl
8735
+ });
8736
+ invalidateGitHubLinkCachesForTarget({
8737
+ companyId,
8738
+ projectId,
8739
+ repositoryUrl: params.targetMapping.repositoryUrl
8740
+ });
8741
+ return {
8742
+ updatedDescriptionsCount: transferPatchResult.descriptionUpdated ? 1 : 0
8743
+ };
8744
+ }
8745
+ function buildUnmappedTransferredIssueCancellationComment(params) {
8746
+ return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because the linked GitHub issue was transferred to \`${formatRepositoryLabel(params.transferredRepository)}\`, which is not mapped to a Paperclip project. The GitHub link was removed so sync will stop updating this Paperclip issue.`;
8747
+ }
8748
+ async function cancelUnmappedTransferredGitHubIssue(ctx, params) {
8749
+ const companyId = normalizeCompanyId(params.mapping.companyId);
8750
+ if (!companyId) {
8751
+ return {
8752
+ updatedStatusesCount: 0
8753
+ };
8754
+ }
8755
+ const paperclipIssue = await ctx.issues.get(params.importedIssue.paperclipIssueId, companyId);
8756
+ if (!paperclipIssue) {
8757
+ return {
8758
+ updatedStatusesCount: 0
8759
+ };
8760
+ }
8761
+ await unlinkPaperclipIssueFromGitHub(ctx, {
8762
+ companyId,
8763
+ issueId: params.importedIssue.paperclipIssueId
8764
+ });
8765
+ const nextStatus = "cancelled";
8766
+ await updatePaperclipIssueState(ctx, {
8767
+ companyId,
8768
+ issueId: params.importedIssue.paperclipIssueId,
8769
+ currentStatus: paperclipIssue.status,
8770
+ syncContext: getPaperclipIssueSyncContext(paperclipIssue),
8771
+ nextStatus,
8772
+ transitionComment: buildUnmappedTransferredIssueCancellationComment({
8773
+ previousStatus: paperclipIssue.status,
8774
+ nextStatus,
8775
+ transferredRepository: params.transferredRepository
8776
+ }),
8777
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8778
+ });
8779
+ return {
8780
+ updatedStatusesCount: paperclipIssue.status === nextStatus ? 0 : 1
8781
+ };
8782
+ }
8783
+ async function fetchTransferredGitHubIssueForImportedRecord(octokit, repository, importedIssue) {
8784
+ if (importedIssue.githubIssueNumber === void 0) {
8785
+ return null;
8786
+ }
8787
+ const response = await octokit.rest.issues.get({
8788
+ owner: repository.owner,
8789
+ repo: repository.repo,
8790
+ issue_number: importedIssue.githubIssueNumber,
8791
+ headers: {
8792
+ accept: "application/vnd.github+json",
8793
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8794
+ }
8795
+ });
8796
+ const rawIssue = response.data;
8797
+ if (rawIssue.pull_request) {
8798
+ return null;
8799
+ }
8800
+ const githubIssue = normalizeGitHubIssueRecord(rawIssue);
8801
+ const transferredIssueReference = getGitHubIssueRepositoryReference(githubIssue);
8802
+ const transferredRepository = transferredIssueReference ? parseRepositoryReference(transferredIssueReference.repositoryUrl) : void 0;
8803
+ if (!transferredRepository || areRepositoriesEqual(repository, transferredRepository)) {
8804
+ return null;
8805
+ }
8806
+ return {
8807
+ githubIssue,
8808
+ transferredRepository
8809
+ };
8810
+ }
8811
+ async function reconcileTransferredImportedIssues(ctx, params) {
8812
+ let updatedStatusesCount = 0;
8813
+ let updatedDescriptionsCount = 0;
8814
+ for (const importedIssue of params.importedIssues) {
8815
+ if (params.allIssuesById.has(importedIssue.githubIssueId)) {
8816
+ continue;
8817
+ }
8818
+ try {
8819
+ updateSyncFailureContext(params.syncFailureContext, {
8820
+ phase: "evaluating_github_status",
8821
+ repositoryUrl: params.sourceRepository.url,
8822
+ githubIssueNumber: importedIssue.githubIssueNumber
8823
+ });
8824
+ const transfer = await fetchTransferredGitHubIssueForImportedRecord(
8825
+ params.octokit,
8826
+ params.sourceRepository,
8827
+ importedIssue
8828
+ );
8829
+ if (!transfer) {
8830
+ continue;
8831
+ }
8832
+ const targetMapping = findTransferredIssueTargetMapping(params.allMappings, {
8833
+ companyId: params.sourceMapping.companyId,
8834
+ repositoryUrl: transfer.transferredRepository.url
8835
+ });
8836
+ if (targetMapping) {
8837
+ const moveResult = await moveImportedIssueToTransferredGitHubMapping(ctx, {
8838
+ sourceMapping: params.sourceMapping,
8839
+ targetMapping,
8840
+ importedIssue,
8841
+ githubIssue: transfer.githubIssue,
8842
+ nextRegistry: params.nextRegistry,
8843
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8844
+ });
8845
+ updatedDescriptionsCount += moveResult.updatedDescriptionsCount;
8846
+ continue;
8847
+ }
8848
+ const cancelResult = await cancelUnmappedTransferredGitHubIssue(ctx, {
8849
+ mapping: params.sourceMapping,
8850
+ importedIssue,
8851
+ transferredRepository: transfer.transferredRepository,
8852
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8853
+ });
8854
+ for (let index = params.nextRegistry.length - 1; index >= 0; index -= 1) {
8855
+ const entry = params.nextRegistry[index];
8856
+ if (entry?.paperclipIssueId === importedIssue.paperclipIssueId && (!entry.companyId || entry.companyId === params.sourceMapping.companyId)) {
8857
+ params.nextRegistry.splice(index, 1);
8858
+ }
8859
+ }
8860
+ updatedStatusesCount += cancelResult.updatedStatusesCount;
8861
+ } catch (error) {
8862
+ if (isGitHubRateLimitError(error)) {
8863
+ throw error;
8864
+ }
8865
+ recordRecoverableSyncFailure(ctx, params.failures, error, params.syncFailureContext);
8866
+ continue;
8867
+ }
8868
+ }
8869
+ return {
8870
+ updatedStatusesCount,
8871
+ updatedDescriptionsCount
8872
+ };
8873
+ }
8453
8874
  async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onGitHubIssueClosed, onProgress) {
8454
8875
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
8455
8876
  return {
@@ -8624,7 +9045,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8624
9045
  paperclipIssueSyncContext,
8625
9046
  advancedSettings
8626
9047
  );
8627
- const nextStatus = resolvePaperclipIssueStatus({
9048
+ let nextStatus = resolvePaperclipIssueStatus({
8628
9049
  currentStatus: paperclipIssue.status,
8629
9050
  snapshot,
8630
9051
  hasTrustedNewComment,
@@ -8633,12 +9054,21 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8633
9054
  maintainerAuthoredImportedIssue,
8634
9055
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
8635
9056
  });
9057
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9058
+ nextStatus = "blocked";
9059
+ }
8636
9060
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
8637
9061
  currentStatus: paperclipIssue.status,
8638
9062
  nextStatus,
8639
9063
  syncContext: paperclipIssueSyncContext
8640
9064
  });
8641
- const nextTransitionAssignee = resolveSyncTransitionAssignee({
9065
+ const shouldPreserveImportedTriageRouting = shouldPreserveImportedTriageAssignee({
9066
+ currentStatus: paperclipIssue.status,
9067
+ nextStatus,
9068
+ wasImportedThisRun,
9069
+ maintainerAuthoredImportedIssue
9070
+ });
9071
+ const nextTransitionAssignee = shouldPreserveImportedTriageRouting ? null : resolveSyncTransitionAssignee({
8642
9072
  currentStatus: paperclipIssue.status,
8643
9073
  nextStatus,
8644
9074
  syncContext: paperclipIssueSyncContext,
@@ -8646,7 +9076,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8646
9076
  });
8647
9077
  const shouldClearTransitionAssignee = nextStatus === "in_review" && (nextTransitionAssignee === null || shouldPreserveMaintainerWaitRouting) && paperclipIssueSyncContext.assignee !== null;
8648
9078
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
8649
- const shouldWakeImportedAssignee = wasImportedThisRun && paperclipIssue.status === nextStatus && nextStatus === "todo" && paperclipIssueSyncContext.assignee?.kind === "agent";
9079
+ const shouldWakeImportedAssignee = wasImportedThisRun && nextStatus === "todo" && (paperclipIssue.status === nextStatus || shouldPreserveImportedTriageRouting) && paperclipIssueSyncContext.assignee?.kind === "agent";
8650
9080
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
8651
9081
  importedIssue.githubIssueNumber = githubIssue.number;
8652
9082
  importedIssue.lastSeenCommentCount = snapshot.commentCount;
@@ -8708,6 +9138,14 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8708
9138
  paperclipApiBaseUrl
8709
9139
  });
8710
9140
  updatedStatusesCount += 1;
9141
+ if (shouldWakeImportedAssignee) {
9142
+ queuedIssueWakeups.push({
9143
+ assigneeAgentId: paperclipIssueSyncContext.assignee?.kind === "agent" ? paperclipIssueSyncContext.assignee.id : null,
9144
+ paperclipIssueId: importedIssue.paperclipIssueId,
9145
+ reason: IMPORTED_ISSUE_WAKE_REASON,
9146
+ mutation: "import"
9147
+ });
9148
+ }
8711
9149
  if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
8712
9150
  queuedIssueWakeups.push({
8713
9151
  assigneeAgentId: nextTransitionAssignee.principal.id,
@@ -8821,11 +9259,14 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
8821
9259
  paperclipIssueSyncContext,
8822
9260
  advancedSettings
8823
9261
  );
8824
- const nextStatus = resolvePaperclipPullRequestIssueStatus({
9262
+ let nextStatus = resolvePaperclipPullRequestIssueStatus({
8825
9263
  currentStatus: paperclipIssue.status,
8826
9264
  pullRequest,
8827
9265
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
8828
9266
  });
9267
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9268
+ nextStatus = "blocked";
9269
+ }
8829
9270
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
8830
9271
  currentStatus: paperclipIssue.status,
8831
9272
  nextStatus,
@@ -12723,6 +13164,22 @@ async function performSync(ctx, trigger, options = {}) {
12723
13164
  await persistRunningProgress(issueIndex === issues.length - 1);
12724
13165
  }
12725
13166
  }
13167
+ const transferReconciliationResult = await reconcileTransferredImportedIssues(ctx, {
13168
+ octokit,
13169
+ sourceMapping: mapping,
13170
+ sourceRepository: repository,
13171
+ allMappings: settings.mappings,
13172
+ importedIssues: [...importRegistryByIssueId.values()].filter(
13173
+ (importedIssue) => doesImportedIssueMatchTarget(importedIssue, options.target)
13174
+ ),
13175
+ allIssuesById,
13176
+ nextRegistry,
13177
+ paperclipApiBaseUrl,
13178
+ syncFailureContext: failureContext,
13179
+ failures: recoverableFailures
13180
+ });
13181
+ updatedStatusesCount += transferReconciliationResult.updatedStatusesCount;
13182
+ updatedDescriptionsCount += transferReconciliationResult.updatedDescriptionsCount;
12726
13183
  const importedIssuesForSynchronization = [...importRegistryByIssueId.values()].filter(
12727
13184
  (importedIssue) => allIssuesById.has(importedIssue.githubIssueId) && doesImportedIssueMatchTarget(importedIssue, options.target)
12728
13185
  );
@@ -14008,6 +14465,18 @@ var plugin = definePlugin({
14008
14465
  requireUnlinked: true
14009
14466
  });
14010
14467
  });
14468
+ ctx.actions.register("issue.unlinkGitHubItem", async (input) => {
14469
+ const record = input && typeof input === "object" ? input : {};
14470
+ const companyId = normalizeCompanyId(record.companyId);
14471
+ const issueId = normalizeOptionalString2(record.issueId);
14472
+ if (!companyId || !issueId) {
14473
+ throw new Error("companyId and issueId are required.");
14474
+ }
14475
+ return unlinkPaperclipIssueFromGitHub(ctx, {
14476
+ companyId,
14477
+ issueId
14478
+ });
14479
+ });
14011
14480
  ctx.actions.register("settings.saveRegistration", async (input) => {
14012
14481
  const previous = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
14013
14482
  const config = await getResolvedConfig(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",