paperclip-github-plugin 0.8.1 → 0.8.3

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 = {
@@ -5201,6 +5202,9 @@ function isHealthyMaintainerWaitTransition(params) {
5201
5202
  const { currentStatus, nextStatus, syncContext } = params;
5202
5203
  return nextStatus === "in_review" && (currentStatus === "done" || currentStatus === "in_review") && syncContext.executionState === null && syncContext.executionPolicy !== null;
5203
5204
  }
5205
+ function shouldPreserveImportedTriageAssignee(params) {
5206
+ return params.wasImportedThisRun && params.maintainerAuthoredImportedIssue === true && params.currentStatus === "backlog" && params.nextStatus === "todo";
5207
+ }
5204
5208
  function doesPaperclipIssueAssigneeMatch(currentAssignee, nextAssignee) {
5205
5209
  return isSamePaperclipIssueAssigneePrincipal(currentAssignee, nextAssignee);
5206
5210
  }
@@ -6365,6 +6369,18 @@ function extractImportedGitHubIssueUrlFromDescription(description) {
6365
6369
  }
6366
6370
  return normalizeGitHubIssueHtmlUrl(legacyMatch[1]);
6367
6371
  }
6372
+ function removeGitHubIssueLinkMetadataFromDescription(description) {
6373
+ if (typeof description !== "string") {
6374
+ return void 0;
6375
+ }
6376
+ let next = description;
6377
+ const hiddenMarkerPattern = getHiddenGitHubImportMarkerPattern();
6378
+ while (hiddenMarkerPattern.test(next)) {
6379
+ next = next.replace(hiddenMarkerPattern, "");
6380
+ }
6381
+ 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();
6382
+ return next === description ? void 0 : next;
6383
+ }
6368
6384
  function compareImportedPaperclipIssueCreatedAt(left, right) {
6369
6385
  const leftTime = Date.parse(String(left.createdAt ?? ""));
6370
6386
  const rightTime = Date.parse(String(right.createdAt ?? ""));
@@ -6693,6 +6709,7 @@ async function listGitHubIssueLinkRecords(ctx, query = {}) {
6693
6709
  }
6694
6710
  records.push({
6695
6711
  paperclipIssueId: entry.scopeId,
6712
+ ...typeof entry.externalId === "string" && entry.externalId.trim() ? { externalId: entry.externalId.trim() } : {},
6696
6713
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
6697
6714
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
6698
6715
  ...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
@@ -6739,6 +6756,7 @@ async function listGitHubPullRequestLinkRecords(ctx, query = {}) {
6739
6756
  }
6740
6757
  records.push({
6741
6758
  paperclipIssueId: entry.scopeId,
6759
+ ...typeof entry.externalId === "string" && entry.externalId.trim() ? { externalId: entry.externalId.trim() } : {},
6742
6760
  ...typeof entry.createdAt === "string" ? { createdAt: entry.createdAt } : {},
6743
6761
  ...typeof entry.updatedAt === "string" ? { updatedAt: entry.updatedAt } : {},
6744
6762
  ...typeof entry.title === "string" && entry.title.trim() ? { title: entry.title.trim() } : {},
@@ -7147,6 +7165,134 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
7147
7165
  githubPullRequestState: pullRequestState
7148
7166
  };
7149
7167
  }
7168
+ function invalidateGitHubLinkCachesForTarget(params) {
7169
+ if (!params.companyId || !params.projectId || !params.repositoryUrl) {
7170
+ return;
7171
+ }
7172
+ const repository = parseRepositoryReference(params.repositoryUrl);
7173
+ if (!repository) {
7174
+ return;
7175
+ }
7176
+ invalidateProjectPullRequestCaches({
7177
+ companyId: params.companyId,
7178
+ projectId: params.projectId,
7179
+ repository
7180
+ });
7181
+ }
7182
+ async function tombstoneGitHubIssueLinkRecord(ctx, record, unlinkedAt) {
7183
+ await ctx.entities.upsert({
7184
+ entityType: ISSUE_LINK_ENTITY_TYPE,
7185
+ scopeKind: "issue",
7186
+ scopeId: record.paperclipIssueId,
7187
+ externalId: record.externalId ?? record.data.githubIssueUrl,
7188
+ title: record.title ?? `GitHub issue #${record.data.githubIssueNumber}`,
7189
+ status: "unlinked",
7190
+ data: {
7191
+ kind: "issue",
7192
+ paperclipIssueId: record.paperclipIssueId,
7193
+ repositoryUrl: record.data.repositoryUrl,
7194
+ githubIssueNumber: record.data.githubIssueNumber,
7195
+ githubIssueUrl: record.data.githubIssueUrl,
7196
+ ...record.data.companyId ? { companyId: record.data.companyId } : {},
7197
+ ...record.data.paperclipProjectId ? { paperclipProjectId: record.data.paperclipProjectId } : {},
7198
+ unlinkedAt
7199
+ }
7200
+ });
7201
+ }
7202
+ async function tombstoneGitHubPullRequestLinkRecord(ctx, record, unlinkedAt) {
7203
+ await ctx.entities.upsert({
7204
+ entityType: PULL_REQUEST_LINK_ENTITY_TYPE,
7205
+ scopeKind: "issue",
7206
+ scopeId: record.paperclipIssueId,
7207
+ externalId: record.externalId ?? record.data.githubPullRequestUrl,
7208
+ title: record.title ?? `GitHub pull request #${record.data.githubPullRequestNumber}`,
7209
+ status: "unlinked",
7210
+ data: {
7211
+ kind: "pull_request",
7212
+ paperclipIssueId: record.paperclipIssueId,
7213
+ repositoryUrl: record.data.repositoryUrl,
7214
+ githubPullRequestNumber: record.data.githubPullRequestNumber,
7215
+ githubPullRequestUrl: record.data.githubPullRequestUrl,
7216
+ ...record.data.companyId ? { companyId: record.data.companyId } : {},
7217
+ ...record.data.paperclipProjectId ? { paperclipProjectId: record.data.paperclipProjectId } : {},
7218
+ unlinkedAt
7219
+ }
7220
+ });
7221
+ }
7222
+ async function unlinkPaperclipIssueFromGitHub(ctx, params) {
7223
+ const companyId = normalizeCompanyId(params.companyId);
7224
+ const issueId = normalizeOptionalString2(params.issueId);
7225
+ if (!companyId || !issueId) {
7226
+ throw new Error("companyId and issueId are required.");
7227
+ }
7228
+ const issue = await ctx.issues.get(issueId, companyId);
7229
+ if (!issue) {
7230
+ throw new Error("Paperclip issue was not found.");
7231
+ }
7232
+ const issueLinkRecords = (await listGitHubIssueLinkRecords(ctx, {
7233
+ paperclipIssueId: issueId
7234
+ })).filter((record) => !record.data.companyId || record.data.companyId === companyId);
7235
+ const pullRequestLinkRecords = (await listGitHubPullRequestLinkRecords(ctx, {
7236
+ paperclipIssueId: issueId
7237
+ })).filter((record) => !record.data.companyId || record.data.companyId === companyId);
7238
+ const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
7239
+ const removedImportRegistryEntries = importRegistry.filter(
7240
+ (entry) => entry.paperclipIssueId === issueId && (!entry.companyId || entry.companyId === companyId)
7241
+ );
7242
+ const nextImportRegistry = importRegistry.filter(
7243
+ (entry) => !(entry.paperclipIssueId === issueId && (!entry.companyId || entry.companyId === companyId))
7244
+ );
7245
+ const unlinkedAt = (/* @__PURE__ */ new Date()).toISOString();
7246
+ for (const record of issueLinkRecords) {
7247
+ await tombstoneGitHubIssueLinkRecord(ctx, record, unlinkedAt);
7248
+ invalidateGitHubLinkCachesForTarget({
7249
+ companyId: record.data.companyId ?? companyId,
7250
+ projectId: record.data.paperclipProjectId ?? issue.projectId ?? void 0,
7251
+ repositoryUrl: record.data.repositoryUrl
7252
+ });
7253
+ }
7254
+ for (const record of pullRequestLinkRecords) {
7255
+ await tombstoneGitHubPullRequestLinkRecord(ctx, record, unlinkedAt);
7256
+ invalidateGitHubLinkCachesForTarget({
7257
+ companyId: record.data.companyId ?? companyId,
7258
+ projectId: record.data.paperclipProjectId ?? issue.projectId ?? void 0,
7259
+ repositoryUrl: record.data.repositoryUrl
7260
+ });
7261
+ }
7262
+ for (const entry of removedImportRegistryEntries) {
7263
+ invalidateGitHubLinkCachesForTarget({
7264
+ companyId: entry.companyId ?? companyId,
7265
+ projectId: entry.paperclipProjectId ?? issue.projectId ?? void 0,
7266
+ repositoryUrl: entry.repositoryUrl
7267
+ });
7268
+ }
7269
+ if (removedImportRegistryEntries.length > 0) {
7270
+ await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextImportRegistry);
7271
+ }
7272
+ const shouldClearGitHubOrigin = issue.originKind === GITHUB_ISSUE_ORIGIN_KIND || issue.originKind === GITHUB_PULL_REQUEST_ORIGIN_KIND;
7273
+ const nextDescription = removeGitHubIssueLinkMetadataFromDescription(issue.description);
7274
+ const issuePatch = {};
7275
+ if (shouldClearGitHubOrigin) {
7276
+ issuePatch.originKind = GITHUB_SYNC_BASE_ORIGIN_KIND;
7277
+ issuePatch.originId = null;
7278
+ issuePatch.originRunId = null;
7279
+ }
7280
+ if (nextDescription !== void 0) {
7281
+ issuePatch.description = nextDescription;
7282
+ }
7283
+ if (Object.keys(issuePatch).length > 0) {
7284
+ await ctx.issues.update(issueId, issuePatch, companyId);
7285
+ }
7286
+ return {
7287
+ paperclipIssueId: issueId,
7288
+ unlinked: issueLinkRecords.length > 0 || pullRequestLinkRecords.length > 0 || removedImportRegistryEntries.length > 0 || shouldClearGitHubOrigin || nextDescription !== void 0,
7289
+ unlinkedIssueLinksCount: issueLinkRecords.length,
7290
+ unlinkedPullRequestLinksCount: pullRequestLinkRecords.length,
7291
+ removedImportRegistryEntriesCount: removedImportRegistryEntries.length,
7292
+ clearedIssueOrigin: shouldClearGitHubOrigin,
7293
+ removedDescriptionMetadata: nextDescription !== void 0
7294
+ };
7295
+ }
7150
7296
  async function upsertStatusTransitionCommentAnnotation(ctx, params) {
7151
7297
  const { issueId, commentId, annotation } = params;
7152
7298
  await ctx.entities.upsert({
@@ -8054,6 +8200,8 @@ async function updatePaperclipIssueState(ctx, params) {
8054
8200
  paperclipApiBaseUrl
8055
8201
  } = params;
8056
8202
  const trimmedTransitionComment = transitionComment.trim();
8203
+ const statusWillChange = currentStatus !== nextStatus;
8204
+ let createdTransitionComment = null;
8057
8205
  let issueUpdated = false;
8058
8206
  const syncExecutionStatePatch = buildSyncFallbackExecutionStatePatch({
8059
8207
  currentStatus,
@@ -8078,6 +8226,15 @@ async function updatePaperclipIssueState(ctx, params) {
8078
8226
  issuePatch.assigneeAgentId = null;
8079
8227
  issuePatch.assigneeUserId = null;
8080
8228
  }
8229
+ if (statusWillChange) {
8230
+ if (!trimmedTransitionComment) {
8231
+ throw new Error("GitHub Sync refused to update a Paperclip issue status without an explanatory transition comment.");
8232
+ }
8233
+ if (typeof ctx.issues.createComment !== "function") {
8234
+ throw new Error("This Paperclip runtime does not expose issue comment creation, so GitHub Sync refused to update a Paperclip issue status without an explanatory comment.");
8235
+ }
8236
+ createdTransitionComment = await ctx.issues.createComment(issueId, trimmedTransitionComment, companyId);
8237
+ }
8081
8238
  if (paperclipApiBaseUrl) {
8082
8239
  try {
8083
8240
  const response = await fetchPaperclipApi(
@@ -8139,7 +8296,19 @@ async function updatePaperclipIssueState(ctx, params) {
8139
8296
  }
8140
8297
  await ctx.issues.update(issueId, sdkIssuePatch, companyId);
8141
8298
  }
8142
- if (trimmedTransitionComment && typeof ctx.issues.createComment === "function") {
8299
+ if (createdTransitionComment) {
8300
+ if (transitionCommentAnnotation) {
8301
+ await upsertStatusTransitionCommentAnnotation(ctx, {
8302
+ issueId,
8303
+ commentId: createdTransitionComment.id,
8304
+ annotation: {
8305
+ ...transitionCommentAnnotation,
8306
+ companyId,
8307
+ paperclipIssueId: issueId
8308
+ }
8309
+ });
8310
+ }
8311
+ } else if (trimmedTransitionComment && typeof ctx.issues.createComment === "function") {
8143
8312
  const createdComment = await ctx.issues.createComment(issueId, trimmedTransitionComment, companyId);
8144
8313
  if (transitionCommentAnnotation) {
8145
8314
  await upsertStatusTransitionCommentAnnotation(ctx, {
@@ -8427,6 +8596,261 @@ async function ensurePaperclipIssueImported(ctx, mapping, advancedSettings, issu
8427
8596
  }
8428
8597
  return createdIssue.id;
8429
8598
  }
8599
+ function getGitHubIssueRepositoryReference(issue) {
8600
+ return parseGitHubIssueHtmlUrl(issue.htmlUrl);
8601
+ }
8602
+ function findTransferredIssueTargetMapping(mappings, params) {
8603
+ const companyId = normalizeCompanyId(params.companyId);
8604
+ const repository = parseRepositoryReference(params.repositoryUrl);
8605
+ if (!companyId || !repository) {
8606
+ return void 0;
8607
+ }
8608
+ return getSyncableMappings(mappings).find((mapping) => {
8609
+ if (normalizeCompanyId(mapping.companyId) !== companyId) {
8610
+ return false;
8611
+ }
8612
+ const mappingRepository = parseRepositoryReference(mapping.repositoryUrl);
8613
+ return Boolean(mappingRepository && areRepositoriesEqual(mappingRepository, repository));
8614
+ });
8615
+ }
8616
+ async function patchPaperclipIssueForGitHubIssueTransfer(ctx, params) {
8617
+ const nextDescription = buildPaperclipIssueDescription(params.githubIssue, []);
8618
+ const nextOriginId = normalizeGitHubIssueHtmlUrl(params.githubIssue.htmlUrl) ?? params.githubIssue.htmlUrl;
8619
+ const descriptionUpdated = normalizeIssueDescriptionValue(params.currentDescription) !== nextDescription;
8620
+ const patch = {
8621
+ projectId: params.projectId,
8622
+ originKind: GITHUB_ISSUE_ORIGIN_KIND,
8623
+ originId: nextOriginId,
8624
+ ...descriptionUpdated ? { description: nextDescription } : {}
8625
+ };
8626
+ let issueUpdated = false;
8627
+ if (params.paperclipApiBaseUrl) {
8628
+ try {
8629
+ const response = await fetchPaperclipApi(
8630
+ getPaperclipIssueEndpoint(params.paperclipApiBaseUrl, params.issueId),
8631
+ {
8632
+ method: "PATCH",
8633
+ headers: {
8634
+ accept: "application/json",
8635
+ "content-type": "application/json"
8636
+ },
8637
+ body: JSON.stringify(patch)
8638
+ },
8639
+ {
8640
+ companyId: params.companyId
8641
+ }
8642
+ );
8643
+ const payloadResult = await readPaperclipApiJsonResponse(response, {
8644
+ operationLabel: "issue update",
8645
+ bodyRequired: false
8646
+ });
8647
+ issueUpdated = !payloadResult.failure;
8648
+ } catch {
8649
+ issueUpdated = false;
8650
+ }
8651
+ }
8652
+ if (!issueUpdated) {
8653
+ await ctx.issues.update(
8654
+ params.issueId,
8655
+ patch,
8656
+ params.companyId
8657
+ );
8658
+ }
8659
+ return {
8660
+ descriptionUpdated
8661
+ };
8662
+ }
8663
+ async function moveImportedIssueToTransferredGitHubMapping(ctx, params) {
8664
+ const companyId = normalizeCompanyId(params.targetMapping.companyId);
8665
+ const projectId = normalizeOptionalString2(params.targetMapping.paperclipProjectId);
8666
+ if (!companyId || !projectId) {
8667
+ return {
8668
+ updatedDescriptionsCount: 0
8669
+ };
8670
+ }
8671
+ const paperclipIssue = await ctx.issues.get(params.importedIssue.paperclipIssueId, companyId);
8672
+ if (!paperclipIssue) {
8673
+ return {
8674
+ updatedDescriptionsCount: 0
8675
+ };
8676
+ }
8677
+ const transferPatchResult = await patchPaperclipIssueForGitHubIssueTransfer(ctx, {
8678
+ companyId,
8679
+ issueId: params.importedIssue.paperclipIssueId,
8680
+ projectId,
8681
+ githubIssue: params.githubIssue,
8682
+ currentDescription: paperclipIssue.description,
8683
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8684
+ });
8685
+ const nextGitHubIssueUrl = normalizeGitHubIssueHtmlUrl(params.githubIssue.htmlUrl) ?? params.githubIssue.htmlUrl;
8686
+ const existingLinks = await listGitHubIssueLinkRecords(ctx, {
8687
+ paperclipIssueId: params.importedIssue.paperclipIssueId
8688
+ });
8689
+ const unlinkedAt = (/* @__PURE__ */ new Date()).toISOString();
8690
+ for (const link of existingLinks) {
8691
+ if ((!link.data.companyId || link.data.companyId === companyId) && link.data.githubIssueUrl !== nextGitHubIssueUrl) {
8692
+ await tombstoneGitHubIssueLinkRecord(ctx, link, unlinkedAt);
8693
+ }
8694
+ }
8695
+ await upsertGitHubIssueLinkRecord(
8696
+ ctx,
8697
+ params.targetMapping,
8698
+ params.importedIssue.paperclipIssueId,
8699
+ params.githubIssue,
8700
+ []
8701
+ );
8702
+ upsertImportedIssueRecord(
8703
+ params.nextRegistry,
8704
+ buildImportedIssueRecord(
8705
+ params.targetMapping,
8706
+ params.githubIssue,
8707
+ params.importedIssue.paperclipIssueId,
8708
+ params.importedIssue.importedAt
8709
+ )
8710
+ );
8711
+ invalidateGitHubLinkCachesForTarget({
8712
+ companyId,
8713
+ projectId: params.sourceMapping.paperclipProjectId ?? paperclipIssue.projectId ?? void 0,
8714
+ repositoryUrl: params.sourceMapping.repositoryUrl
8715
+ });
8716
+ invalidateGitHubLinkCachesForTarget({
8717
+ companyId,
8718
+ projectId,
8719
+ repositoryUrl: params.targetMapping.repositoryUrl
8720
+ });
8721
+ return {
8722
+ updatedDescriptionsCount: transferPatchResult.descriptionUpdated ? 1 : 0
8723
+ };
8724
+ }
8725
+ function buildUnmappedTransferredIssueCancellationComment(params) {
8726
+ 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.`;
8727
+ }
8728
+ async function cancelUnmappedTransferredGitHubIssue(ctx, params) {
8729
+ const companyId = normalizeCompanyId(params.mapping.companyId);
8730
+ if (!companyId) {
8731
+ return {
8732
+ updatedStatusesCount: 0
8733
+ };
8734
+ }
8735
+ const paperclipIssue = await ctx.issues.get(params.importedIssue.paperclipIssueId, companyId);
8736
+ if (!paperclipIssue) {
8737
+ return {
8738
+ updatedStatusesCount: 0
8739
+ };
8740
+ }
8741
+ await unlinkPaperclipIssueFromGitHub(ctx, {
8742
+ companyId,
8743
+ issueId: params.importedIssue.paperclipIssueId
8744
+ });
8745
+ const nextStatus = "cancelled";
8746
+ await updatePaperclipIssueState(ctx, {
8747
+ companyId,
8748
+ issueId: params.importedIssue.paperclipIssueId,
8749
+ currentStatus: paperclipIssue.status,
8750
+ syncContext: getPaperclipIssueSyncContext(paperclipIssue),
8751
+ nextStatus,
8752
+ transitionComment: buildUnmappedTransferredIssueCancellationComment({
8753
+ previousStatus: paperclipIssue.status,
8754
+ nextStatus,
8755
+ transferredRepository: params.transferredRepository
8756
+ }),
8757
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8758
+ });
8759
+ return {
8760
+ updatedStatusesCount: paperclipIssue.status === nextStatus ? 0 : 1
8761
+ };
8762
+ }
8763
+ async function fetchTransferredGitHubIssueForImportedRecord(octokit, repository, importedIssue) {
8764
+ if (importedIssue.githubIssueNumber === void 0) {
8765
+ return null;
8766
+ }
8767
+ const response = await octokit.rest.issues.get({
8768
+ owner: repository.owner,
8769
+ repo: repository.repo,
8770
+ issue_number: importedIssue.githubIssueNumber,
8771
+ headers: {
8772
+ accept: "application/vnd.github+json",
8773
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8774
+ }
8775
+ });
8776
+ const rawIssue = response.data;
8777
+ if (rawIssue.pull_request) {
8778
+ return null;
8779
+ }
8780
+ const githubIssue = normalizeGitHubIssueRecord(rawIssue);
8781
+ const transferredIssueReference = getGitHubIssueRepositoryReference(githubIssue);
8782
+ const transferredRepository = transferredIssueReference ? parseRepositoryReference(transferredIssueReference.repositoryUrl) : void 0;
8783
+ if (!transferredRepository || areRepositoriesEqual(repository, transferredRepository)) {
8784
+ return null;
8785
+ }
8786
+ return {
8787
+ githubIssue,
8788
+ transferredRepository
8789
+ };
8790
+ }
8791
+ async function reconcileTransferredImportedIssues(ctx, params) {
8792
+ let updatedStatusesCount = 0;
8793
+ let updatedDescriptionsCount = 0;
8794
+ for (const importedIssue of params.importedIssues) {
8795
+ if (params.allIssuesById.has(importedIssue.githubIssueId)) {
8796
+ continue;
8797
+ }
8798
+ try {
8799
+ updateSyncFailureContext(params.syncFailureContext, {
8800
+ phase: "evaluating_github_status",
8801
+ repositoryUrl: params.sourceRepository.url,
8802
+ githubIssueNumber: importedIssue.githubIssueNumber
8803
+ });
8804
+ const transfer = await fetchTransferredGitHubIssueForImportedRecord(
8805
+ params.octokit,
8806
+ params.sourceRepository,
8807
+ importedIssue
8808
+ );
8809
+ if (!transfer) {
8810
+ continue;
8811
+ }
8812
+ const targetMapping = findTransferredIssueTargetMapping(params.allMappings, {
8813
+ companyId: params.sourceMapping.companyId,
8814
+ repositoryUrl: transfer.transferredRepository.url
8815
+ });
8816
+ if (targetMapping) {
8817
+ const moveResult = await moveImportedIssueToTransferredGitHubMapping(ctx, {
8818
+ sourceMapping: params.sourceMapping,
8819
+ targetMapping,
8820
+ importedIssue,
8821
+ githubIssue: transfer.githubIssue,
8822
+ nextRegistry: params.nextRegistry,
8823
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8824
+ });
8825
+ updatedDescriptionsCount += moveResult.updatedDescriptionsCount;
8826
+ continue;
8827
+ }
8828
+ const cancelResult = await cancelUnmappedTransferredGitHubIssue(ctx, {
8829
+ mapping: params.sourceMapping,
8830
+ importedIssue,
8831
+ transferredRepository: transfer.transferredRepository,
8832
+ paperclipApiBaseUrl: params.paperclipApiBaseUrl
8833
+ });
8834
+ for (let index = params.nextRegistry.length - 1; index >= 0; index -= 1) {
8835
+ const entry = params.nextRegistry[index];
8836
+ if (entry?.paperclipIssueId === importedIssue.paperclipIssueId && (!entry.companyId || entry.companyId === params.sourceMapping.companyId)) {
8837
+ params.nextRegistry.splice(index, 1);
8838
+ }
8839
+ }
8840
+ updatedStatusesCount += cancelResult.updatedStatusesCount;
8841
+ } catch (error) {
8842
+ if (isGitHubRateLimitError(error)) {
8843
+ throw error;
8844
+ }
8845
+ recordRecoverableSyncFailure(ctx, params.failures, error, params.syncFailureContext);
8846
+ continue;
8847
+ }
8848
+ }
8849
+ return {
8850
+ updatedStatusesCount,
8851
+ updatedDescriptionsCount
8852
+ };
8853
+ }
8430
8854
  async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onGitHubIssueClosed, onProgress) {
8431
8855
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
8432
8856
  return {
@@ -8615,7 +9039,13 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8615
9039
  nextStatus,
8616
9040
  syncContext: paperclipIssueSyncContext
8617
9041
  });
8618
- const nextTransitionAssignee = resolveSyncTransitionAssignee({
9042
+ const shouldPreserveImportedTriageRouting = shouldPreserveImportedTriageAssignee({
9043
+ currentStatus: paperclipIssue.status,
9044
+ nextStatus,
9045
+ wasImportedThisRun,
9046
+ maintainerAuthoredImportedIssue
9047
+ });
9048
+ const nextTransitionAssignee = shouldPreserveImportedTriageRouting ? null : resolveSyncTransitionAssignee({
8619
9049
  currentStatus: paperclipIssue.status,
8620
9050
  nextStatus,
8621
9051
  syncContext: paperclipIssueSyncContext,
@@ -8623,7 +9053,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8623
9053
  });
8624
9054
  const shouldClearTransitionAssignee = nextStatus === "in_review" && (nextTransitionAssignee === null || shouldPreserveMaintainerWaitRouting) && paperclipIssueSyncContext.assignee !== null;
8625
9055
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
8626
- const shouldWakeImportedAssignee = wasImportedThisRun && paperclipIssue.status === nextStatus && nextStatus === "todo" && paperclipIssueSyncContext.assignee?.kind === "agent";
9056
+ const shouldWakeImportedAssignee = wasImportedThisRun && nextStatus === "todo" && (paperclipIssue.status === nextStatus || shouldPreserveImportedTriageRouting) && paperclipIssueSyncContext.assignee?.kind === "agent";
8627
9057
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
8628
9058
  importedIssue.githubIssueNumber = githubIssue.number;
8629
9059
  importedIssue.lastSeenCommentCount = snapshot.commentCount;
@@ -8685,6 +9115,14 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
8685
9115
  paperclipApiBaseUrl
8686
9116
  });
8687
9117
  updatedStatusesCount += 1;
9118
+ if (shouldWakeImportedAssignee) {
9119
+ queuedIssueWakeups.push({
9120
+ assigneeAgentId: paperclipIssueSyncContext.assignee?.kind === "agent" ? paperclipIssueSyncContext.assignee.id : null,
9121
+ paperclipIssueId: importedIssue.paperclipIssueId,
9122
+ reason: IMPORTED_ISSUE_WAKE_REASON,
9123
+ mutation: "import"
9124
+ });
9125
+ }
8688
9126
  if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
8689
9127
  queuedIssueWakeups.push({
8690
9128
  assigneeAgentId: nextTransitionAssignee.principal.id,
@@ -12700,6 +13138,22 @@ async function performSync(ctx, trigger, options = {}) {
12700
13138
  await persistRunningProgress(issueIndex === issues.length - 1);
12701
13139
  }
12702
13140
  }
13141
+ const transferReconciliationResult = await reconcileTransferredImportedIssues(ctx, {
13142
+ octokit,
13143
+ sourceMapping: mapping,
13144
+ sourceRepository: repository,
13145
+ allMappings: settings.mappings,
13146
+ importedIssues: [...importRegistryByIssueId.values()].filter(
13147
+ (importedIssue) => doesImportedIssueMatchTarget(importedIssue, options.target)
13148
+ ),
13149
+ allIssuesById,
13150
+ nextRegistry,
13151
+ paperclipApiBaseUrl,
13152
+ syncFailureContext: failureContext,
13153
+ failures: recoverableFailures
13154
+ });
13155
+ updatedStatusesCount += transferReconciliationResult.updatedStatusesCount;
13156
+ updatedDescriptionsCount += transferReconciliationResult.updatedDescriptionsCount;
12703
13157
  const importedIssuesForSynchronization = [...importRegistryByIssueId.values()].filter(
12704
13158
  (importedIssue) => allIssuesById.has(importedIssue.githubIssueId) && doesImportedIssueMatchTarget(importedIssue, options.target)
12705
13159
  );
@@ -13985,6 +14439,18 @@ var plugin = definePlugin({
13985
14439
  requireUnlinked: true
13986
14440
  });
13987
14441
  });
14442
+ ctx.actions.register("issue.unlinkGitHubItem", async (input) => {
14443
+ const record = input && typeof input === "object" ? input : {};
14444
+ const companyId = normalizeCompanyId(record.companyId);
14445
+ const issueId = normalizeOptionalString2(record.issueId);
14446
+ if (!companyId || !issueId) {
14447
+ throw new Error("companyId and issueId are required.");
14448
+ }
14449
+ return unlinkPaperclipIssueFromGitHub(ctx, {
14450
+ companyId,
14451
+ issueId
14452
+ });
14453
+ });
13988
14454
  ctx.actions.register("settings.saveRegistration", async (input) => {
13989
14455
  const previous = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
13990
14456
  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.1",
3
+ "version": "0.8.3",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",