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/README.md +11 -7
- package/dist/manifest.js +1 -1
- package/dist/ui/index.js +100 -2
- package/dist/ui/index.js.map +2 -2
- package/dist/worker.js +331 -93
- package/package.json +3 -3
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 =
|
|
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
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
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
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
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
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
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
|
-
|
|
9759
|
-
|
|
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
|
|
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 =
|
|
9940
|
+
let nextStatus = resolvePaperclipDirectPullRequestIssueStatus({
|
|
9777
9941
|
currentStatus: paperclipIssue.status,
|
|
9778
|
-
|
|
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:
|
|
9970
|
+
repositoryUrl: primaryRepository?.url,
|
|
9803
9971
|
githubIssueNumber: void 0
|
|
9804
9972
|
});
|
|
9805
9973
|
await updatePaperclipIssueState(ctx, {
|
|
9806
9974
|
companyId: mapping.companyId,
|
|
9807
|
-
issueId:
|
|
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
|
-
|
|
9990
|
+
pullRequests: pullRequestSnapshots
|
|
9823
9991
|
});
|
|
9824
9992
|
updateSyncFailureContext(syncFailureContext, {
|
|
9825
9993
|
phase: "updating_paperclip_status",
|
|
9826
|
-
repositoryUrl:
|
|
9994
|
+
repositoryUrl: primaryRepository?.url,
|
|
9827
9995
|
githubIssueNumber: void 0
|
|
9828
9996
|
});
|
|
9829
9997
|
await updatePaperclipIssueState(ctx, {
|
|
9830
9998
|
companyId: mapping.companyId,
|
|
9831
|
-
issueId:
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
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.
|
|
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@
|
|
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.
|
|
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",
|