paperclip-github-plugin 0.8.11 → 0.9.0
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 +6 -6
- package/dist/manifest.js +1 -1
- package/dist/worker.js +183 -110
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The plugin adds a full in-host workflow instead of a one-off import script:
|
|
|
44
44
|
2. Connect one or more GitHub repositories to Paperclip projects.
|
|
45
45
|
3. Run a sync manually or let the scheduled job keep things up to date.
|
|
46
46
|
|
|
47
|
-
During sync, the plugin imports one top-level Paperclip issue per GitHub issue, stamps it with a namespaced GitHub Sync plugin origin, updates already imported issues instead of recreating them, maps GitHub labels into Paperclip labels, and keeps GitHub-specific metadata in dedicated Paperclip surfaces rather than stuffing everything into the issue description. On Paperclip `2026.
|
|
47
|
+
During sync, the plugin imports one top-level Paperclip issue per GitHub issue, stamps it with a namespaced GitHub Sync plugin origin, updates already imported issues instead of recreating them, maps GitHub labels into Paperclip labels, and keeps GitHub-specific metadata in dedicated Paperclip surfaces rather than stuffing everything into the issue description. On Paperclip `2026.512.0` and newer, the plugin passes the intended initial import status explicitly so Paperclip's assigned-issue creation defaults cannot override GitHub Sync's routing, and the detail surfaces can recover GitHub issue and pull request links from Paperclip's own `originKind` / `originId` fields when the plugin registry or legacy hidden marker is missing.
|
|
48
48
|
|
|
49
49
|
When the host exposes plugin issue creation, imported GitHub issues are created through the Paperclip plugin SDK path so they are not attributed to the connected board user. The worker still uses direct local Paperclip REST calls for label sync and for description, assignee, or status repair paths when those routes are available.
|
|
50
50
|
|
|
@@ -94,7 +94,7 @@ If a Paperclip issue was created locally or by an agent workflow before GitHub S
|
|
|
94
94
|
|
|
95
95
|
Manual GitHub issue links are added to the same import registry and issue-link entity used by normal sync, so future syncs update the Paperclip issue from the GitHub issue. Manual pull request links are added to the PR-link entity used by the project Pull Requests page, so future syncs monitor PR status even when there is no closing GitHub issue.
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Operators can unlink a linked Paperclip issue from the GitHub detail surface when they intentionally want GitHub Sync to stop updating it. Agent-facing tools and native agent API routes can create durable issue and pull request links, but they do not expose an unlink operation; internal sync repair may still tombstone a link when GitHub transfers an issue to an unmapped repository.
|
|
98
98
|
|
|
99
99
|
### Agent workflows built in
|
|
100
100
|
|
|
@@ -104,7 +104,7 @@ They can also link a Paperclip issue to a GitHub issue or pull request in any ac
|
|
|
104
104
|
## Requirements
|
|
105
105
|
|
|
106
106
|
- Node.js 20+
|
|
107
|
-
- a Paperclip host with plugin installation enabled. GitHub Sync is built and tested against Paperclip `2026.
|
|
107
|
+
- a Paperclip host with plugin installation enabled. GitHub Sync is built and tested against Paperclip `2026.512.0`; the manifest relies on explicit capabilities instead of a strict host-version gate because current latest/development hosts can report `0.0.0` during plugin upgrade.
|
|
108
108
|
- a GitHub token with API access to the repositories you want to sync
|
|
109
109
|
|
|
110
110
|
## Install from npm
|
|
@@ -219,7 +219,7 @@ Notes:
|
|
|
219
219
|
|
|
220
220
|
GitHub Sync uses direct worker-side Paperclip REST calls for host paths that are not fully covered by the plugin SDK, such as label reconciliation and some issue repair paths. By default, manual setup and sync actions use the current browser origin. If that origin is not reachable from the plugin worker, set **Worker Paperclip API URL** in GitHub Sync settings; the value is saved internally as plugin config `paperclipApiBaseUrl`.
|
|
221
221
|
|
|
222
|
-
For private LAN, Docker, Kubernetes, custom DNS, or self-signed-certificate deployments, set that field to a local route the plugin worker can reach, such as `http://localhost:3100`, and port-forward or route that address to the Paperclip API when needed. Do not rely on exporting a process environment variable named `PAPERCLIP_API_URL` to Paperclip;
|
|
222
|
+
For private LAN, Docker, Kubernetes, custom DNS, or self-signed-certificate deployments, set that field to a local route the plugin worker can reach, such as `http://localhost:3100`, and port-forward or route that address to the Paperclip API when needed. Do not rely on exporting a process environment variable named `PAPERCLIP_API_URL` to Paperclip; release-target verification keeps this as explicit plugin configuration because worker runtime environments do not guarantee that process variable.
|
|
223
223
|
|
|
224
224
|
## GitHub agent tools
|
|
225
225
|
|
|
@@ -354,8 +354,8 @@ Useful scripts:
|
|
|
354
354
|
|
|
355
355
|
- `pnpm dev` watches the manifest, worker, and UI bundles and rebuilds them into `dist/`
|
|
356
356
|
- `pnpm dev:ui` starts a local Paperclip plugin UI dev server from `dist/ui` on port `4177`
|
|
357
|
-
- `pnpm test:e2e` builds the plugin, boots an isolated Paperclip `2026.
|
|
358
|
-
- `pnpm verify:manual` builds the plugin, boots a local-trusted Paperclip `2026.
|
|
357
|
+
- `pnpm test:e2e` builds the plugin, boots an isolated Paperclip `2026.512.0` instance, installs the plugin, and verifies the hosted settings page renders
|
|
358
|
+
- `pnpm verify:manual` builds the plugin, boots a local-trusted Paperclip `2026.512.0` instance for manual inspection, seeds a `Dummy Company` with a mapped review project and a `CEO` agent on the Codex local adapter using model `gpt-5.4`, installs the plugin, and opens the company dashboard without seeding KPI history.
|
|
359
359
|
|
|
360
360
|
For fast hosted UI iteration, run `pnpm dev` in one terminal and `pnpm dev:ui` in another.
|
|
361
361
|
|
package/dist/manifest.js
CHANGED
|
@@ -642,7 +642,7 @@ var PULL_REQUEST_ASSET_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_I
|
|
|
642
642
|
var require2 = createRequire(import.meta.url);
|
|
643
643
|
var packageJson = require2("../package.json");
|
|
644
644
|
var SCHEDULE_TICK_CRON = "* * * * *";
|
|
645
|
-
var MANIFEST_VERSION = "0.
|
|
645
|
+
var MANIFEST_VERSION = "0.9.0"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
|
|
646
646
|
var manifest = {
|
|
647
647
|
id: GITHUB_SYNC_PLUGIN_ID,
|
|
648
648
|
apiVersion: 1,
|
package/dist/worker.js
CHANGED
|
@@ -2857,21 +2857,10 @@ async function buildToolbarSyncState(ctx, input) {
|
|
|
2857
2857
|
if (entityType === "issue" && entityId && companyId) {
|
|
2858
2858
|
const link = await resolvePaperclipIssueGitHubLink(ctx, entityId, companyId);
|
|
2859
2859
|
if (link) {
|
|
2860
|
-
const mappings2 = getSyncableMappingsForTarget(settings.mappings, {
|
|
2861
|
-
kind: "issue",
|
|
2862
|
-
companyId,
|
|
2863
|
-
projectId: link.paperclipProjectId,
|
|
2864
|
-
issueId: entityId,
|
|
2865
|
-
repositoryUrl: link.repositoryUrl,
|
|
2866
|
-
githubIssueId: link.githubIssueId,
|
|
2867
|
-
githubIssueNumber: link.githubIssueNumber,
|
|
2868
|
-
githubIssueUrl: link.githubIssueUrl,
|
|
2869
|
-
displayLabel: `issue #${link.githubIssueNumber}`
|
|
2870
|
-
});
|
|
2871
2860
|
return {
|
|
2872
2861
|
kind: "issue",
|
|
2873
2862
|
visible: false,
|
|
2874
|
-
canRun: githubTokenConfigured
|
|
2863
|
+
canRun: githubTokenConfigured,
|
|
2875
2864
|
label: `Sync #${link.githubIssueNumber}`,
|
|
2876
2865
|
message: `Sync ${link.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} issue #${link.githubIssueNumber}.`,
|
|
2877
2866
|
syncState: settings.syncState,
|
|
@@ -2880,20 +2869,10 @@ async function buildToolbarSyncState(ctx, input) {
|
|
|
2880
2869
|
};
|
|
2881
2870
|
}
|
|
2882
2871
|
const pullRequestLink = await resolvePaperclipIssueGitHubPullRequestLink(ctx, entityId, companyId);
|
|
2883
|
-
const mappings = pullRequestLink ? getSyncableMappingsForTarget(settings.mappings, {
|
|
2884
|
-
kind: "issue",
|
|
2885
|
-
companyId,
|
|
2886
|
-
projectId: pullRequestLink.data.paperclipProjectId,
|
|
2887
|
-
issueId: entityId,
|
|
2888
|
-
repositoryUrl: pullRequestLink.data.repositoryUrl,
|
|
2889
|
-
githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
|
|
2890
|
-
githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
|
|
2891
|
-
displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
|
|
2892
|
-
}) : [];
|
|
2893
2872
|
return {
|
|
2894
2873
|
kind: "issue",
|
|
2895
2874
|
visible: false,
|
|
2896
|
-
canRun: githubTokenConfigured &&
|
|
2875
|
+
canRun: githubTokenConfigured && Boolean(pullRequestLink),
|
|
2897
2876
|
label: pullRequestLink?.data.githubPullRequestNumber ? `Sync PR #${pullRequestLink.data.githubPullRequestNumber}` : "Sync issue",
|
|
2898
2877
|
message: pullRequestLink ? `Sync ${pullRequestLink.data.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} pull request #${pullRequestLink.data.githubPullRequestNumber}.` : "This Paperclip issue is not linked to GitHub yet.",
|
|
2899
2878
|
syncState: settings.syncState,
|
|
@@ -5702,9 +5681,19 @@ function buildPaperclipIssueStatusTransitionComment(params) {
|
|
|
5702
5681
|
};
|
|
5703
5682
|
}
|
|
5704
5683
|
function buildPaperclipPullRequestIssueStatusTransitionComment(params) {
|
|
5705
|
-
const reason =
|
|
5684
|
+
const reason = describeGitHubDirectPullRequestIssueStatusReason(params.pullRequests);
|
|
5706
5685
|
return `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(params.previousStatus)}\` to \`${formatPaperclipIssueStatus(params.nextStatus)}\` because ${reason}.`;
|
|
5707
5686
|
}
|
|
5687
|
+
function describeGitHubDirectPullRequestIssueStatusReason(pullRequests) {
|
|
5688
|
+
const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
|
|
5689
|
+
if (openPullRequests.length > 0) {
|
|
5690
|
+
return describeGitHubLinkedPullRequestsStatusReason(openPullRequests);
|
|
5691
|
+
}
|
|
5692
|
+
if (pullRequests.some((entry) => entry.lifecycleState === "merged")) {
|
|
5693
|
+
return pullRequests.length === 1 ? "the linked pull request was merged" : "at least one linked pull request was merged";
|
|
5694
|
+
}
|
|
5695
|
+
return pullRequests.length === 1 ? "the linked pull request was closed without merge" : "all linked pull requests were closed without merge";
|
|
5696
|
+
}
|
|
5708
5697
|
function resolvePaperclipIssueStatus(params) {
|
|
5709
5698
|
const {
|
|
5710
5699
|
currentStatus,
|
|
@@ -5757,6 +5746,23 @@ function resolvePaperclipPullRequestIssueStatus(params) {
|
|
|
5757
5746
|
preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
|
|
5758
5747
|
});
|
|
5759
5748
|
}
|
|
5749
|
+
function resolvePaperclipDirectPullRequestIssueStatus(params) {
|
|
5750
|
+
const { currentStatus, pullRequests, hasExecutorHandoffTarget } = params;
|
|
5751
|
+
const openPullRequests = pullRequests.map((entry) => entry.pullRequest).filter((pullRequest) => Boolean(pullRequest));
|
|
5752
|
+
if (openPullRequests.length > 0) {
|
|
5753
|
+
if (shouldPreserveBlockedExternalPullRequestWait({
|
|
5754
|
+
currentStatus,
|
|
5755
|
+
linkedPullRequests: openPullRequests
|
|
5756
|
+
})) {
|
|
5757
|
+
return "blocked";
|
|
5758
|
+
}
|
|
5759
|
+
return resolvePaperclipStatusFromLinkedPullRequests(openPullRequests, {
|
|
5760
|
+
preferInProgress: hasExecutorHandoffTarget,
|
|
5761
|
+
preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
return pullRequests.some((entry) => entry.lifecycleState === "merged") ? "done" : "cancelled";
|
|
5765
|
+
}
|
|
5760
5766
|
async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
|
|
5761
5767
|
const linkedPullRequests = [];
|
|
5762
5768
|
const seenPullRequestKeys = /* @__PURE__ */ new Set();
|
|
@@ -8996,6 +9002,16 @@ async function assignImportedPaperclipIssueToUser(ctx, params) {
|
|
|
8996
9002
|
});
|
|
8997
9003
|
}
|
|
8998
9004
|
}
|
|
9005
|
+
async function ensurePaperclipIssueStandardWorkMode(ctx, issue, companyId) {
|
|
9006
|
+
if (!("workMode" in issue) || issue.workMode === "standard") {
|
|
9007
|
+
return issue;
|
|
9008
|
+
}
|
|
9009
|
+
return ctx.issues.update(
|
|
9010
|
+
issue.id,
|
|
9011
|
+
{ workMode: "standard" },
|
|
9012
|
+
companyId
|
|
9013
|
+
);
|
|
9014
|
+
}
|
|
8999
9015
|
async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, availableLabels, paperclipApiBaseUrl, syncFailureContext) {
|
|
9000
9016
|
if (!mapping.companyId || !mapping.paperclipProjectId) {
|
|
9001
9017
|
throw new Error(`Mapping ${mapping.id} is missing resolved Paperclip project identifiers.`);
|
|
@@ -9003,15 +9019,20 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
|
|
|
9003
9019
|
const title = issue.title;
|
|
9004
9020
|
const description = buildPaperclipIssueDescription(issue);
|
|
9005
9021
|
const defaultAssignee = getConfiguredAdvancedAssigneePrincipal(advancedSettings, "default");
|
|
9006
|
-
const createdIssue = await
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9022
|
+
const createdIssue = await ensurePaperclipIssueStandardWorkMode(
|
|
9023
|
+
ctx,
|
|
9024
|
+
await ctx.issues.create({
|
|
9025
|
+
companyId: mapping.companyId,
|
|
9026
|
+
projectId: mapping.paperclipProjectId,
|
|
9027
|
+
title,
|
|
9028
|
+
...description ? { description } : {},
|
|
9029
|
+
status: advancedSettings.defaultStatus,
|
|
9030
|
+
originKind: GITHUB_ISSUE_ORIGIN_KIND,
|
|
9031
|
+
originId: normalizeGitHubIssueHtmlUrl(issue.htmlUrl) ?? issue.htmlUrl,
|
|
9032
|
+
...defaultAssignee?.kind === "agent" ? { assigneeAgentId: defaultAssignee.id } : defaultAssignee?.kind === "user" ? { assigneeUserId: defaultAssignee.id } : {}
|
|
9033
|
+
}),
|
|
9034
|
+
mapping.companyId
|
|
9035
|
+
);
|
|
9015
9036
|
const ensuredCreatedIssueId = createdIssue.id;
|
|
9016
9037
|
const normalizedCreatedIssueDescription = createdIssue.description ?? void 0;
|
|
9017
9038
|
const createPath = "sdk";
|
|
@@ -9729,6 +9750,18 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9729
9750
|
updatedDescriptionsCount
|
|
9730
9751
|
};
|
|
9731
9752
|
}
|
|
9753
|
+
function groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks) {
|
|
9754
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9755
|
+
const sortedLinks = [...pullRequestLinks].sort(
|
|
9756
|
+
(left, right) => left.paperclipIssueId.localeCompare(right.paperclipIssueId) || left.data.repositoryUrl.localeCompare(right.data.repositoryUrl) || left.data.githubPullRequestNumber - right.data.githubPullRequestNumber
|
|
9757
|
+
);
|
|
9758
|
+
for (const pullRequestLink of sortedLinks) {
|
|
9759
|
+
const group = groups.get(pullRequestLink.paperclipIssueId) ?? [];
|
|
9760
|
+
group.push(pullRequestLink);
|
|
9761
|
+
groups.set(pullRequestLink.paperclipIssueId, group);
|
|
9762
|
+
}
|
|
9763
|
+
return groups;
|
|
9764
|
+
}
|
|
9732
9765
|
async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
9733
9766
|
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
9734
9767
|
return {
|
|
@@ -9740,52 +9773,93 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9740
9773
|
const mappingCompanyId = mapping.companyId;
|
|
9741
9774
|
const mappingProjectId = mapping.paperclipProjectId;
|
|
9742
9775
|
const totalIssueCount = pullRequestLinks.length;
|
|
9776
|
+
const pullRequestLinkGroups = groupGitHubPullRequestLinksByPaperclipIssue(pullRequestLinks);
|
|
9743
9777
|
const queuedIssueWakeups = [];
|
|
9744
|
-
for (const
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
9756
|
-
owner: pullRequestRepository.owner,
|
|
9757
|
-
repo: pullRequestRepository.repo,
|
|
9758
|
-
pull_number: pullRequestLink.data.githubPullRequestNumber,
|
|
9759
|
-
headers: {
|
|
9760
|
-
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
9761
|
-
}
|
|
9762
|
-
});
|
|
9763
|
-
const livePullRequestState = getPullRequestApiState({
|
|
9764
|
-
state: pullRequestResponse.data.state,
|
|
9765
|
-
merged: pullRequestResponse.data.merged
|
|
9766
|
-
}) === "open" ? "open" : "closed";
|
|
9767
|
-
if (livePullRequestState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
|
|
9768
|
-
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
9769
|
-
companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
|
|
9770
|
-
projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
|
|
9771
|
-
issueId: pullRequestLink.paperclipIssueId,
|
|
9778
|
+
for (const [paperclipIssueId, issuePullRequestLinks] of pullRequestLinkGroups.entries()) {
|
|
9779
|
+
const pullRequestSnapshots = [];
|
|
9780
|
+
let groupHadFailure = false;
|
|
9781
|
+
for (const pullRequestLink of issuePullRequestLinks) {
|
|
9782
|
+
if (assertNotCancelled) {
|
|
9783
|
+
await assertNotCancelled();
|
|
9784
|
+
}
|
|
9785
|
+
try {
|
|
9786
|
+
const pullRequestRepository = requireRepositoryReference(pullRequestLink.data.repositoryUrl);
|
|
9787
|
+
updateSyncFailureContext(syncFailureContext, {
|
|
9788
|
+
phase: "evaluating_github_status",
|
|
9772
9789
|
repositoryUrl: pullRequestRepository.url,
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9790
|
+
githubIssueNumber: void 0
|
|
9791
|
+
});
|
|
9792
|
+
const pullRequestResponse = await octokit.rest.pulls.get({
|
|
9793
|
+
owner: pullRequestRepository.owner,
|
|
9794
|
+
repo: pullRequestRepository.repo,
|
|
9795
|
+
pull_number: pullRequestLink.data.githubPullRequestNumber,
|
|
9796
|
+
headers: {
|
|
9797
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
9798
|
+
}
|
|
9777
9799
|
});
|
|
9800
|
+
const livePullRequestLifecycleState = getPullRequestApiState({
|
|
9801
|
+
state: pullRequestResponse.data.state,
|
|
9802
|
+
merged: pullRequestResponse.data.merged
|
|
9803
|
+
});
|
|
9804
|
+
const livePullRequestLinkState = livePullRequestLifecycleState === "open" ? "open" : "closed";
|
|
9805
|
+
if (livePullRequestLinkState !== pullRequestLink.data.githubPullRequestState || pullRequestResponse.data.html_url !== pullRequestLink.data.githubPullRequestUrl || pullRequestResponse.data.title !== pullRequestLink.data.title) {
|
|
9806
|
+
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
9807
|
+
companyId: pullRequestLink.data.companyId ?? mappingCompanyId,
|
|
9808
|
+
projectId: pullRequestLink.data.paperclipProjectId ?? mappingProjectId,
|
|
9809
|
+
issueId: pullRequestLink.paperclipIssueId,
|
|
9810
|
+
repositoryUrl: pullRequestRepository.url,
|
|
9811
|
+
pullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
|
|
9812
|
+
pullRequestUrl: pullRequestResponse.data.html_url ?? pullRequestLink.data.githubPullRequestUrl,
|
|
9813
|
+
pullRequestTitle: pullRequestResponse.data.title || pullRequestLink.data.title || `Pull request #${pullRequestLink.data.githubPullRequestNumber}`,
|
|
9814
|
+
pullRequestState: livePullRequestLinkState
|
|
9815
|
+
});
|
|
9816
|
+
}
|
|
9817
|
+
if (livePullRequestLifecycleState === "open") {
|
|
9818
|
+
const pullRequest = await getGitHubPullRequestStatusSnapshot(
|
|
9819
|
+
octokit,
|
|
9820
|
+
pullRequestRepository,
|
|
9821
|
+
pullRequestLink.data.githubPullRequestNumber,
|
|
9822
|
+
pullRequestStatusCache
|
|
9823
|
+
);
|
|
9824
|
+
pullRequestSnapshots.push({
|
|
9825
|
+
pullRequestLink,
|
|
9826
|
+
repository: pullRequestRepository,
|
|
9827
|
+
lifecycleState: "open",
|
|
9828
|
+
pullRequest
|
|
9829
|
+
});
|
|
9830
|
+
} else {
|
|
9831
|
+
pullRequestSnapshots.push({
|
|
9832
|
+
pullRequestLink,
|
|
9833
|
+
repository: pullRequestRepository,
|
|
9834
|
+
lifecycleState: livePullRequestLifecycleState
|
|
9835
|
+
});
|
|
9836
|
+
}
|
|
9837
|
+
} catch (error) {
|
|
9838
|
+
if (isGitHubRateLimitError(error)) {
|
|
9839
|
+
throw error;
|
|
9840
|
+
}
|
|
9841
|
+
groupHadFailure = true;
|
|
9842
|
+
recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
|
|
9843
|
+
} finally {
|
|
9844
|
+
completedIssueCount += 1;
|
|
9845
|
+
if (onProgress) {
|
|
9846
|
+
await onProgress({
|
|
9847
|
+
pullRequestLink,
|
|
9848
|
+
completedIssueCount,
|
|
9849
|
+
totalIssueCount
|
|
9850
|
+
});
|
|
9851
|
+
}
|
|
9778
9852
|
}
|
|
9779
|
-
|
|
9780
|
-
|
|
9853
|
+
}
|
|
9854
|
+
if (groupHadFailure || pullRequestSnapshots.length === 0) {
|
|
9855
|
+
continue;
|
|
9856
|
+
}
|
|
9857
|
+
const primaryRepository = pullRequestSnapshots[0]?.repository;
|
|
9858
|
+
try {
|
|
9859
|
+
if (assertNotCancelled) {
|
|
9860
|
+
await assertNotCancelled();
|
|
9781
9861
|
}
|
|
9782
|
-
const
|
|
9783
|
-
octokit,
|
|
9784
|
-
pullRequestRepository,
|
|
9785
|
-
pullRequestLink.data.githubPullRequestNumber,
|
|
9786
|
-
pullRequestStatusCache
|
|
9787
|
-
);
|
|
9788
|
-
const paperclipIssue = await ctx.issues.get(pullRequestLink.paperclipIssueId, mapping.companyId);
|
|
9862
|
+
const paperclipIssue = await ctx.issues.get(paperclipIssueId, mapping.companyId);
|
|
9789
9863
|
if (!paperclipIssue) {
|
|
9790
9864
|
continue;
|
|
9791
9865
|
}
|
|
@@ -9794,12 +9868,12 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9794
9868
|
paperclipIssueSyncContext,
|
|
9795
9869
|
advancedSettings
|
|
9796
9870
|
);
|
|
9797
|
-
let nextStatus =
|
|
9871
|
+
let nextStatus = resolvePaperclipDirectPullRequestIssueStatus({
|
|
9798
9872
|
currentStatus: paperclipIssue.status,
|
|
9799
|
-
|
|
9873
|
+
pullRequests: pullRequestSnapshots,
|
|
9800
9874
|
hasExecutorHandoffTarget: Boolean(executorTransitionAssignee)
|
|
9801
9875
|
});
|
|
9802
|
-
if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
|
|
9876
|
+
if (paperclipIssue.status === "blocked" && nextStatus !== "blocked" && pullRequestSnapshots.some((entry) => entry.lifecycleState === "open") && await hasUnresolvedPaperclipIssueBlocker(ctx, paperclipIssue, mapping.companyId)) {
|
|
9803
9877
|
nextStatus = "blocked";
|
|
9804
9878
|
}
|
|
9805
9879
|
const shouldPreserveMaintainerWaitRouting = isHealthyMaintainerWaitTransition({
|
|
@@ -9807,6 +9881,10 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9807
9881
|
nextStatus,
|
|
9808
9882
|
syncContext: paperclipIssueSyncContext
|
|
9809
9883
|
});
|
|
9884
|
+
const shouldClearCompletedExecutionPolicy = shouldClearCompletedSyncExecutionPolicy({
|
|
9885
|
+
nextStatus,
|
|
9886
|
+
syncContext: paperclipIssueSyncContext
|
|
9887
|
+
});
|
|
9810
9888
|
const nextTransitionAssignee = resolveSyncTransitionAssignee({
|
|
9811
9889
|
currentStatus: paperclipIssue.status,
|
|
9812
9890
|
nextStatus,
|
|
@@ -9817,20 +9895,20 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9817
9895
|
const nextAssigneeChanged = nextTransitionAssignee ? !doesPaperclipIssueAssigneeMatch(paperclipIssueSyncContext.assignee, nextTransitionAssignee.principal) : false;
|
|
9818
9896
|
const shouldWakeTransitionAssignee = paperclipIssue.status !== nextStatus && nextTransitionAssignee?.principal.kind === "agent" && isActionablePaperclipIssueStatus(nextStatus) && (nextAssigneeChanged || paperclipIssue.status !== nextStatus);
|
|
9819
9897
|
if (paperclipIssue.status === nextStatus) {
|
|
9820
|
-
if (shouldClearTransitionAssignee) {
|
|
9898
|
+
if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
|
|
9821
9899
|
updateSyncFailureContext(syncFailureContext, {
|
|
9822
9900
|
phase: "updating_paperclip_status",
|
|
9823
|
-
repositoryUrl:
|
|
9901
|
+
repositoryUrl: primaryRepository?.url,
|
|
9824
9902
|
githubIssueNumber: void 0
|
|
9825
9903
|
});
|
|
9826
9904
|
await updatePaperclipIssueState(ctx, {
|
|
9827
9905
|
companyId: mapping.companyId,
|
|
9828
|
-
issueId:
|
|
9906
|
+
issueId: paperclipIssueId,
|
|
9829
9907
|
currentStatus: paperclipIssue.status,
|
|
9830
9908
|
syncContext: paperclipIssueSyncContext,
|
|
9831
9909
|
nextStatus,
|
|
9832
|
-
clearAssignee: true,
|
|
9833
|
-
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
9910
|
+
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
9911
|
+
...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
|
|
9834
9912
|
transitionComment: "",
|
|
9835
9913
|
paperclipApiBaseUrl
|
|
9836
9914
|
});
|
|
@@ -9840,22 +9918,22 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9840
9918
|
const transitionComment = buildPaperclipPullRequestIssueStatusTransitionComment({
|
|
9841
9919
|
previousStatus: paperclipIssue.status,
|
|
9842
9920
|
nextStatus,
|
|
9843
|
-
|
|
9921
|
+
pullRequests: pullRequestSnapshots
|
|
9844
9922
|
});
|
|
9845
9923
|
updateSyncFailureContext(syncFailureContext, {
|
|
9846
9924
|
phase: "updating_paperclip_status",
|
|
9847
|
-
repositoryUrl:
|
|
9925
|
+
repositoryUrl: primaryRepository?.url,
|
|
9848
9926
|
githubIssueNumber: void 0
|
|
9849
9927
|
});
|
|
9850
9928
|
await updatePaperclipIssueState(ctx, {
|
|
9851
9929
|
companyId: mapping.companyId,
|
|
9852
|
-
issueId:
|
|
9930
|
+
issueId: paperclipIssueId,
|
|
9853
9931
|
currentStatus: paperclipIssue.status,
|
|
9854
9932
|
syncContext: paperclipIssueSyncContext,
|
|
9855
9933
|
nextStatus,
|
|
9856
9934
|
...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
|
|
9857
9935
|
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
9858
|
-
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
9936
|
+
...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
|
|
9859
9937
|
transitionComment,
|
|
9860
9938
|
paperclipApiBaseUrl
|
|
9861
9939
|
});
|
|
@@ -9863,7 +9941,7 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9863
9941
|
if (shouldWakeTransitionAssignee && nextTransitionAssignee?.principal.kind === "agent") {
|
|
9864
9942
|
queuedIssueWakeups.push({
|
|
9865
9943
|
assigneeAgentId: nextTransitionAssignee.principal.id,
|
|
9866
|
-
paperclipIssueId
|
|
9944
|
+
paperclipIssueId,
|
|
9867
9945
|
reason: STATUS_TRANSITION_WAKE_REASON,
|
|
9868
9946
|
mutation: "status_transition",
|
|
9869
9947
|
previousStatus: paperclipIssue.status,
|
|
@@ -9875,16 +9953,6 @@ async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mappin
|
|
|
9875
9953
|
throw error;
|
|
9876
9954
|
}
|
|
9877
9955
|
recordRecoverableSyncFailure(ctx, failures, error, syncFailureContext);
|
|
9878
|
-
continue;
|
|
9879
|
-
} finally {
|
|
9880
|
-
completedIssueCount += 1;
|
|
9881
|
-
if (onProgress) {
|
|
9882
|
-
await onProgress({
|
|
9883
|
-
pullRequestLink,
|
|
9884
|
-
completedIssueCount,
|
|
9885
|
-
totalIssueCount
|
|
9886
|
-
});
|
|
9887
|
-
}
|
|
9888
9956
|
}
|
|
9889
9957
|
}
|
|
9890
9958
|
await mapWithConcurrency(
|
|
@@ -12909,19 +12977,24 @@ async function createProjectPullRequestPaperclipIssue(ctx, input) {
|
|
|
12909
12977
|
};
|
|
12910
12978
|
}
|
|
12911
12979
|
const requestedTitle = typeof input.title === "string" && input.title.trim() ? input.title.trim() : pullRequest.title.trim();
|
|
12912
|
-
const createdIssue = await
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12980
|
+
const createdIssue = await ensurePaperclipIssueStandardWorkMode(
|
|
12981
|
+
ctx,
|
|
12982
|
+
await ctx.issues.create({
|
|
12983
|
+
companyId: scope.companyId,
|
|
12984
|
+
projectId: scope.projectId,
|
|
12985
|
+
title: requestedTitle,
|
|
12986
|
+
status: "todo",
|
|
12987
|
+
originKind: GITHUB_PULL_REQUEST_ORIGIN_KIND,
|
|
12988
|
+
originId: pullRequestUrl,
|
|
12989
|
+
description: buildPaperclipIssueDescriptionFromPullRequest({
|
|
12990
|
+
repository: scope.repository,
|
|
12991
|
+
pullRequestNumber,
|
|
12992
|
+
pullRequestUrl,
|
|
12993
|
+
body: pullRequest.body
|
|
12994
|
+
})
|
|
12995
|
+
}),
|
|
12996
|
+
scope.companyId
|
|
12997
|
+
);
|
|
12925
12998
|
const resolvedIssue = await ctx.issues.get(createdIssue.id, scope.companyId) ?? createdIssue;
|
|
12926
12999
|
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
12927
13000
|
companyId: scope.companyId,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-github-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|