paperclip-github-plugin 0.8.12 → 0.9.1

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
@@ -1,9 +1,9 @@
1
1
  // src/worker.ts
2
2
  import { Buffer } from "node:buffer";
3
3
  import { realpathSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
4
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
- import { join, resolve } from "node:path";
6
+ import { dirname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { Octokit } from "@octokit/rest";
9
9
  import {
@@ -1604,6 +1604,10 @@ function getErrorMessage(error) {
1604
1604
  }
1605
1605
  return String(error);
1606
1606
  }
1607
+ function isPluginSecretReferenceDisabledError(error) {
1608
+ const message = getErrorMessage(error).toLowerCase();
1609
+ return message.includes("plugin secret reference") && message.includes("disabled") || message.includes("company-scoped plugin config lands");
1610
+ }
1607
1611
  function getErrorCause(error) {
1608
1612
  if (!error || typeof error !== "object" || !("cause" in error)) {
1609
1613
  return void 0;
@@ -1974,6 +1978,20 @@ function normalizeGitHubTokenRefs(value) {
1974
1978
  }
1975
1979
  return Object.fromEntries(entries);
1976
1980
  }
1981
+ function normalizeGitHubTokensByCompanyId(value) {
1982
+ if (!value || typeof value !== "object") {
1983
+ return void 0;
1984
+ }
1985
+ const entries = Object.entries(value).map(([companyId, token]) => {
1986
+ const normalizedCompanyId = normalizeCompanyId(companyId);
1987
+ const normalizedToken = normalizeGitHubToken(token);
1988
+ return normalizedCompanyId && normalizedToken ? [normalizedCompanyId, normalizedToken] : null;
1989
+ }).filter((entry) => entry !== null);
1990
+ if (entries.length === 0) {
1991
+ return void 0;
1992
+ }
1993
+ return Object.fromEntries(entries);
1994
+ }
1977
1995
  function formatUtcTimestamp(value) {
1978
1996
  const parsed = new Date(value);
1979
1997
  if (Number.isNaN(parsed.getTime())) {
@@ -3710,12 +3728,14 @@ function normalizeConfig(value) {
3710
3728
  const githubTokenRefs = normalizeGitHubTokenRefs(record.githubTokenRefs);
3711
3729
  const githubTokenRef = normalizeGitHubTokenRef(record.githubTokenRef);
3712
3730
  const githubToken = normalizeGitHubToken(record.githubToken);
3731
+ const githubTokensByCompanyId = normalizeGitHubTokensByCompanyId(record.githubTokensByCompanyId);
3713
3732
  const paperclipBoardApiTokenRefs = normalizePaperclipBoardApiTokenRefs(record.paperclipBoardApiTokenRefs);
3714
3733
  const paperclipApiBaseUrl = normalizePaperclipApiBaseUrl(record.paperclipApiBaseUrl);
3715
3734
  return {
3716
3735
  ...githubTokenRefs ? { githubTokenRefs } : {},
3717
3736
  ...githubTokenRef ? { githubTokenRef } : {},
3718
3737
  ...githubToken ? { githubToken } : {},
3738
+ ...githubTokensByCompanyId ? { githubTokensByCompanyId } : {},
3719
3739
  ...paperclipBoardApiTokenRefs ? { paperclipBoardApiTokenRefs } : {},
3720
3740
  ...paperclipApiBaseUrl ? { paperclipApiBaseUrl } : {}
3721
3741
  };
@@ -3791,6 +3811,55 @@ async function readExternalConfig(ctx) {
3791
3811
  return {};
3792
3812
  }
3793
3813
  }
3814
+ async function readExternalConfigRecordForWrite(ctx, filePath) {
3815
+ try {
3816
+ const rawConfig = await readFile(filePath, "utf8");
3817
+ const parsedConfig = JSON.parse(rawConfig);
3818
+ return parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig) ? { ...parsedConfig } : {};
3819
+ } catch (error) {
3820
+ const errorCode = error && typeof error === "object" && "code" in error ? error.code : void 0;
3821
+ if (errorCode === "ENOENT") {
3822
+ return {};
3823
+ }
3824
+ if (error instanceof SyntaxError) {
3825
+ ctx.logger.warn("Ignoring the GitHub Sync worker-local token fallback config file because it is not valid JSON.", {
3826
+ filePath,
3827
+ error: error.message
3828
+ });
3829
+ return {};
3830
+ }
3831
+ throw error;
3832
+ }
3833
+ }
3834
+ async function writeExternalCompanyGitHubTokenFallback(ctx, companyId, token) {
3835
+ const externalConfigFilePath = getExternalConfigFilePath();
3836
+ if (!externalConfigFilePath) {
3837
+ throw new Error("Could not resolve a Paperclip home directory for the GitHub Sync fallback token config.");
3838
+ }
3839
+ const currentRecord = await readExternalConfigRecordForWrite(ctx, externalConfigFilePath);
3840
+ const currentCompanyTokens = normalizeGitHubTokensByCompanyId(currentRecord.githubTokensByCompanyId) ?? {};
3841
+ const nextRecord = {
3842
+ ...currentRecord,
3843
+ githubTokensByCompanyId: {
3844
+ ...currentCompanyTokens,
3845
+ [companyId]: token
3846
+ }
3847
+ };
3848
+ await mkdir(dirname(externalConfigFilePath), { recursive: true });
3849
+ await writeFile(externalConfigFilePath, `${JSON.stringify(nextRecord, null, 2)}
3850
+ `, {
3851
+ encoding: "utf8",
3852
+ mode: 384
3853
+ });
3854
+ try {
3855
+ await chmod(externalConfigFilePath, 384);
3856
+ } catch (error) {
3857
+ ctx.logger.warn("GitHub Sync could not tighten permissions on the worker-local token fallback file.", {
3858
+ filePath: externalConfigFilePath,
3859
+ error: getErrorMessage(error)
3860
+ });
3861
+ }
3862
+ }
3794
3863
  function normalizePaperclipBoardApiTokenRefs(value) {
3795
3864
  if (!value || typeof value !== "object") {
3796
3865
  return void 0;
@@ -5681,9 +5750,19 @@ function buildPaperclipIssueStatusTransitionComment(params) {
5681
5750
  };
5682
5751
  }
5683
5752
  function buildPaperclipPullRequestIssueStatusTransitionComment(params) {
5684
- const reason = describeGitHubLinkedPullRequestsStatusReason([params.pullRequest]);
5753
+ const reason = describeGitHubDirectPullRequestIssueStatusReason(params.pullRequests);
5685
5754
  return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because ${reason}.`;
5686
5755
  }
5756
+ function describeGitHubDirectPullRequestIssueStatusReason(pullRequests) {
5757
+ const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
5758
+ if (openPullRequests.length > 0) {
5759
+ return describeGitHubLinkedPullRequestsStatusReason(openPullRequests);
5760
+ }
5761
+ if (pullRequests.some((entry) => entry.lifecycleState === "merged")) {
5762
+ return pullRequests.length === 1 ? "the linked pull request was merged" : "at least one linked pull request was merged";
5763
+ }
5764
+ return pullRequests.length === 1 ? "the linked pull request was closed without merge" : "all linked pull requests were closed without merge";
5765
+ }
5687
5766
  function resolvePaperclipIssueStatus(params) {
5688
5767
  const {
5689
5768
  currentStatus,
@@ -5736,6 +5815,23 @@ function resolvePaperclipPullRequestIssueStatus(params) {
5736
5815
  preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5737
5816
  });
5738
5817
  }
5818
+ function resolvePaperclipDirectPullRequestIssueStatus(params) {
5819
+ const { currentStatus, pullRequests, hasExecutorHandoffTarget } = params;
5820
+ const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
5821
+ if (openPullRequests.length > 0) {
5822
+ if (shouldPreserveBlockedExternalPullRequestWait({
5823
+ currentStatus,
5824
+ linkedPullRequests: openPullRequests
5825
+ })) {
5826
+ return "blocked";
5827
+ }
5828
+ return resolvePaperclipStatusFromLinkedPullRequests(openPullRequests, {
5829
+ preferInProgress: hasExecutorHandoffTarget,
5830
+ preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
5831
+ });
5832
+ }
5833
+ return pullRequests.some((entry) => entry.lifecycleState === "merged") ? "done" : "cancelled";
5834
+ }
5739
5835
  async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
5740
5836
  const linkedPullRequests = [];
5741
5837
  const seenPullRequestKeys = /* @__PURE__ */ new Set();
@@ -8975,6 +9071,16 @@ async function assignImportedPaperclipIssueToUser(ctx, params) {
8975
9071
  });
8976
9072
  }
8977
9073
  }
9074
+ async function ensurePaperclipIssueStandardWorkMode(ctx, issue, companyId) {
9075
+ if (!("workMode" in issue) || issue.workMode === "standard") {
9076
+ return issue;
9077
+ }
9078
+ return ctx.issues.update(
9079
+ issue.id,
9080
+ { workMode: "standard" },
9081
+ companyId
9082
+ );
9083
+ }
8978
9084
  async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, availableLabels, paperclipApiBaseUrl, syncFailureContext) {
8979
9085
  if (!mapping.companyId || !mapping.paperclipProjectId) {
8980
9086
  throw new Error(`Mapping ${mapping.id} is missing resolved Paperclip project identifiers.`);
@@ -8982,15 +9088,20 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
8982
9088
  const title = issue.title;
8983
9089
  const description = buildPaperclipIssueDescription(issue);
8984
9090
  const defaultAssignee = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "default");
8985
- const createdIssue = await ctx.issues.create({
8986
- companyId: mapping.companyId,
8987
- projectId: mapping.paperclipProjectId,
8988
- title,
8989
- ...description ? { description } : {},
8990
- originKind: GITHUB_ISSUE_ORIGIN_KIND,
8991
- originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
8992
- ...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
8993
- });
9091
+ const createdIssue = await ensurePaperclipIssueStandardWorkMode(
9092
+ ctx,
9093
+ await ctx.issues.create({
9094
+ companyId: mapping.companyId,
9095
+ projectId: mapping.paperclipProjectId,
9096
+ title,
9097
+ ...description ? { description } : {},
9098
+ status: advancedSettings.defaultStatus,
9099
+ originKind: GITHUB_ISSUE_ORIGIN_KIND,
9100
+ originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
9101
+ ...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
9102
+ }),
9103
+ mapping.companyId
9104
+ );
8994
9105
  const ensuredCreatedIssueId = createdIssue.id;
8995
9106
  const normalizedCreatedIssueDescription = createdIssue.description ?? void 0;
8996
9107
  const createPath = "sdk";
@@ -9708,6 +9819,18 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
9708
9819
  updatedDescriptionsCount
9709
9820
  };
9710
9821
  }
9822
+ function groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks) {
9823
+ const groups = /* @__PURE__ */ new Map();
9824
+ const sortedLinks = [...pullRequestLinks].sort(
9825
+ (left, right) => left.paperclipIssueId.localeCompare(right.paperclipIssueId) || left.data.repositoryUrl.localeCompare(right.data.repositoryUrl) || left.data.githubPullRequestNumber - right.data.githubPullRequestNumber
9826
+ );
9827
+ for (const pullRequestLink of sortedLinks) {
9828
+ const group = groups.get(pullRequestLink.paperclipIssueId) ?? [];
9829
+ group.push(pullRequestLink);
9830
+ groups.set(pullRequestLink.paperclipIssueId, group);
9831
+ }
9832
+ return groups;
9833
+ }
9711
9834
  async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
9712
9835
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
9713
9836
  return {
@@ -9719,52 +9842,93 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9719
9842
  const mappingCompanyId = mapping.companyId;
9720
9843
  const mappingProjectId = mapping.paperclipProjectId;
9721
9844
  const totalIssueCount = pullRequestLinks.length;
9845
+ const pullRequestLinkGroups = groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks);
9722
9846
  const queuedIssueWakeups = [];
9723
- for (const pullRequestLink of pullRequestLinks) {
9724
- if (assertNotCancelled) {
9725
- await assertNotCancelled();
9726
- }
9727
- try {
9728
- const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
9729
- updateSyncFailureContext(syncFailureContext, {
9730
- phase: "evaluating_github_status",
9731
- repositoryUrl: pullRequestRepository.url,
9732
- githubIssueNumber: void 0
9733
- });
9734
- const pullRequestResponse = await octokit.rest.pulls.get({
9735
- owner: pullRequestRepository.owner,
9736
- repo: pullRequestRepository.repo,
9737
- pull_number: pullRequestLink.data.githubPullRequestNumber,
9738
- headers: {
9739
- "X-GitHub-Api-Version": GITHUB_API_VERSION
9740
- }
9741
- });
9742
- const livePullRequestState = getPullRequestApiState({
9743
- state: pullRequestResponse.data.state,
9744
- merged: pullRequestResponse.data.merged
9745
- }) === "open" ? "open" : "closed";
9746
- if (livePullRequestState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
9747
- await upsertGitHubPullRequestLinkRecord(ctx, {
9748
- companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
9749
- projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
9750
- issueId: pullRequestLink.paperclipIssueId,
9847
+ for (const [paperclipIssueId, issuePullRequestLinks] of pullRequestLinkGroups.entries()) {
9848
+ const pullRequestSnapshots = [];
9849
+ let groupHadFailure = false;
9850
+ for (const pullRequestLink of issuePullRequestLinks) {
9851
+ if (assertNotCancelled) {
9852
+ await assertNotCancelled();
9853
+ }
9854
+ try {
9855
+ const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
9856
+ updateSyncFailureContext(syncFailureContext, {
9857
+ phase: "evaluating_github_status",
9751
9858
  repositoryUrl: pullRequestRepository.url,
9752
- pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
9753
- pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
9754
- pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
9755
- pullRequestState: livePullRequestState
9859
+ githubIssueNumber: void 0
9860
+ });
9861
+ const pullRequestResponse = await octokit.rest.pulls.get({
9862
+ owner: pullRequestRepository.owner,
9863
+ repo: pullRequestRepository.repo,
9864
+ pull_number: pullRequestLink.data.githubPullRequestNumber,
9865
+ headers: {
9866
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
9867
+ }
9756
9868
  });
9869
+ const livePullRequestLifecycleState = getPullRequestApiState({
9870
+ state: pullRequestResponse.data.state,
9871
+ merged: pullRequestResponse.data.merged
9872
+ });
9873
+ const livePullRequestLinkState = livePullRequestLifecycleState === "open" ? "open" : "closed";
9874
+ if (livePullRequestLinkState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
9875
+ await upsertGitHubPullRequestLinkRecord(ctx, {
9876
+ companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
9877
+ projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
9878
+ issueId: pullRequestLink.paperclipIssueId,
9879
+ repositoryUrl: pullRequestRepository.url,
9880
+ pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
9881
+ pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
9882
+ pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
9883
+ pullRequestState: livePullRequestLinkState
9884
+ });
9885
+ }
9886
+ if (livePullRequestLifecycleState === "open") {
9887
+ const pullRequest = await getGitHubPullRequestStatusSnapshot(
9888
+ octokit,
9889
+ pullRequestRepository,
9890
+ pullRequestLink.data.githubPullRequestNumber,
9891
+ pullRequestStatusCache
9892
+ );
9893
+ pullRequestSnapshots.push({
9894
+ pullRequestLink,
9895
+ repository: pullRequestRepository,
9896
+ lifecycleState: "open",
9897
+ pullRequest
9898
+ });
9899
+ } else {
9900
+ pullRequestSnapshots.push({
9901
+ pullRequestLink,
9902
+ repository: pullRequestRepository,
9903
+ lifecycleState: livePullRequestLifecycleState
9904
+ });
9905
+ }
9906
+ } catch (error) {
9907
+ if (isGitHubRateLimitError(error)) {
9908
+ throw error;
9909
+ }
9910
+ groupHadFailure = true;
9911
+ recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
9912
+ } finally {
9913
+ completedIssueCount += 1;
9914
+ if (onProgress) {
9915
+ await onProgress({
9916
+ pullRequestLink,
9917
+ completedIssueCount,
9918
+ totalIssueCount
9919
+ });
9920
+ }
9757
9921
  }
9758
- if (livePullRequestState !== "open") {
9759
- continue;
9922
+ }
9923
+ if (groupHadFailure || pullRequestSnapshots.length === 0) {
9924
+ continue;
9925
+ }
9926
+ const primaryRepository = pullRequestSnapshots[0]?.repository;
9927
+ try {
9928
+ if (assertNotCancelled) {
9929
+ await assertNotCancelled();
9760
9930
  }
9761
- const pullRequest = await getGitHubPullRequestStatusSnapshot(
9762
- octokit,
9763
- pullRequestRepository,
9764
- pullRequestLink.data.githubPullRequestNumber,
9765
- pullRequestStatusCache
9766
- );
9767
- const paperclipIssue = await ctx.issues.get(pullRequestLink.paperclipIssueId, mapping.companyId);
9931
+ const paperclipIssue = await ctx.issues.get(paperclipIssueId, mapping.companyId);
9768
9932
  if (!paperclipIssue) {
9769
9933
  continue;
9770
9934
  }
@@ -9773,12 +9937,12 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9773
9937
  paperclipIssueSyncContext,
9774
9938
  advancedSettings
9775
9939
  );
9776
- let nextStatus = resolvePaperclipPullRequestIssueStatus({
9940
+ let nextStatus = resolvePaperclipDirectPullRequestIssueStatus({
9777
9941
  currentStatus: paperclipIssue.status,
9778
- pullRequest,
9942
+ pullRequests: pullRequestSnapshots,
9779
9943
  hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
9780
9944
  });
9781
- if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9945
+ if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && pullRequestSnapshots.some((entry) => entry.lifecycleState === "open") && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
9782
9946
  nextStatus = "blocked";
9783
9947
  }
9784
9948
  const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
@@ -9786,6 +9950,10 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9786
9950
  nextStatus,
9787
9951
  syncContext: paperclipIssueSyncContext
9788
9952
  });
9953
+ const shouldClearCompletedExecutionPolicy = shouldClearCompletedSyncExecutionPolicy({
9954
+ nextStatus,
9955
+ syncContext: paperclipIssueSyncContext
9956
+ });
9789
9957
  const nextTransitionAssignee = resolveSyncTransitionAssignee({
9790
9958
  currentStatus: paperclipIssue.status,
9791
9959
  nextStatus,
@@ -9796,20 +9964,20 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9796
9964
  const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
9797
9965
  const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
9798
9966
  if (paperclipIssue.status === nextStatus) {
9799
- if (shouldClearTransitionAssignee) {
9967
+ if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
9800
9968
  updateSyncFailureContext(syncFailureContext, {
9801
9969
  phase: "updating_paperclip_status",
9802
- repositoryUrl: pullRequestRepository.url,
9970
+ repositoryUrl: primaryRepository?.url,
9803
9971
  githubIssueNumber: void 0
9804
9972
  });
9805
9973
  await updatePaperclipIssueState(ctx, {
9806
9974
  companyId: mapping.companyId,
9807
- issueId: pullRequestLink.paperclipIssueId,
9975
+ issueId: paperclipIssueId,
9808
9976
  currentStatus: paperclipIssue.status,
9809
9977
  syncContext: paperclipIssueSyncContext,
9810
9978
  nextStatus,
9811
- clearAssignee: true,
9812
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
9979
+ ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9980
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9813
9981
  transitionComment: "",
9814
9982
  paperclipApiBaseUrl
9815
9983
  });
@@ -9819,22 +9987,22 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9819
9987
  const transitionComment = buildPaperclipPullRequestIssueStatusTransitionComment({
9820
9988
  previousStatus: paperclipIssue.status,
9821
9989
  nextStatus,
9822
- pullRequest
9990
+ pullRequests: pullRequestSnapshots
9823
9991
  });
9824
9992
  updateSyncFailureContext(syncFailureContext, {
9825
9993
  phase: "updating_paperclip_status",
9826
- repositoryUrl: pullRequestRepository.url,
9994
+ repositoryUrl: primaryRepository?.url,
9827
9995
  githubIssueNumber: void 0
9828
9996
  });
9829
9997
  await updatePaperclipIssueState(ctx, {
9830
9998
  companyId: mapping.companyId,
9831
- issueId: pullRequestLink.paperclipIssueId,
9999
+ issueId: paperclipIssueId,
9832
10000
  currentStatus: paperclipIssue.status,
9833
10001
  syncContext: paperclipIssueSyncContext,
9834
10002
  nextStatus,
9835
10003
  ...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
9836
10004
  ...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
9837
- ...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
10005
+ ...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
9838
10006
  transitionComment,
9839
10007
  paperclipApiBaseUrl
9840
10008
  });
@@ -9842,7 +10010,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9842
10010
  if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
9843
10011
  queuedIssueWakeups.push({
9844
10012
  assigneeAgentId: nextTransitionAssignee.principal.id,
9845
- paperclipIssueId: pullRequestLink.paperclipIssueId,
10013
+ paperclipIssueId,
9846
10014
  reason: STATUS_TRANSITION_WAKE_REASON,
9847
10015
  mutation: "status_transition",
9848
10016
  previousStatus: paperclipIssue.status,
@@ -9854,16 +10022,6 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
9854
10022
  throw error;
9855
10023
  }
9856
10024
  recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
9857
- continue;
9858
- } finally {
9859
- completedIssueCount += 1;
9860
- if (onProgress) {
9861
- await onProgress({
9862
- pullRequestLink,
9863
- completedIssueCount,
9864
- totalIssueCount
9865
- });
9866
- }
9867
10025
  }
9868
10026
  }
9869
10027
  await mapWithConcurrency(
@@ -9896,6 +10054,7 @@ async function getResolvedConfig(ctx) {
9896
10054
  }
9897
10055
  function getConfiguredGithubTokenSource(settings, config, companyId) {
9898
10056
  const normalizedCompanyId = normalizeCompanyId(companyId);
10057
+ const companyFallbackToken = normalizedCompanyId ? normalizeGitHubToken(config.githubTokensByCompanyId?.[normalizedCompanyId]) : void 0;
9899
10058
  const hasScopedGitHubTokenRefs = hasAnyScopedValue(settings?.githubTokenRefs) || hasAnyScopedValue(config.githubTokenRefs);
9900
10059
  const secretRef = normalizedCompanyId ? normalizeSecretRef(config.githubTokenRefs?.[normalizedCompanyId]) ?? normalizeSecretRef(settings?.githubTokenRefs?.[normalizedCompanyId]) ?? (!hasScopedGitHubTokenRefs ? normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) : void 0) : normalizeGitHubTokenRef(config.githubTokenRef) ?? normalizeGitHubTokenRef(settings?.githubTokenRef) ?? (() => {
9901
10060
  const configuredRefs = [
@@ -9906,14 +10065,17 @@ function getConfiguredGithubTokenSource(settings, config, companyId) {
9906
10065
  return uniqueRefs.length === 1 ? uniqueRefs[0] : void 0;
9907
10066
  })();
9908
10067
  if (secretRef) {
9909
- return { secretRef };
10068
+ return {
10069
+ secretRef,
10070
+ ...companyFallbackToken ? { fallbackToken: companyFallbackToken } : {}
10071
+ };
9910
10072
  }
9911
- const token = !normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0;
10073
+ const token = companyFallbackToken ?? (!normalizedCompanyId || !hasScopedGitHubTokenRefs ? normalizeGitHubToken(config.githubToken) : void 0);
9912
10074
  return token ? { token } : {};
9913
10075
  }
9914
10076
  function hasConfiguredGithubToken(settings, config, companyId) {
9915
10077
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, companyId);
9916
- if (configuredTokenSource.secretRef ?? configuredTokenSource.token) {
10078
+ if (configuredTokenSource.secretRef ?? configuredTokenSource.token ?? configuredTokenSource.fallbackToken) {
9917
10079
  return true;
9918
10080
  }
9919
10081
  if (normalizeCompanyId(companyId)) {
@@ -9923,6 +10085,18 @@ function hasConfiguredGithubToken(settings, config, companyId) {
9923
10085
  settings?.githubTokenRefs && Object.keys(settings.githubTokenRefs).length > 0 || config.githubTokenRefs && Object.keys(config.githubTokenRefs).length > 0
9924
10086
  );
9925
10087
  }
10088
+ function getSavedGitHubTokenRef(settings, companyId) {
10089
+ if (!companyId) {
10090
+ return void 0;
10091
+ }
10092
+ return normalizeSecretRef(settings?.githubTokenRefs?.[companyId]);
10093
+ }
10094
+ function getConfiguredGitHubTokenRef(config, companyId) {
10095
+ if (!companyId) {
10096
+ return void 0;
10097
+ }
10098
+ return normalizeSecretRef(config?.githubTokenRefs?.[companyId]);
10099
+ }
9926
10100
  function getSavedPaperclipBoardApiTokenRef(settings, companyId) {
9927
10101
  if (!companyId) {
9928
10102
  return void 0;
@@ -10006,7 +10180,23 @@ async function resolveGithubToken(ctx, options = {}) {
10006
10180
  const config = options.config ?? await getResolvedConfig(ctx);
10007
10181
  const configuredTokenSource = getConfiguredGithubTokenSource(settings, config, options.companyId);
10008
10182
  if (configuredTokenSource.secretRef) {
10009
- return ctx.secrets.resolve(configuredTokenSource.secretRef);
10183
+ try {
10184
+ const token = (await ctx.secrets.resolve(configuredTokenSource.secretRef)).trim();
10185
+ if (token) {
10186
+ return token;
10187
+ }
10188
+ return configuredTokenSource.fallbackToken ?? "";
10189
+ } catch (error) {
10190
+ if (configuredTokenSource.fallbackToken && isPluginSecretReferenceDisabledError(error)) {
10191
+ ctx.logger.warn("GitHub Sync is using a worker-local company token fallback because plugin secret refs are unavailable in this host.", {
10192
+ companyId: normalizeCompanyId(options.companyId),
10193
+ secretRef: configuredTokenSource.secretRef,
10194
+ error: getErrorMessage(error)
10195
+ });
10196
+ return configuredTokenSource.fallbackToken;
10197
+ }
10198
+ throw error;
10199
+ }
10010
10200
  }
10011
10201
  return configuredTokenSource.token ?? "";
10012
10202
  }
@@ -12888,19 +13078,24 @@ async function createProjectPullRequestPaperclipIssue(ctx, input) {
12888
13078
  };
12889
13079
  }
12890
13080
  const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
12891
- const createdIssue = await ctx.issues.create({
12892
- companyId: scope.companyId,
12893
- projectId: scope.projectId,
12894
- title: requestedTitle,
12895
- originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
12896
- originId: pullRequestUrl,
12897
- description: buildPaperclipIssueDescriptionFromPullRequest({
12898
- repository: scope.repository,
12899
- pullRequestNumber,
12900
- pullRequestUrl,
12901
- body: pullRequest.body
12902
- })
12903
- });
13081
+ const createdIssue = await ensurePaperclipIssueStandardWorkMode(
13082
+ ctx,
13083
+ await ctx.issues.create({
13084
+ companyId: scope.companyId,
13085
+ projectId: scope.projectId,
13086
+ title: requestedTitle,
13087
+ status: "todo",
13088
+ originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
13089
+ originId: pullRequestUrl,
13090
+ description: buildPaperclipIssueDescriptionFromPullRequest({
13091
+ repository: scope.repository,
13092
+ pullRequestNumber,
13093
+ pullRequestUrl,
13094
+ body: pullRequest.body
13095
+ })
13096
+ }),
13097
+ scope.companyId
13098
+ );
12904
13099
  const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
12905
13100
  await upsertGitHubPullRequestLinkRecord(ctx, {
12906
13101
  companyId: scope.companyId,
@@ -15459,6 +15654,7 @@ var __testing = {
15459
15654
  formatPaperclipApiFetchErrorMessage,
15460
15655
  hasUnresolvedPaperclipIssueBlocker,
15461
15656
  isHealthyMaintainerWaitTransition,
15657
+ resolveGithubToken,
15462
15658
  resolvePaperclipPullRequestIssueStatus,
15463
15659
  resolveSyncTransitionAssignee
15464
15660
  };
@@ -15475,6 +15671,8 @@ var plugin = definePlugin({
15475
15671
  const normalizedSettings = normalizeSettings(saved);
15476
15672
  const config = await getResolvedConfig(ctx);
15477
15673
  const githubTokenConfigured = hasConfiguredGithubToken(normalizedSettings, config, requestedCompanyId);
15674
+ const configuredGitHubTokenRef = getConfiguredGitHubTokenRef(config, requestedCompanyId);
15675
+ const savedGitHubTokenRef = getSavedGitHubTokenRef(normalizedSettings, requestedCompanyId);
15478
15676
  const configuredBoardTokenRef = getConfiguredPaperclipBoardApiTokenRef(config, requestedCompanyId);
15479
15677
  const savedBoardTokenRef = getSavedPaperclipBoardApiTokenRef(normalizedSettings, requestedCompanyId);
15480
15678
  const settingsForResponse = sanitizeSettingsForCurrentSetup(
@@ -15496,6 +15694,8 @@ var plugin = definePlugin({
15496
15694
  paperclipApiBaseUrlConfigured: Boolean(normalizePaperclipApiBaseUrl(config.paperclipApiBaseUrl)),
15497
15695
  githubTokenConfigured,
15498
15696
  paperclipBoardAccessConfigured: requestedCompanyId ? hasConfiguredPaperclipBoardAccess(settingsForResponse, config, requestedCompanyId) : hasConfiguredPaperclipBoardAccessForMappings(settingsForResponse, config, scopedMappings),
15697
+ ...savedGitHubTokenRef ? { githubTokenConfigSyncRef: savedGitHubTokenRef } : {},
15698
+ githubTokenNeedsConfigSync: Boolean(savedGitHubTokenRef && configuredGitHubTokenRef !== savedGitHubTokenRef),
15499
15699
  ...savedBoardTokenRef ? { paperclipBoardAccessConfigSyncRef: savedBoardTokenRef } : {},
15500
15700
  paperclipBoardAccessNeedsConfigSync: Boolean(savedBoardTokenRef && !configuredBoardTokenRef)
15501
15701
  };
@@ -15762,6 +15962,44 @@ var plugin = definePlugin({
15762
15962
  }
15763
15963
  return validateGithubToken(ctx, trimmedToken);
15764
15964
  });
15965
+ ctx.actions.register("settings.ensureGitHubTokenAvailable", async (input) => {
15966
+ const record = input && typeof input === "object" ? input : {};
15967
+ const companyId = normalizeCompanyId(record.companyId);
15968
+ const githubTokenRef = normalizeSecretRef(record.githubTokenRef);
15969
+ const token = normalizeGitHubToken(record.token);
15970
+ if (!companyId) {
15971
+ throw new Error("Company context is required to verify worker access to the GitHub token.");
15972
+ }
15973
+ if (!githubTokenRef) {
15974
+ throw new Error("A GitHub token secret ref is required to verify worker token access.");
15975
+ }
15976
+ if (!token) {
15977
+ throw new Error("A validated GitHub token is required to prepare the worker token fallback.");
15978
+ }
15979
+ try {
15980
+ const resolvedToken = (await ctx.secrets.resolve(githubTokenRef)).trim();
15981
+ if (resolvedToken) {
15982
+ return {
15983
+ secretResolvable: true,
15984
+ fallbackStored: false
15985
+ };
15986
+ }
15987
+ } catch (error) {
15988
+ if (!isPluginSecretReferenceDisabledError(error)) {
15989
+ throw error;
15990
+ }
15991
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
15992
+ return {
15993
+ secretResolvable: false,
15994
+ fallbackStored: true
15995
+ };
15996
+ }
15997
+ await writeExternalCompanyGitHubTokenFallback(ctx, companyId, token);
15998
+ return {
15999
+ secretResolvable: false,
16000
+ fallbackStored: true
16001
+ };
16002
+ });
15765
16003
  ctx.actions.register("project.pullRequests.createIssue", async (input) => {
15766
16004
  const record = input && typeof input === "object" ? input : {};
15767
16005
  return createProjectPullRequestPaperclipIssue(ctx, record);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.8.12",
3
+ "version": "0.9.1",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
- "packageManager": "pnpm@10.33.2",
7
+ "packageManager": "pnpm@11.0.9",
8
8
  "engines": {
9
9
  "node": ">=20"
10
10
  },
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@octokit/rest": "^22.0.1",
44
- "@paperclipai/plugin-sdk": "^2026.428.0",
44
+ "@paperclipai/plugin-sdk": "^2026.512.0",
45
45
  "react": "^19.2.6",
46
46
  "react-markdown": "^10.1.0",
47
47
  "rehype-raw": "^7.0.0",