paperclip-github-plugin 0.8.6 → 0.8.8
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 +34 -4
- package/dist/manifest.js +60 -1
- package/dist/worker.js +702 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ If a company already has a Paperclip project bound to a GitHub repository worksp
|
|
|
62
62
|
|
|
63
63
|
### Status sync with delivery context
|
|
64
64
|
|
|
65
|
-
The plugin does more than mirror issue text. It looks at linked pull requests, mergeability, CI, review decisions, review threads, and trusted new GitHub comments so imported Paperclip issues can reflect where the work actually is. When GitHub links an issue to a pull request in another repository, GitHub Sync now follows that pull request's actual repository for status checks, review state, and deep links instead of assuming the issue repository. When sync closes an imported issue as `done` or `cancelled`, it also clears any pending Paperclip review or approval execution state so the host accepts the terminal transition cleanly.
|
|
65
|
+
The plugin does more than mirror issue text. It looks at linked pull requests, mergeability, CI, review decisions, review threads, and trusted new GitHub comments so imported Paperclip issues can reflect where the work actually is. When GitHub links an issue to a pull request in another repository, GitHub Sync now follows that pull request's actual repository for status checks, review state, and deep links instead of assuming the issue repository. When sync closes an imported issue as `done` or `cancelled`, it also clears any pending Paperclip review or approval execution policy/state so the host accepts the terminal transition cleanly and does not keep waking stale review participants.
|
|
66
66
|
|
|
67
67
|
### Company KPI dashboard
|
|
68
68
|
|
|
@@ -72,6 +72,12 @@ Because GitHub alone cannot tell which pull requests came from a Paperclip compa
|
|
|
72
72
|
|
|
73
73
|
That API route path matters on authenticated Paperclip deployments today because a current host bug blocks agents from calling plugin tools unless the instance runs in `local_trusted` mode. Those agents can still use `gh` with the propagated `GITHUB_TOKEN`, then call the agent-authenticated plugin API route from the shell after they create a PR. The Paperclip host authenticates `Authorization: Bearer <PAPERCLIP_API_KEY>`, scopes the request to the calling agent's company, and rejects anonymous or non-agent calls before dispatching to the worker.
|
|
74
74
|
|
|
75
|
+
### Third-party issue links
|
|
76
|
+
|
|
77
|
+
Sometimes a Paperclip issue is implemented through a GitHub issue or pull request in a repository that is not mapped to any Paperclip project. GitHub Sync can now track those targeted links without enrolling the whole repository in sync. The `link_github_item` agent tool records the durable link on local-trusted Paperclip instances, and authenticated agent runs can post the same link to `/api/plugins/paperclip-github-plugin/api/issue-link` with `PAPERCLIP_API_KEY` when plugin tools are blocked by host board-auth gates.
|
|
78
|
+
|
|
79
|
+
Third-party links are company-scoped and sync only the linked GitHub issue or pull request. Future manual or scheduled company sync runs refresh those external records and update the Paperclip issue status from the same CI, mergeability, review, thread, and issue-state rules used for mapped repositories.
|
|
80
|
+
|
|
75
81
|
### Project pull request command center
|
|
76
82
|
|
|
77
83
|
Each mapped project can expose a **Pull Requests** entry in the sidebar that opens a live GitHub queue page for that repository. The sidebar badge uses a lightweight total-count read, while the queue keeps the default view fast by loading only the current 10-row page, uses a repo-wide metrics read for the summary cards, reuses that cached metrics scan to keep filtered views fast by fetching only the visible filtered rows, keeps repo-scoped count, metrics, and per-PR review/check insight caches warm for repeat visits, lets operators explicitly bust those caches with Refresh when they want a live reread, shows total, mergeable, reviewable, and failing cards that filter the table, only treats a pull request as mergeable when it targets the current default branch with green checks, at least one approval, no outstanding change requests, and no unresolved review threads, includes an **Up to date** column that distinguishes current branches, clean update candidates, conflict cases, and unknown freshness when GitHub cannot confirm the comparison, shows the PR target branch with a highlighted default-branch badge, keeps the list sorted by most recently updated first, paginates larger repositories, keeps a compact bottom detail pane with markdown-and-HTML-rendered conversation plus an inline comment composer, supports deterministic **Update branch** actions for clean behind-base pull requests, adds Copilot quick actions that post `@copilot` requests for **Fix CI**, **Rebase**, and **Address review feedback**, requests Copilot through GitHub’s native reviewer flow for **Review**, keeps comment, review, quick approve/request-changes, re-run CI, merge, and close actions available, lets the review modal submit comment-only, approve, or request-changes reviews, hides any pull request action whose required GitHub permission is not verified for the saved token, and opens linked Paperclip issues in a plugin-provided right drawer so operators can stay on the queue page.
|
|
@@ -93,6 +99,7 @@ Linked Paperclip issues can also be unlinked from the GitHub detail surface. Unl
|
|
|
93
99
|
### Agent workflows built in
|
|
94
100
|
|
|
95
101
|
Paperclip agents can search GitHub for duplicates, read and update issues, assign issues to the saved token owner, post comments, create pull requests, inspect changed files and CI, reply to review threads, resolve or unresolve threads, request reviewers, search org-level GitHub Projects, and associate pull requests with those projects without leaving the Paperclip plugin surface.
|
|
102
|
+
They can also link a Paperclip issue to a GitHub issue or pull request in any accessible repository with `link_github_item`, including third-party repositories that are not mapped to a Paperclip project.
|
|
96
103
|
|
|
97
104
|
## Requirements
|
|
98
105
|
|
|
@@ -158,7 +165,7 @@ When the local Paperclip API is available, the plugin also syncs labels by name,
|
|
|
158
165
|
| --- | --- |
|
|
159
166
|
| Open issue with no linked pull request, created by a repository maintainer | `todo` on first import |
|
|
160
167
|
| Open issue with no linked pull request | Configured default status, which defaults to `backlog` |
|
|
161
|
-
| Open issue with a linked pull request and unfinished CI | `in_progress` |
|
|
168
|
+
| Open issue with a linked pull request and unfinished CI | `in_progress`; already-blocked pending-only PR waits remain `blocked` |
|
|
162
169
|
| Open issue with failing CI, a non-mergeable linked pull request, or unresolved review threads | `todo`, or `in_progress` when GitHub Sync can hand the work back to an executor |
|
|
163
170
|
| Open issue with green CI, a merge-ready linked pull request, and all review threads resolved | `in_review` |
|
|
164
171
|
| Closed issue completed as finished work | `done` |
|
|
@@ -170,7 +177,7 @@ Additional behavior:
|
|
|
170
177
|
- If the Paperclip host initially creates that imported maintainer issue in `backlog`, GitHub Sync promotes it to `todo` without replacing the configured default assignee with the executor handoff assignee, so triage ownership stays intact.
|
|
171
178
|
- When Paperclip board access is connected for a company, the advanced assignee dropdowns list both company agents and `Me` for the connected board user.
|
|
172
179
|
- Newly imported issues that finish sync in `todo` and are assigned to an agent enqueue an assignee wakeup so the agent can pick them up promptly.
|
|
173
|
-
- For linked pull requests, GitHub Sync treats merge-conflict, behind-branch, blocked, draft, unstable merge states, and unresolved review threads as executor work, while merge-ready states such as `CLEAN` and `HAS_HOOKS` can move work into `in_review` when CI is green and review threads are resolved. A stale aggregate `CHANGES_REQUESTED` review decision alone does not move that maintainer wait back to active execution. Transient `UNKNOWN` mergeability also does not move an already `in_review` maintainer wait back to active execution when CI is green and review threads are resolved.
|
|
180
|
+
- For linked pull requests, GitHub Sync treats merge-conflict, behind-branch, blocked, draft, unstable merge states, and unresolved review threads as executor work, while merge-ready states such as `CLEAN` and `HAS_HOOKS` can move work into `in_review` when CI is green and review threads are resolved. If an issue is already `blocked` and GitHub reports only pending external merge requirements while CI is unfinished, sync preserves the external wait instead of waking an executor. A stale aggregate `CHANGES_REQUESTED` review decision alone does not move that maintainer wait back to active execution. Transient `UNKNOWN` mergeability also does not move an already `in_review` maintainer wait back to active execution when CI is green and review threads are resolved.
|
|
174
181
|
- Imported issues that are already `blocked` stay `blocked` while any first-class `blockedBy` issue is still non-terminal, even if the linked GitHub pull request is otherwise green and review-ready.
|
|
175
182
|
- When sync moves work into `in_review`, GitHub Sync first follows the Paperclip issue execution policy's current reviewer or approver when that stage is visible on the issue. If Paperclip exposes an internal review or approval stage but not yet the participant, the plugin falls back to the configured reviewer or approver handoff assignee. If the transition is only a healthy linked-PR wait with no visible internal review or approval stage, GitHub Sync leaves the issue unassigned so it can wait on normal maintainer review without waking an internal owner.
|
|
176
183
|
- When sync moves work back into active execution, GitHub Sync first follows the Paperclip issue execution policy `returnAssignee` when it is available. Otherwise it falls back to the configured executor handoff assignee and then to the default imported assignee.
|
|
@@ -259,10 +266,33 @@ Current host caveat: on authenticated Paperclip deployments, the Paperclip host
|
|
|
259
266
|
|
|
260
267
|
Because the KPI attribution endpoint is a native plugin JSON route rather than a plugin tool, authenticated agent runs can still call it directly with `PAPERCLIP_API_KEY` even while that host bug blocks the GitHub Sync tool surface.
|
|
261
268
|
|
|
269
|
+
### Issue link API route
|
|
270
|
+
|
|
271
|
+
Authenticated agent runs can link the current Paperclip issue to a GitHub issue or pull request by posting to `/api/plugins/paperclip-github-plugin/api/issue-link`. This is useful after creating a PR with `gh` in a repository that is not mapped to a Paperclip project.
|
|
272
|
+
|
|
273
|
+
Supported payload fields:
|
|
274
|
+
|
|
275
|
+
- `paperclipIssueId` required: the Paperclip issue id to link
|
|
276
|
+
- `kind` optional: `issue` or `pull_request`; omitted values are inferred from full GitHub URLs when possible
|
|
277
|
+
- `reference` optional: a GitHub issue or pull request number, or a full GitHub URL
|
|
278
|
+
- `repository` optional: `owner/repo` or `https://github.com/owner/repo`, required for number-only references when the issue project is not mapped to that repository
|
|
279
|
+
- `issueNumber`, `pullRequestNumber`, or `pullRequestUrl` optional alternatives to `reference`
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
payload='{"paperclipIssueId":"iss_123","pullRequestUrl":"https://github.com/third-party/external/pull/77"}'
|
|
285
|
+
|
|
286
|
+
curl -X POST "${PAPERCLIP_API_URL%/}/api/plugins/paperclip-github-plugin/api/issue-link" \
|
|
287
|
+
-H "content-type: application/json" \
|
|
288
|
+
-H "authorization: Bearer ${PAPERCLIP_API_KEY}" \
|
|
289
|
+
-d "${payload}"
|
|
290
|
+
```
|
|
291
|
+
|
|
262
292
|
## Troubleshooting
|
|
263
293
|
|
|
264
294
|
- If an older GitHub Sync build fails upgrade with `requires host version 2026.427.0 or newer, but this server is running 0.0.0`, upgrade to a build that removes the strict manifest host-version gate. The host is reporting a development-version sentinel, so the plugin now relies on declared capabilities and runtime fallbacks instead.
|
|
265
|
-
- If setup is reported as incomplete, confirm that a GitHub token has been saved or that `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` contains `githubToken`, and make sure at least one mapping has a created Paperclip project.
|
|
295
|
+
- If setup is reported as incomplete, confirm that a GitHub token has been saved or that `${PAPERCLIP_HOME:-~/.paperclip}/plugins/github-sync/config.json` contains `githubToken`, and make sure at least one mapping has a created Paperclip project or at least one Paperclip issue has been linked to GitHub.
|
|
266
296
|
- If Paperclip says board access is required, open plugin settings inside the affected company and complete the Paperclip board access flow before retrying sync.
|
|
267
297
|
- If GitHub Sync agent tools fail with `403 {"error":"Board access required"}` on `/api/plugins/tools` or `/api/plugins/tools/execute`, the current Paperclip host rejected the request before the plugin worker ran. Re-run from a board-authenticated session or agent run that has board access to the target company.
|
|
268
298
|
- If a KPI API route call is rejected, make sure the request includes `Authorization: Bearer ${PAPERCLIP_API_KEY}`, that the token is still valid for the current run, and that any `companyId` in the payload matches the calling agent's company.
|
package/dist/manifest.js
CHANGED
|
@@ -522,6 +522,55 @@ var GITHUB_AGENT_TOOLS = [
|
|
|
522
522
|
projectNumber: projectNumberProperty
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "link_github_item",
|
|
528
|
+
displayName: "Link GitHub Item",
|
|
529
|
+
description: "Link a Paperclip issue to a GitHub issue or pull request so GitHub Sync can monitor status even when the repository is not mapped to a Paperclip project.",
|
|
530
|
+
parametersSchema: {
|
|
531
|
+
type: "object",
|
|
532
|
+
additionalProperties: false,
|
|
533
|
+
required: ["kind", "paperclipIssueId"],
|
|
534
|
+
anyOf: [
|
|
535
|
+
{
|
|
536
|
+
required: ["reference"]
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
required: ["issueNumber"]
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
required: ["pullRequestNumber"]
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
required: ["pullRequestUrl"]
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
properties: {
|
|
549
|
+
kind: {
|
|
550
|
+
type: "string",
|
|
551
|
+
enum: ["issue", "pull_request"],
|
|
552
|
+
description: "Whether to link a GitHub issue or pull request."
|
|
553
|
+
},
|
|
554
|
+
paperclipIssueId: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "Paperclip issue id that should receive the GitHub link."
|
|
557
|
+
},
|
|
558
|
+
repository: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "GitHub repository as owner/repo or https://github.com/owner/repo. Required when using a number instead of a full GitHub URL and the Paperclip issue project is not mapped to that repository."
|
|
561
|
+
},
|
|
562
|
+
reference: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description: "GitHub issue or pull request number, or a full GitHub issue or pull request URL."
|
|
565
|
+
},
|
|
566
|
+
issueNumber: issueNumberProperty,
|
|
567
|
+
pullRequestNumber: pullRequestNumberProperty,
|
|
568
|
+
pullRequestUrl: {
|
|
569
|
+
type: "string",
|
|
570
|
+
description: "Full GitHub pull request URL."
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
525
574
|
}
|
|
526
575
|
];
|
|
527
576
|
|
|
@@ -530,12 +579,15 @@ var GITHUB_SYNC_PLUGIN_ID = "paperclip-github-plugin";
|
|
|
530
579
|
var COMPANY_METRIC_API_ROUTE_KEY = "record-company-metric-event";
|
|
531
580
|
var COMPANY_METRIC_API_ROUTE_PATH = "/company-metrics/events";
|
|
532
581
|
var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${COMPANY_METRIC_API_ROUTE_PATH}`;
|
|
582
|
+
var ISSUE_LINK_API_ROUTE_KEY = "link-github-item";
|
|
583
|
+
var ISSUE_LINK_API_ROUTE_PATH = "/issue-link";
|
|
584
|
+
var ISSUE_LINK_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${ISSUE_LINK_API_ROUTE_PATH}`;
|
|
533
585
|
|
|
534
586
|
// src/manifest.ts
|
|
535
587
|
var require2 = createRequire(import.meta.url);
|
|
536
588
|
var packageJson = require2("../package.json");
|
|
537
589
|
var SCHEDULE_TICK_CRON = "* * * * *";
|
|
538
|
-
var MANIFEST_VERSION = "0.8.
|
|
590
|
+
var MANIFEST_VERSION = "0.8.8"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
|
|
539
591
|
var manifest = {
|
|
540
592
|
id: GITHUB_SYNC_PLUGIN_ID,
|
|
541
593
|
apiVersion: 1,
|
|
@@ -607,6 +659,13 @@ var manifest = {
|
|
|
607
659
|
path: COMPANY_METRIC_API_ROUTE_PATH,
|
|
608
660
|
auth: "agent",
|
|
609
661
|
capability: "api.routes.register"
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
routeKey: ISSUE_LINK_API_ROUTE_KEY,
|
|
665
|
+
method: "POST",
|
|
666
|
+
path: ISSUE_LINK_API_ROUTE_PATH,
|
|
667
|
+
auth: "agent",
|
|
668
|
+
capability: "api.routes.register"
|
|
610
669
|
}
|
|
611
670
|
],
|
|
612
671
|
tools: GITHUB_AGENT_TOOLS,
|
package/dist/worker.js
CHANGED
|
@@ -531,6 +531,55 @@ var GITHUB_AGENT_TOOLS = [
|
|
|
531
531
|
projectNumber: projectNumberProperty
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "link_github_item",
|
|
537
|
+
displayName: "Link GitHub Item",
|
|
538
|
+
description: "Link a Paperclip issue to a GitHub issue or pull request so GitHub Sync can monitor status even when the repository is not mapped to a Paperclip project.",
|
|
539
|
+
parametersSchema: {
|
|
540
|
+
type: "object",
|
|
541
|
+
additionalProperties: false,
|
|
542
|
+
required: ["kind", "paperclipIssueId"],
|
|
543
|
+
anyOf: [
|
|
544
|
+
{
|
|
545
|
+
required: ["reference"]
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
required: ["issueNumber"]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
required: ["pullRequestNumber"]
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
required: ["pullRequestUrl"]
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
properties: {
|
|
558
|
+
kind: {
|
|
559
|
+
type: "string",
|
|
560
|
+
enum: ["issue", "pull_request"],
|
|
561
|
+
description: "Whether to link a GitHub issue or pull request."
|
|
562
|
+
},
|
|
563
|
+
paperclipIssueId: {
|
|
564
|
+
type: "string",
|
|
565
|
+
description: "Paperclip issue id that should receive the GitHub link."
|
|
566
|
+
},
|
|
567
|
+
repository: {
|
|
568
|
+
type: "string",
|
|
569
|
+
description: "GitHub repository as owner/repo or https://github.com/owner/repo. Required when using a number instead of a full GitHub URL and the Paperclip issue project is not mapped to that repository."
|
|
570
|
+
},
|
|
571
|
+
reference: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: "GitHub issue or pull request number, or a full GitHub issue or pull request URL."
|
|
574
|
+
},
|
|
575
|
+
issueNumber: issueNumberProperty,
|
|
576
|
+
pullRequestNumber: pullRequestNumberProperty,
|
|
577
|
+
pullRequestUrl: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Full GitHub pull request URL."
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
534
583
|
}
|
|
535
584
|
];
|
|
536
585
|
function getGitHubAgentToolDeclaration(name) {
|
|
@@ -585,6 +634,9 @@ var GITHUB_SYNC_PLUGIN_ID = "paperclip-github-plugin";
|
|
|
585
634
|
var COMPANY_METRIC_API_ROUTE_KEY = "record-company-metric-event";
|
|
586
635
|
var COMPANY_METRIC_API_ROUTE_PATH = "/company-metrics/events";
|
|
587
636
|
var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${COMPANY_METRIC_API_ROUTE_PATH}`;
|
|
637
|
+
var ISSUE_LINK_API_ROUTE_KEY = "link-github-item";
|
|
638
|
+
var ISSUE_LINK_API_ROUTE_PATH = "/issue-link";
|
|
639
|
+
var ISSUE_LINK_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${ISSUE_LINK_API_ROUTE_PATH}`;
|
|
588
640
|
|
|
589
641
|
// src/paperclip-health.ts
|
|
590
642
|
function normalizeOptionalString(value) {
|
|
@@ -636,6 +688,10 @@ var COMPANY_KPI_SCOPE = {
|
|
|
636
688
|
scopeKind: "instance",
|
|
637
689
|
stateKey: "paperclip-github-plugin-company-kpis"
|
|
638
690
|
};
|
|
691
|
+
var EXTERNAL_LINK_COMPANY_INDEX_SCOPE = {
|
|
692
|
+
scopeKind: "instance",
|
|
693
|
+
stateKey: "paperclip-github-plugin-external-link-companies"
|
|
694
|
+
};
|
|
639
695
|
var DEFAULT_SCHEDULE_FREQUENCY_MINUTES = 15;
|
|
640
696
|
var DEFAULT_IMPORTED_ISSUE_STATUS = "backlog";
|
|
641
697
|
var DEFAULT_IGNORED_GITHUB_ISSUE_USERNAMES = ["renovate"];
|
|
@@ -2544,19 +2600,6 @@ async function resolveManualSyncTarget(ctx, settings, input) {
|
|
|
2544
2600
|
if (!pullRequestLink) {
|
|
2545
2601
|
throw new Error("This Paperclip issue is not linked to a GitHub issue or pull request yet. Run a broader sync first.");
|
|
2546
2602
|
}
|
|
2547
|
-
const candidateMappings = getSyncableMappingsForTarget(settings.mappings, {
|
|
2548
|
-
kind: "issue",
|
|
2549
|
-
companyId: companyId2,
|
|
2550
|
-
projectId: pullRequestLink.data.paperclipProjectId,
|
|
2551
|
-
repositoryUrl: pullRequestLink.data.repositoryUrl,
|
|
2552
|
-
issueId: input.issueId,
|
|
2553
|
-
githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
|
|
2554
|
-
githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
|
|
2555
|
-
displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
|
|
2556
|
-
});
|
|
2557
|
-
if (candidateMappings.length === 0) {
|
|
2558
|
-
throw new Error("No saved GitHub repository mapping matches this Paperclip issue.");
|
|
2559
|
-
}
|
|
2560
2603
|
return {
|
|
2561
2604
|
kind: "issue",
|
|
2562
2605
|
companyId: companyId2,
|
|
@@ -4481,6 +4524,58 @@ function normalizeImportRegistry(value) {
|
|
|
4481
4524
|
};
|
|
4482
4525
|
}).filter((entry) => entry !== null);
|
|
4483
4526
|
}
|
|
4527
|
+
function normalizeExternalGitHubLinkCompanyIndex(value) {
|
|
4528
|
+
const rawCompanyIds = Array.isArray(value) ? value : value && typeof value === "object" && Array.isArray(value.companyIds) ? value.companyIds : [];
|
|
4529
|
+
const companyIds = [
|
|
4530
|
+
...new Set(
|
|
4531
|
+
rawCompanyIds.map((entry) => normalizeCompanyId(entry)).filter((companyId) => Boolean(companyId))
|
|
4532
|
+
)
|
|
4533
|
+
].sort();
|
|
4534
|
+
const updatedAt = value && typeof value === "object" && typeof value.updatedAt === "string" ? value.updatedAt.trim() : void 0;
|
|
4535
|
+
return {
|
|
4536
|
+
companyIds,
|
|
4537
|
+
...updatedAt ? { updatedAt } : {}
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
async function getExternalGitHubLinkCompanyIds(ctx) {
|
|
4541
|
+
return normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE)).companyIds;
|
|
4542
|
+
}
|
|
4543
|
+
async function rememberExternalGitHubLinkCompany(ctx, companyId) {
|
|
4544
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4545
|
+
if (!normalizedCompanyId) {
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
const index = normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE));
|
|
4549
|
+
if (index.companyIds.includes(normalizedCompanyId)) {
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
await ctx.state.set(EXTERNAL_LINK_COMPANY_INDEX_SCOPE, {
|
|
4553
|
+
companyIds: [...index.companyIds, normalizedCompanyId].sort(),
|
|
4554
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
async function forgetExternalGitHubLinkCompanyIfEmpty(ctx, companyId) {
|
|
4558
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4559
|
+
if (!normalizedCompanyId) {
|
|
4560
|
+
return;
|
|
4561
|
+
}
|
|
4562
|
+
const [issueLinks, pullRequestLinks] = await Promise.all([
|
|
4563
|
+
listGitHubIssueLinkRecords(ctx),
|
|
4564
|
+
listGitHubPullRequestLinkRecords(ctx)
|
|
4565
|
+
]);
|
|
4566
|
+
const hasRemainingLinks = issueLinks.some((record) => record.data.companyId === normalizedCompanyId) || pullRequestLinks.some((record) => record.data.companyId === normalizedCompanyId);
|
|
4567
|
+
if (hasRemainingLinks) {
|
|
4568
|
+
return;
|
|
4569
|
+
}
|
|
4570
|
+
const index = normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE));
|
|
4571
|
+
if (!index.companyIds.includes(normalizedCompanyId)) {
|
|
4572
|
+
return;
|
|
4573
|
+
}
|
|
4574
|
+
await ctx.state.set(EXTERNAL_LINK_COMPANY_INDEX_SCOPE, {
|
|
4575
|
+
companyIds: index.companyIds.filter((entry) => entry !== normalizedCompanyId),
|
|
4576
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4484
4579
|
function requireRepositoryReference(repositoryInput) {
|
|
4485
4580
|
const parsed = parseRepositoryReference(repositoryInput);
|
|
4486
4581
|
if (!parsed) {
|
|
@@ -5329,6 +5424,9 @@ function isHealthyMaintainerWaitTransition(params) {
|
|
|
5329
5424
|
const { currentStatus, nextStatus, syncContext } = params;
|
|
5330
5425
|
return nextStatus === "in_review" && (currentStatus === "done" || currentStatus === "in_review") && syncContext.executionState === null && syncContext.executionPolicy !== null;
|
|
5331
5426
|
}
|
|
5427
|
+
function shouldClearCompletedSyncExecutionPolicy(params) {
|
|
5428
|
+
return (params.nextStatus === "done" || params.nextStatus === "cancelled") && (params.syncContext.executionPolicy !== null || params.syncContext.executionState !== null);
|
|
5429
|
+
}
|
|
5332
5430
|
function shouldPreserveImportedTriageAssignee(params) {
|
|
5333
5431
|
return params.wasImportedThisRun && params.maintainerAuthoredImportedIssue === true && params.currentStatus === "backlog" && params.nextStatus === "todo";
|
|
5334
5432
|
}
|
|
@@ -5518,6 +5616,12 @@ function resolvePaperclipIssueStatus(params) {
|
|
|
5518
5616
|
return hasExecutorHandoffTarget ? "in_progress" : "todo";
|
|
5519
5617
|
}
|
|
5520
5618
|
if (snapshot.linkedPullRequests.length > 0) {
|
|
5619
|
+
if (shouldPreserveBlockedExternalPullRequestWait({
|
|
5620
|
+
currentStatus,
|
|
5621
|
+
linkedPullRequests: snapshot.linkedPullRequests
|
|
5622
|
+
})) {
|
|
5623
|
+
return "blocked";
|
|
5624
|
+
}
|
|
5521
5625
|
return resolvePaperclipStatusFromLinkedPullRequests(snapshot.linkedPullRequests, {
|
|
5522
5626
|
preferInProgress: hasExecutorHandoffTarget,
|
|
5523
5627
|
preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
|
|
@@ -5536,6 +5640,12 @@ function resolvePaperclipPullRequestIssueStatus(params) {
|
|
|
5536
5640
|
if (currentStatus === "done" || currentStatus === "cancelled") {
|
|
5537
5641
|
return currentStatus;
|
|
5538
5642
|
}
|
|
5643
|
+
if (shouldPreserveBlockedExternalPullRequestWait({
|
|
5644
|
+
currentStatus,
|
|
5645
|
+
linkedPullRequests: [pullRequest]
|
|
5646
|
+
})) {
|
|
5647
|
+
return "blocked";
|
|
5648
|
+
}
|
|
5539
5649
|
return resolvePaperclipStatusFromLinkedPullRequests([pullRequest], {
|
|
5540
5650
|
preferInProgress: hasExecutorHandoffTarget,
|
|
5541
5651
|
preserveTransientUnknownMergeabilityWait: currentStatus === "in_review"
|
|
@@ -5687,6 +5797,12 @@ function normalizeGitHubPullRequestReviewDecision(value) {
|
|
|
5687
5797
|
function isGitHubPullRequestActionRequiredForSync(pullRequest) {
|
|
5688
5798
|
return pullRequest.mergeability === "conflicting" || ACTION_REQUIRED_GITHUB_PULL_REQUEST_MERGE_STATE_STATUSES.has(pullRequest.mergeStateStatus);
|
|
5689
5799
|
}
|
|
5800
|
+
function isGitHubPullRequestPendingExternalWaitForSync(pullRequest) {
|
|
5801
|
+
return pullRequest.ciState === "unfinished" && !pullRequest.hasUnresolvedReviewThreads && pullRequest.mergeability !== "conflicting" && (pullRequest.mergeStateStatus === "blocked" || pullRequest.mergeStateStatus === "unstable");
|
|
5802
|
+
}
|
|
5803
|
+
function shouldPreserveBlockedExternalPullRequestWait(params) {
|
|
5804
|
+
return params.currentStatus === "blocked" && params.linkedPullRequests.length > 0 && params.linkedPullRequests.every((pullRequest) => isGitHubPullRequestPendingExternalWaitForSync(pullRequest));
|
|
5805
|
+
}
|
|
5690
5806
|
function isGitHubPullRequestTransientUnknownMergeabilityWait(pullRequest) {
|
|
5691
5807
|
return pullRequest.ciState === "green" && !pullRequest.hasUnresolvedReviewThreads && pullRequest.mergeability !== "conflicting" && pullRequest.mergeStateStatus === "unknown";
|
|
5692
5808
|
}
|
|
@@ -6946,6 +7062,126 @@ async function listGitHubPullRequestIssueLinksForMapping(ctx, mapping, target) {
|
|
|
6946
7062
|
}
|
|
6947
7063
|
return [...recordsByKey.values()];
|
|
6948
7064
|
}
|
|
7065
|
+
function doesGitHubIssueLinkRecordMatchMapping(record, mapping) {
|
|
7066
|
+
if (record.data.repositoryUrl !== getNormalizedMappingRepositoryUrl(mapping)) {
|
|
7067
|
+
return false;
|
|
7068
|
+
}
|
|
7069
|
+
if (record.data.companyId && record.data.companyId !== mapping.companyId) {
|
|
7070
|
+
return false;
|
|
7071
|
+
}
|
|
7072
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== mapping.paperclipProjectId) {
|
|
7073
|
+
return false;
|
|
7074
|
+
}
|
|
7075
|
+
return Boolean(mapping.companyId && mapping.paperclipProjectId);
|
|
7076
|
+
}
|
|
7077
|
+
function doesGitHubIssueLinkRecordMatchTarget(record, target) {
|
|
7078
|
+
if (!target) {
|
|
7079
|
+
return true;
|
|
7080
|
+
}
|
|
7081
|
+
switch (target.kind) {
|
|
7082
|
+
case "company":
|
|
7083
|
+
return !record.data.companyId || record.data.companyId === target.companyId;
|
|
7084
|
+
case "project":
|
|
7085
|
+
return (!record.data.companyId || record.data.companyId === target.companyId) && (!record.data.paperclipProjectId || record.data.paperclipProjectId === target.projectId);
|
|
7086
|
+
case "issue":
|
|
7087
|
+
return Boolean(target.issueId && record.paperclipIssueId === target.issueId) && (!record.data.companyId || record.data.companyId === target.companyId);
|
|
7088
|
+
default:
|
|
7089
|
+
return true;
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
function isGitHubIssueLinkCoveredByMappings(record, mappings) {
|
|
7093
|
+
return mappings.some((mapping) => doesGitHubIssueLinkRecordMatchMapping(record, mapping));
|
|
7094
|
+
}
|
|
7095
|
+
function isGitHubPullRequestLinkCoveredByMappings(record, mappings) {
|
|
7096
|
+
return mappings.some((mapping) => doesGitHubPullRequestLinkRecordMatchMapping(record, mapping));
|
|
7097
|
+
}
|
|
7098
|
+
async function listExternalGitHubLinkSyncWork(ctx, mappings, target) {
|
|
7099
|
+
const syncableMappings = getSyncableMappingsForTarget(mappings, target);
|
|
7100
|
+
const issueLinksByKey = /* @__PURE__ */ new Map();
|
|
7101
|
+
const pullRequestLinksByKey = /* @__PURE__ */ new Map();
|
|
7102
|
+
const [issueLinks, pullRequestLinks] = await Promise.all([
|
|
7103
|
+
listGitHubIssueLinkRecords(ctx, {
|
|
7104
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
7105
|
+
}),
|
|
7106
|
+
listGitHubPullRequestLinkRecords(ctx, {
|
|
7107
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
7108
|
+
})
|
|
7109
|
+
]);
|
|
7110
|
+
for (const record of issueLinks) {
|
|
7111
|
+
if (!doesGitHubIssueLinkRecordMatchTarget(record, target) || isGitHubIssueLinkCoveredByMappings(record, syncableMappings)) {
|
|
7112
|
+
continue;
|
|
7113
|
+
}
|
|
7114
|
+
issueLinksByKey.set(
|
|
7115
|
+
`${record.paperclipIssueId}:${record.data.githubIssueUrl}`,
|
|
7116
|
+
record
|
|
7117
|
+
);
|
|
7118
|
+
}
|
|
7119
|
+
for (const record of pullRequestLinks) {
|
|
7120
|
+
if (!doesGitHubPullRequestLinkRecordMatchTarget(record, target) || isGitHubPullRequestLinkCoveredByMappings(record, syncableMappings)) {
|
|
7121
|
+
continue;
|
|
7122
|
+
}
|
|
7123
|
+
pullRequestLinksByKey.set(
|
|
7124
|
+
`${record.paperclipIssueId}:${buildGitHubPullRequestReferenceKey({
|
|
7125
|
+
number: record.data.githubPullRequestNumber,
|
|
7126
|
+
repositoryUrl: record.data.repositoryUrl
|
|
7127
|
+
})}`,
|
|
7128
|
+
record
|
|
7129
|
+
);
|
|
7130
|
+
}
|
|
7131
|
+
return {
|
|
7132
|
+
issueLinks: [...issueLinksByKey.values()],
|
|
7133
|
+
pullRequestLinks: [...pullRequestLinksByKey.values()]
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
function buildExternalLinkAuthMappings(work) {
|
|
7137
|
+
const mappingsByKey = /* @__PURE__ */ new Map();
|
|
7138
|
+
const addMapping = (params) => {
|
|
7139
|
+
const companyId = normalizeCompanyId(params.companyId);
|
|
7140
|
+
if (!companyId) {
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
7143
|
+
const repositoryUrl = getNormalizedMappingRepositoryUrl({
|
|
7144
|
+
repositoryUrl: params.repositoryUrl
|
|
7145
|
+
});
|
|
7146
|
+
const key = `${companyId}:${params.projectId ?? ""}:${repositoryUrl}`;
|
|
7147
|
+
mappingsByKey.set(key, {
|
|
7148
|
+
id: `external-link:${key}`,
|
|
7149
|
+
repositoryUrl,
|
|
7150
|
+
paperclipProjectName: "",
|
|
7151
|
+
...params.projectId ? { paperclipProjectId: params.projectId } : {},
|
|
7152
|
+
companyId
|
|
7153
|
+
});
|
|
7154
|
+
};
|
|
7155
|
+
for (const link of work.issueLinks) {
|
|
7156
|
+
addMapping({
|
|
7157
|
+
companyId: link.data.companyId,
|
|
7158
|
+
projectId: link.data.paperclipProjectId,
|
|
7159
|
+
repositoryUrl: link.data.repositoryUrl
|
|
7160
|
+
});
|
|
7161
|
+
}
|
|
7162
|
+
for (const link of work.pullRequestLinks) {
|
|
7163
|
+
addMapping({
|
|
7164
|
+
companyId: link.data.companyId,
|
|
7165
|
+
projectId: link.data.paperclipProjectId,
|
|
7166
|
+
repositoryUrl: link.data.repositoryUrl
|
|
7167
|
+
});
|
|
7168
|
+
}
|
|
7169
|
+
return [...mappingsByKey.values()];
|
|
7170
|
+
}
|
|
7171
|
+
function countExternalLinkRepositories(work) {
|
|
7172
|
+
const repositories = /* @__PURE__ */ new Set();
|
|
7173
|
+
for (const link of work.issueLinks) {
|
|
7174
|
+
if (link.data.companyId) {
|
|
7175
|
+
repositories.add(`${link.data.companyId}:${link.data.repositoryUrl}`);
|
|
7176
|
+
}
|
|
7177
|
+
}
|
|
7178
|
+
for (const link of work.pullRequestLinks) {
|
|
7179
|
+
if (link.data.companyId) {
|
|
7180
|
+
repositories.add(`${link.data.companyId}:${link.data.repositoryUrl}`);
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
7183
|
+
return repositories.size;
|
|
7184
|
+
}
|
|
6949
7185
|
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
6950
7186
|
const issueId = params.issueId.trim();
|
|
6951
7187
|
const commentId = params.commentId.trim();
|
|
@@ -7031,7 +7267,7 @@ async function upsertGitHubPullRequestLinkRecord(ctx, params) {
|
|
|
7031
7267
|
status: params.pullRequestState,
|
|
7032
7268
|
data: {
|
|
7033
7269
|
companyId: params.companyId,
|
|
7034
|
-
paperclipProjectId: params.projectId,
|
|
7270
|
+
...params.projectId ? { paperclipProjectId: params.projectId } : {},
|
|
7035
7271
|
repositoryUrl: getNormalizedMappingRepositoryUrl({
|
|
7036
7272
|
repositoryUrl: params.repositoryUrl
|
|
7037
7273
|
}),
|
|
@@ -7070,6 +7306,56 @@ async function assertPaperclipIssueHasNoManualGitHubLink(ctx, params) {
|
|
|
7070
7306
|
throw new Error("This Paperclip issue is already linked to a GitHub pull request.");
|
|
7071
7307
|
}
|
|
7072
7308
|
}
|
|
7309
|
+
async function resolveIssueGitHubLinkScope(ctx, params) {
|
|
7310
|
+
if (!params.allowUnmapped) {
|
|
7311
|
+
const mappedScope = await resolveIssueGitHubLinkMapping(ctx, params);
|
|
7312
|
+
return {
|
|
7313
|
+
issue: mappedScope.issue,
|
|
7314
|
+
projectId: mappedScope.projectId,
|
|
7315
|
+
mapping: mappedScope.mapping,
|
|
7316
|
+
repository: mappedScope.repository
|
|
7317
|
+
};
|
|
7318
|
+
}
|
|
7319
|
+
const issue = await ctx.issues.get(params.issueId, params.companyId);
|
|
7320
|
+
if (!issue) {
|
|
7321
|
+
throw new Error("Paperclip issue was not found.");
|
|
7322
|
+
}
|
|
7323
|
+
const projectId = typeof issue.projectId === "string" && issue.projectId.trim() ? issue.projectId.trim() : void 0;
|
|
7324
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7325
|
+
const requestedRepository = params.repositoryUrl ? parseRepositoryReference(params.repositoryUrl) : null;
|
|
7326
|
+
if (params.repositoryUrl && !requestedRepository) {
|
|
7327
|
+
throw new Error(`Invalid GitHub repository: ${params.repositoryUrl}. Use owner/repo or https://github.com/owner/repo.`);
|
|
7328
|
+
}
|
|
7329
|
+
const candidateMappings = projectId ? await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7330
|
+
companyId: params.companyId,
|
|
7331
|
+
projectId
|
|
7332
|
+
}) : [];
|
|
7333
|
+
const matchingMappings = requestedRepository ? candidateMappings.filter(
|
|
7334
|
+
(mapping) => areRepositoriesEqual(requireRepositoryReference(mapping.repositoryUrl), requestedRepository)
|
|
7335
|
+
) : candidateMappings;
|
|
7336
|
+
if (matchingMappings.length === 1) {
|
|
7337
|
+
const mapping = matchingMappings[0];
|
|
7338
|
+
return {
|
|
7339
|
+
issue,
|
|
7340
|
+
projectId,
|
|
7341
|
+
mapping,
|
|
7342
|
+
repository: requestedRepository ?? requireRepositoryReference(mapping.repositoryUrl)
|
|
7343
|
+
};
|
|
7344
|
+
}
|
|
7345
|
+
if (matchingMappings.length > 1 && !requestedRepository) {
|
|
7346
|
+
throw new Error("This Paperclip issue project has multiple GitHub repositories. Enter the full GitHub URL.");
|
|
7347
|
+
}
|
|
7348
|
+
if (!requestedRepository) {
|
|
7349
|
+
throw new Error(
|
|
7350
|
+
"Enter a full GitHub URL or repository because this Paperclip issue project is not mapped to that repository."
|
|
7351
|
+
);
|
|
7352
|
+
}
|
|
7353
|
+
return {
|
|
7354
|
+
issue,
|
|
7355
|
+
...projectId ? { projectId } : {},
|
|
7356
|
+
repository: requestedRepository
|
|
7357
|
+
};
|
|
7358
|
+
}
|
|
7073
7359
|
async function resolveIssueGitHubLinkMapping(ctx, params) {
|
|
7074
7360
|
const issue = await ctx.issues.get(params.issueId, params.companyId);
|
|
7075
7361
|
if (!issue) {
|
|
@@ -7192,10 +7478,11 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7192
7478
|
repositoryUrl: params.repositoryUrl,
|
|
7193
7479
|
issueNumber: params.issueNumber
|
|
7194
7480
|
});
|
|
7195
|
-
const scope = await
|
|
7481
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7196
7482
|
companyId,
|
|
7197
7483
|
issueId,
|
|
7198
|
-
repositoryUrl: reference.repositoryUrl
|
|
7484
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7485
|
+
allowUnmapped: params.allowUnmapped
|
|
7199
7486
|
});
|
|
7200
7487
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7201
7488
|
const response = await octokit.rest.issues.get({
|
|
@@ -7212,18 +7499,29 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7212
7499
|
}
|
|
7213
7500
|
const githubIssue = normalizeGitHubIssueRecord(rawIssue);
|
|
7214
7501
|
const linkedPullRequests = await listLinkedPullRequestsForIssue(octokit, scope.repository, githubIssue.number);
|
|
7215
|
-
|
|
7216
|
-
const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
|
|
7217
|
-
upsertImportedIssueRecord(
|
|
7218
|
-
importRegistry,
|
|
7219
|
-
buildImportedIssueRecord(scope.mapping, githubIssue, issueId, (/* @__PURE__ */ new Date()).toISOString())
|
|
7220
|
-
);
|
|
7221
|
-
await ctx.state.set(IMPORT_REGISTRY_SCOPE, importRegistry);
|
|
7222
|
-
invalidateProjectPullRequestCaches({
|
|
7502
|
+
const linkTarget = {
|
|
7223
7503
|
companyId,
|
|
7224
|
-
projectId: scope.mapping
|
|
7225
|
-
|
|
7226
|
-
}
|
|
7504
|
+
...scope.mapping?.paperclipProjectId ?? scope.projectId ? { paperclipProjectId: scope.mapping?.paperclipProjectId ?? scope.projectId } : {},
|
|
7505
|
+
repositoryUrl: scope.repository.url
|
|
7506
|
+
};
|
|
7507
|
+
await upsertGitHubIssueLinkRecord(ctx, linkTarget, issueId, githubIssue, linkedPullRequests);
|
|
7508
|
+
await rememberExternalGitHubLinkCompany(ctx, companyId);
|
|
7509
|
+
if (scope.mapping) {
|
|
7510
|
+
const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
|
|
7511
|
+
upsertImportedIssueRecord(
|
|
7512
|
+
importRegistry,
|
|
7513
|
+
buildImportedIssueRecord(scope.mapping, githubIssue, issueId, (/* @__PURE__ */ new Date()).toISOString())
|
|
7514
|
+
);
|
|
7515
|
+
await ctx.state.set(IMPORT_REGISTRY_SCOPE, importRegistry);
|
|
7516
|
+
}
|
|
7517
|
+
const projectIdForCacheInvalidation = scope.mapping?.paperclipProjectId ?? scope.projectId;
|
|
7518
|
+
if (projectIdForCacheInvalidation) {
|
|
7519
|
+
invalidateProjectPullRequestCaches({
|
|
7520
|
+
companyId,
|
|
7521
|
+
projectId: projectIdForCacheInvalidation,
|
|
7522
|
+
repository: scope.repository
|
|
7523
|
+
});
|
|
7524
|
+
}
|
|
7227
7525
|
return {
|
|
7228
7526
|
kind: "issue",
|
|
7229
7527
|
paperclipIssueId: issueId,
|
|
@@ -7251,10 +7549,11 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7251
7549
|
pullRequestNumber: params.pullRequestNumber,
|
|
7252
7550
|
pullRequestUrl: params.pullRequestUrl
|
|
7253
7551
|
});
|
|
7254
|
-
const scope = await
|
|
7552
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7255
7553
|
companyId,
|
|
7256
7554
|
issueId,
|
|
7257
|
-
repositoryUrl: reference.repositoryUrl
|
|
7555
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7556
|
+
allowUnmapped: params.allowUnmapped
|
|
7258
7557
|
});
|
|
7259
7558
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7260
7559
|
const response = await octokit.rest.pulls.get({
|
|
@@ -7272,7 +7571,7 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7272
7571
|
});
|
|
7273
7572
|
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
7274
7573
|
companyId,
|
|
7275
|
-
projectId: scope.mapping
|
|
7574
|
+
projectId: scope.mapping?.paperclipProjectId ?? scope.projectId,
|
|
7276
7575
|
issueId,
|
|
7277
7576
|
repositoryUrl: scope.repository.url,
|
|
7278
7577
|
pullRequestNumber: reference.pullRequestNumber,
|
|
@@ -7280,11 +7579,15 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7280
7579
|
pullRequestTitle: response.data.title || `Pull request #${reference.pullRequestNumber}`,
|
|
7281
7580
|
pullRequestState
|
|
7282
7581
|
});
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7582
|
+
await rememberExternalGitHubLinkCompany(ctx, companyId);
|
|
7583
|
+
const projectIdForCacheInvalidation = scope.mapping?.paperclipProjectId ?? scope.projectId;
|
|
7584
|
+
if (projectIdForCacheInvalidation) {
|
|
7585
|
+
invalidateProjectPullRequestCaches({
|
|
7586
|
+
companyId,
|
|
7587
|
+
projectId: projectIdForCacheInvalidation,
|
|
7588
|
+
repository: scope.repository
|
|
7589
|
+
});
|
|
7590
|
+
}
|
|
7288
7591
|
return {
|
|
7289
7592
|
kind: "pull_request",
|
|
7290
7593
|
paperclipIssueId: issueId,
|
|
@@ -7412,6 +7715,7 @@ async function unlinkPaperclipIssueFromGitHub(ctx, params) {
|
|
|
7412
7715
|
if (Object.keys(issuePatch).length > 0) {
|
|
7413
7716
|
await ctx.issues.update(issueId, issuePatch, companyId);
|
|
7414
7717
|
}
|
|
7718
|
+
await forgetExternalGitHubLinkCompanyIfEmpty(ctx, companyId);
|
|
7415
7719
|
return {
|
|
7416
7720
|
paperclipIssueId: issueId,
|
|
7417
7721
|
unlinked: issueLinkRecords.length > 0 || pullRequestLinkRecords.length > 0 || removedImportRegistryEntries.length > 0 || shouldClearGitHubOrigin || nextDescription !== void 0,
|
|
@@ -9171,6 +9475,10 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9171
9475
|
nextStatus,
|
|
9172
9476
|
syncContext: paperclipIssueSyncContext
|
|
9173
9477
|
});
|
|
9478
|
+
const shouldClearCompletedExecutionPolicy = shouldClearCompletedSyncExecutionPolicy({
|
|
9479
|
+
nextStatus,
|
|
9480
|
+
syncContext: paperclipIssueSyncContext
|
|
9481
|
+
});
|
|
9174
9482
|
const shouldPreserveImportedTriageRouting = shouldPreserveImportedTriageAssignee({
|
|
9175
9483
|
currentStatus: paperclipIssue.status,
|
|
9176
9484
|
nextStatus,
|
|
@@ -9192,7 +9500,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9192
9500
|
importedIssue.lastSeenGitHubState = snapshot.state;
|
|
9193
9501
|
importedIssue.linkedPullRequestCommentCounts = currentLinkedPullRequestCommentCounts;
|
|
9194
9502
|
if (paperclipIssue.status === nextStatus) {
|
|
9195
|
-
if (shouldClearTransitionAssignee) {
|
|
9503
|
+
if (shouldClearTransitionAssignee || shouldClearCompletedExecutionPolicy) {
|
|
9196
9504
|
updateSyncFailureContext(syncFailureContext, {
|
|
9197
9505
|
phase: "updating_paperclip_status",
|
|
9198
9506
|
repositoryUrl: repository.url,
|
|
@@ -9204,8 +9512,8 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9204
9512
|
currentStatus: paperclipIssue.status,
|
|
9205
9513
|
syncContext: paperclipIssueSyncContext,
|
|
9206
9514
|
nextStatus,
|
|
9207
|
-
clearAssignee: true,
|
|
9208
|
-
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
9515
|
+
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
9516
|
+
...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
|
|
9209
9517
|
transitionComment: "",
|
|
9210
9518
|
paperclipApiBaseUrl
|
|
9211
9519
|
});
|
|
@@ -9241,7 +9549,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9241
9549
|
nextStatus,
|
|
9242
9550
|
...nextTransitionAssignee ? { nextAssignee: nextTransitionAssignee.principal } : {},
|
|
9243
9551
|
...shouldClearTransitionAssignee ? { clearAssignee: true } : {},
|
|
9244
|
-
...shouldPreserveMaintainerWaitRouting ? { clearExecutionPolicy: true } : {},
|
|
9552
|
+
...shouldPreserveMaintainerWaitRouting || shouldClearCompletedExecutionPolicy ? { clearExecutionPolicy: true } : {},
|
|
9245
9553
|
transitionComment: transitionComment.body,
|
|
9246
9554
|
transitionCommentAnnotation: transitionComment.annotation,
|
|
9247
9555
|
paperclipApiBaseUrl
|
|
@@ -9304,7 +9612,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9304
9612
|
};
|
|
9305
9613
|
}
|
|
9306
9614
|
async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
9307
|
-
if (!mapping.companyId || !
|
|
9615
|
+
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
9308
9616
|
return {
|
|
9309
9617
|
updatedStatusesCount: 0
|
|
9310
9618
|
};
|
|
@@ -9753,14 +10061,6 @@ function parseCompanyMetricApiRouteBody(input) {
|
|
|
9753
10061
|
return input.body;
|
|
9754
10062
|
}
|
|
9755
10063
|
async function handleCompanyMetricApiRoute(ctx, input) {
|
|
9756
|
-
if (input.routeKey !== COMPANY_METRIC_API_ROUTE_KEY) {
|
|
9757
|
-
return {
|
|
9758
|
-
status: 404,
|
|
9759
|
-
body: {
|
|
9760
|
-
error: `Unsupported plugin API route: ${input.routeKey}.`
|
|
9761
|
-
}
|
|
9762
|
-
};
|
|
9763
|
-
}
|
|
9764
10064
|
if (input.actor.actorType !== "agent") {
|
|
9765
10065
|
throw new Error("Company KPI metric events must be recorded by an authenticated Paperclip agent.");
|
|
9766
10066
|
}
|
|
@@ -9860,6 +10160,83 @@ async function handleCompanyMetricApiRoute(ctx, input) {
|
|
|
9860
10160
|
}
|
|
9861
10161
|
};
|
|
9862
10162
|
}
|
|
10163
|
+
function parseIssueLinkApiRouteBody(input) {
|
|
10164
|
+
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) {
|
|
10165
|
+
throw new Error("Issue link route body must be a JSON object.");
|
|
10166
|
+
}
|
|
10167
|
+
return input.body;
|
|
10168
|
+
}
|
|
10169
|
+
function normalizeIssueLinkApiRouteKind(payload) {
|
|
10170
|
+
const explicitKind = normalizeIssueGitHubLinkKind(payload.kind);
|
|
10171
|
+
if (explicitKind) {
|
|
10172
|
+
return explicitKind;
|
|
10173
|
+
}
|
|
10174
|
+
const reference = normalizeOptionalString2(payload.reference);
|
|
10175
|
+
if (normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(payload.pullRequestUrl)) || normalizeToolPositiveInteger(payload.pullRequestNumber) || reference && parseGitHubPullRequestHtmlUrl(reference)) {
|
|
10176
|
+
return "pull_request";
|
|
10177
|
+
}
|
|
10178
|
+
if (normalizeToolPositiveInteger(payload.issueNumber) || reference && parseGitHubIssueHtmlUrl(reference)) {
|
|
10179
|
+
return "issue";
|
|
10180
|
+
}
|
|
10181
|
+
return null;
|
|
10182
|
+
}
|
|
10183
|
+
async function handleIssueLinkApiRoute(ctx, input) {
|
|
10184
|
+
if (input.actor.actorType !== "agent") {
|
|
10185
|
+
throw new Error("GitHub issue links must be recorded by an authenticated Paperclip agent.");
|
|
10186
|
+
}
|
|
10187
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
10188
|
+
if (!companyId) {
|
|
10189
|
+
throw new Error("GitHub issue links require the host to provide the authenticated agent company.");
|
|
10190
|
+
}
|
|
10191
|
+
const payload = parseIssueLinkApiRouteBody(input);
|
|
10192
|
+
const requestedCompanyId = normalizeCompanyId(payload.companyId);
|
|
10193
|
+
if (requestedCompanyId && requestedCompanyId !== companyId) {
|
|
10194
|
+
throw new Error("companyId must match the authenticated Paperclip agent company.");
|
|
10195
|
+
}
|
|
10196
|
+
const kind = normalizeIssueLinkApiRouteKind(payload);
|
|
10197
|
+
if (!kind) {
|
|
10198
|
+
throw new Error('kind must be "issue" or "pull_request", or the payload must include a full GitHub URL.');
|
|
10199
|
+
}
|
|
10200
|
+
const paperclipIssueId = normalizeOptionalString2(payload.paperclipIssueId) ?? normalizeOptionalString2(payload.issueId);
|
|
10201
|
+
if (!paperclipIssueId) {
|
|
10202
|
+
throw new Error("paperclipIssueId is required.");
|
|
10203
|
+
}
|
|
10204
|
+
const linkResult = kind === "issue" ? await linkPaperclipIssueToGitHubIssue(ctx, {
|
|
10205
|
+
companyId,
|
|
10206
|
+
issueId: paperclipIssueId,
|
|
10207
|
+
reference: payload.reference,
|
|
10208
|
+
repositoryUrl: normalizeOptionalString2(payload.repository),
|
|
10209
|
+
issueNumber: payload.issueNumber,
|
|
10210
|
+
allowUnmapped: true
|
|
10211
|
+
}) : await linkPaperclipIssueToGitHubPullRequest(ctx, {
|
|
10212
|
+
companyId,
|
|
10213
|
+
issueId: paperclipIssueId,
|
|
10214
|
+
reference: payload.reference,
|
|
10215
|
+
repositoryUrl: normalizeOptionalString2(payload.repository),
|
|
10216
|
+
pullRequestNumber: payload.pullRequestNumber,
|
|
10217
|
+
pullRequestUrl: payload.pullRequestUrl,
|
|
10218
|
+
allowUnmapped: true
|
|
10219
|
+
});
|
|
10220
|
+
ctx.logger.info("GitHub Sync recorded a GitHub issue link API route event.", {
|
|
10221
|
+
routeKey: input.routeKey,
|
|
10222
|
+
companyId,
|
|
10223
|
+
kind,
|
|
10224
|
+
paperclipIssueId,
|
|
10225
|
+
repositoryUrl: normalizeOptionalString2(linkResult.repositoryUrl),
|
|
10226
|
+
githubIssueNumber: normalizeToolPositiveInteger(linkResult.githubIssueNumber),
|
|
10227
|
+
githubPullRequestNumber: normalizeToolPositiveInteger(linkResult.githubPullRequestNumber),
|
|
10228
|
+
agentId: input.actor.agentId ?? null,
|
|
10229
|
+
runId: input.actor.runId ?? null
|
|
10230
|
+
});
|
|
10231
|
+
return {
|
|
10232
|
+
status: 201,
|
|
10233
|
+
body: {
|
|
10234
|
+
status: "linked",
|
|
10235
|
+
companyId,
|
|
10236
|
+
...linkResult
|
|
10237
|
+
}
|
|
10238
|
+
};
|
|
10239
|
+
}
|
|
9863
10240
|
async function createGitHubToolOctokit(ctx, companyId, context = {}) {
|
|
9864
10241
|
const token = (await resolveGithubToken(ctx, { companyId })).trim();
|
|
9865
10242
|
if (!token) {
|
|
@@ -12811,10 +13188,14 @@ function shouldRunScheduledSync(settings, scheduledAt) {
|
|
|
12811
13188
|
}
|
|
12812
13189
|
return now - lastCheckedAt >= settings.scheduleFrequencyMinutes * 6e4;
|
|
12813
13190
|
}
|
|
12814
|
-
function listScheduledSyncTargets(settings) {
|
|
13191
|
+
async function listScheduledSyncTargets(ctx, settings) {
|
|
13192
|
+
const externalLinkCompanyIds = await getExternalGitHubLinkCompanyIds(ctx);
|
|
12815
13193
|
const companyIds = [
|
|
12816
|
-
|
|
12817
|
-
|
|
13194
|
+
.../* @__PURE__ */ new Set(
|
|
13195
|
+
[
|
|
13196
|
+
...settings.mappings.map((mapping) => normalizeCompanyId(mapping.companyId)).filter((companyId) => Boolean(companyId)),
|
|
13197
|
+
...externalLinkCompanyIds
|
|
13198
|
+
]
|
|
12818
13199
|
)
|
|
12819
13200
|
];
|
|
12820
13201
|
if (companyIds.length === 0) {
|
|
@@ -12833,6 +13214,9 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12833
13214
|
const token = typeof options.resolvedToken === "string" ? options.resolvedToken : await resolveGithubToken(ctx, { companyId: targetCompanyId });
|
|
12834
13215
|
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(baseSettings, config, targetCompanyId);
|
|
12835
13216
|
const mappings = getSyncableMappingsForTarget(settings.mappings, options.target);
|
|
13217
|
+
const externalSyncWork = options.externalSyncWork ?? await listExternalGitHubLinkSyncWork(ctx, settings.mappings, options.target);
|
|
13218
|
+
const externalLinkAuthMappings = buildExternalLinkAuthMappings(externalSyncWork);
|
|
13219
|
+
const externalLinkCount = externalSyncWork.issueLinks.length + externalSyncWork.pullRequestLinks.length;
|
|
12836
13220
|
activePaperclipApiAuthTokensByCompanyId = null;
|
|
12837
13221
|
const failureContext = {
|
|
12838
13222
|
phase: "configuration"
|
|
@@ -12844,14 +13228,18 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12844
13228
|
};
|
|
12845
13229
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12846
13230
|
}
|
|
12847
|
-
if (mappings.length === 0) {
|
|
13231
|
+
if (mappings.length === 0 && externalLinkCount === 0) {
|
|
12848
13232
|
const next = {
|
|
12849
13233
|
...settings,
|
|
12850
13234
|
syncState: createSetupConfigurationErrorSyncState("missing_mapping", trigger)
|
|
12851
13235
|
};
|
|
12852
13236
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12853
13237
|
}
|
|
12854
|
-
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13238
|
+
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13239
|
+
settings,
|
|
13240
|
+
config,
|
|
13241
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13242
|
+
);
|
|
12855
13243
|
if (mappingsMissingBoardAccess.length > 0 && await detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl)) {
|
|
12856
13244
|
const next = {
|
|
12857
13245
|
...settings,
|
|
@@ -12859,7 +13247,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12859
13247
|
};
|
|
12860
13248
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12861
13249
|
}
|
|
12862
|
-
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
13250
|
+
if (mappings.length > 0 && (!ctx.issues || typeof ctx.issues.create !== "function")) {
|
|
12863
13251
|
const errorDetails = {
|
|
12864
13252
|
phase: "configuration",
|
|
12865
13253
|
suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
|
|
@@ -12885,7 +13273,12 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12885
13273
|
};
|
|
12886
13274
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12887
13275
|
}
|
|
12888
|
-
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13276
|
+
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13277
|
+
ctx,
|
|
13278
|
+
settings,
|
|
13279
|
+
config,
|
|
13280
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13281
|
+
);
|
|
12889
13282
|
const octokitLogContext = {
|
|
12890
13283
|
companyId: targetCompanyId,
|
|
12891
13284
|
operation: "sync.github-issues",
|
|
@@ -13137,6 +13530,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13137
13530
|
continue;
|
|
13138
13531
|
}
|
|
13139
13532
|
}
|
|
13533
|
+
totalTrackedIssueCount += externalLinkCount;
|
|
13534
|
+
syncedIssuesCount = totalTrackedIssueCount;
|
|
13140
13535
|
recordCompanyBacklogSnapshotsFromPlans(repositoryPlans);
|
|
13141
13536
|
if (repositoryPlans.length > 0) {
|
|
13142
13537
|
const firstPlan = repositoryPlans[0];
|
|
@@ -13149,6 +13544,14 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13149
13544
|
totalIssueCount: totalTrackedIssueCount,
|
|
13150
13545
|
detailLabel: "Loading linked pull requests, review threads, and CI status before syncing."
|
|
13151
13546
|
};
|
|
13547
|
+
} else if (externalLinkCount > 0) {
|
|
13548
|
+
currentProgress = {
|
|
13549
|
+
phase: "preparing",
|
|
13550
|
+
totalRepositoryCount: countExternalLinkRepositories(externalSyncWork),
|
|
13551
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13552
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13553
|
+
detailLabel: "Loading linked GitHub issues and pull requests before syncing."
|
|
13554
|
+
};
|
|
13152
13555
|
} else {
|
|
13153
13556
|
currentProgress = {
|
|
13154
13557
|
phase: "preparing",
|
|
@@ -13413,6 +13816,194 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13413
13816
|
continue;
|
|
13414
13817
|
}
|
|
13415
13818
|
}
|
|
13819
|
+
for (const [externalIssueIndex, issueLink] of externalSyncWork.issueLinks.entries()) {
|
|
13820
|
+
await throwIfSyncCancelled();
|
|
13821
|
+
try {
|
|
13822
|
+
const companyId = normalizeCompanyId(issueLink.data.companyId);
|
|
13823
|
+
if (!companyId) {
|
|
13824
|
+
continue;
|
|
13825
|
+
}
|
|
13826
|
+
const repository = requireRepositoryReference(issueLink.data.repositoryUrl);
|
|
13827
|
+
octokitLogContext.repositoryUrl = repository.url;
|
|
13828
|
+
const advancedSettings = getCompanyAdvancedSettings(settings, companyId);
|
|
13829
|
+
let availableLabels = companyLabelDirectoryCache.get(companyId);
|
|
13830
|
+
if (!availableLabels) {
|
|
13831
|
+
updateSyncFailureContext(failureContext, {
|
|
13832
|
+
phase: "loading_paperclip_labels",
|
|
13833
|
+
repositoryUrl: repository.url,
|
|
13834
|
+
githubIssueNumber: issueLink.data.githubIssueNumber
|
|
13835
|
+
});
|
|
13836
|
+
availableLabels = supportsPaperclipLabelMapping ? await buildPaperclipLabelDirectory(ctx, companyId, paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
|
|
13837
|
+
companyLabelDirectoryCache.set(companyId, availableLabels);
|
|
13838
|
+
}
|
|
13839
|
+
currentProgress = {
|
|
13840
|
+
phase: "syncing",
|
|
13841
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13842
|
+
currentRepositoryUrl: repository.url,
|
|
13843
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13844
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13845
|
+
currentIssueNumber: issueLink.data.githubIssueNumber,
|
|
13846
|
+
detailLabel: `Syncing linked GitHub issue #${issueLink.data.githubIssueNumber} in ${repository.owner}/${repository.repo}.`
|
|
13847
|
+
};
|
|
13848
|
+
await persistRunningProgress(true);
|
|
13849
|
+
updateSyncFailureContext(failureContext, {
|
|
13850
|
+
phase: "evaluating_github_status",
|
|
13851
|
+
repositoryUrl: repository.url,
|
|
13852
|
+
githubIssueNumber: issueLink.data.githubIssueNumber
|
|
13853
|
+
});
|
|
13854
|
+
const response = await octokit.rest.issues.get({
|
|
13855
|
+
owner: repository.owner,
|
|
13856
|
+
repo: repository.repo,
|
|
13857
|
+
issue_number: issueLink.data.githubIssueNumber,
|
|
13858
|
+
headers: {
|
|
13859
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
13860
|
+
}
|
|
13861
|
+
});
|
|
13862
|
+
const githubIssue = normalizeGitHubIssueRecord(response.data);
|
|
13863
|
+
if (response.data.pull_request) {
|
|
13864
|
+
continue;
|
|
13865
|
+
}
|
|
13866
|
+
const virtualMapping = {
|
|
13867
|
+
id: `external-link:${companyId}:${repository.url}`,
|
|
13868
|
+
repositoryUrl: repository.url,
|
|
13869
|
+
paperclipProjectName: "",
|
|
13870
|
+
...issueLink.data.paperclipProjectId ? { paperclipProjectId: issueLink.data.paperclipProjectId } : {},
|
|
13871
|
+
companyId
|
|
13872
|
+
};
|
|
13873
|
+
const importedIssue = {
|
|
13874
|
+
mappingId: virtualMapping.id,
|
|
13875
|
+
githubIssueId: githubIssue.id,
|
|
13876
|
+
githubIssueNumber: githubIssue.number,
|
|
13877
|
+
paperclipIssueId: issueLink.paperclipIssueId,
|
|
13878
|
+
importedAt: issueLink.createdAt ?? issueLink.data.syncedAt,
|
|
13879
|
+
lastSeenCommentCount: issueLink.data.commentsCount,
|
|
13880
|
+
lastSeenGitHubState: issueLink.data.githubIssueState,
|
|
13881
|
+
linkedPullRequestCommentCounts: [],
|
|
13882
|
+
repositoryUrl: repository.url,
|
|
13883
|
+
...issueLink.data.paperclipProjectId ? { paperclipProjectId: issueLink.data.paperclipProjectId } : {},
|
|
13884
|
+
companyId
|
|
13885
|
+
};
|
|
13886
|
+
const synchronizationResult = await synchronizePaperclipIssueStatuses(
|
|
13887
|
+
ctx,
|
|
13888
|
+
octokit,
|
|
13889
|
+
repository,
|
|
13890
|
+
virtualMapping,
|
|
13891
|
+
advancedSettings,
|
|
13892
|
+
/* @__PURE__ */ new Map([[githubIssue.id, githubIssue]]),
|
|
13893
|
+
[importedIssue],
|
|
13894
|
+
/* @__PURE__ */ new Set(),
|
|
13895
|
+
availableLabels,
|
|
13896
|
+
paperclipApiBaseUrl,
|
|
13897
|
+
/* @__PURE__ */ new Map(),
|
|
13898
|
+
/* @__PURE__ */ new Map(),
|
|
13899
|
+
/* @__PURE__ */ new Map(),
|
|
13900
|
+
repositoryMaintainerCache,
|
|
13901
|
+
failureContext,
|
|
13902
|
+
recoverableFailures,
|
|
13903
|
+
throwIfSyncCancelled,
|
|
13904
|
+
async (params) => {
|
|
13905
|
+
const recorded = recordCompanyActivityMetricEvent(companyKpiState, {
|
|
13906
|
+
companyId: params.companyId,
|
|
13907
|
+
metric: "githubIssuesClosedCount",
|
|
13908
|
+
eventKey: `${params.repositoryUrl}/issues/${params.githubIssueNumber}:${coerceDate(params.occurredAt).toISOString()}`,
|
|
13909
|
+
repositoryUrl: params.repositoryUrl,
|
|
13910
|
+
occurredAt: params.occurredAt
|
|
13911
|
+
});
|
|
13912
|
+
companyKpiState = recorded.state;
|
|
13913
|
+
companyKpiStateDirty = companyKpiStateDirty || recorded.recorded;
|
|
13914
|
+
},
|
|
13915
|
+
async (progress) => {
|
|
13916
|
+
completedTrackedIssueCount += 1;
|
|
13917
|
+
currentProgress = {
|
|
13918
|
+
phase: "syncing",
|
|
13919
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13920
|
+
currentRepositoryUrl: repository.url,
|
|
13921
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13922
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13923
|
+
...progress.currentIssueNumber !== void 0 ? { currentIssueNumber: progress.currentIssueNumber } : {}
|
|
13924
|
+
};
|
|
13925
|
+
await persistRunningProgress(externalIssueIndex === externalSyncWork.issueLinks.length - 1);
|
|
13926
|
+
}
|
|
13927
|
+
);
|
|
13928
|
+
updatedStatusesCount += synchronizationResult.updatedStatusesCount;
|
|
13929
|
+
updatedLabelsCount += synchronizationResult.updatedLabelsCount;
|
|
13930
|
+
updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
|
|
13931
|
+
} catch (error) {
|
|
13932
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
13933
|
+
throw error;
|
|
13934
|
+
}
|
|
13935
|
+
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
13936
|
+
continue;
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
const externalPullRequestLinksByCompanyId = /* @__PURE__ */ new Map();
|
|
13940
|
+
for (const pullRequestLink of externalSyncWork.pullRequestLinks) {
|
|
13941
|
+
const companyId = normalizeCompanyId(pullRequestLink.data.companyId);
|
|
13942
|
+
if (!companyId) {
|
|
13943
|
+
continue;
|
|
13944
|
+
}
|
|
13945
|
+
const companyLinks = externalPullRequestLinksByCompanyId.get(companyId) ?? [];
|
|
13946
|
+
companyLinks.push(pullRequestLink);
|
|
13947
|
+
externalPullRequestLinksByCompanyId.set(companyId, companyLinks);
|
|
13948
|
+
}
|
|
13949
|
+
for (const [companyId, pullRequestLinks] of externalPullRequestLinksByCompanyId.entries()) {
|
|
13950
|
+
await throwIfSyncCancelled();
|
|
13951
|
+
try {
|
|
13952
|
+
const firstRepositoryUrl = pullRequestLinks[0]?.data.repositoryUrl;
|
|
13953
|
+
if (!firstRepositoryUrl) {
|
|
13954
|
+
continue;
|
|
13955
|
+
}
|
|
13956
|
+
const repository = requireRepositoryReference(firstRepositoryUrl);
|
|
13957
|
+
octokitLogContext.repositoryUrl = repository.url;
|
|
13958
|
+
currentProgress = {
|
|
13959
|
+
phase: "syncing",
|
|
13960
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13961
|
+
currentRepositoryUrl: repository.url,
|
|
13962
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13963
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13964
|
+
detailLabel: `Syncing ${pullRequestLinks.length} linked GitHub pull request${pullRequestLinks.length === 1 ? "" : "s"}.`
|
|
13965
|
+
};
|
|
13966
|
+
await persistRunningProgress(true);
|
|
13967
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
13968
|
+
const synchronizationResult = await synchronizePaperclipPullRequestIssueStatuses(
|
|
13969
|
+
ctx,
|
|
13970
|
+
octokit,
|
|
13971
|
+
{
|
|
13972
|
+
id: `external-link:${companyId}:${repository.url}`,
|
|
13973
|
+
repositoryUrl: repository.url,
|
|
13974
|
+
paperclipProjectName: "",
|
|
13975
|
+
companyId
|
|
13976
|
+
},
|
|
13977
|
+
getCompanyAdvancedSettings(settings, companyId),
|
|
13978
|
+
pullRequestLinks,
|
|
13979
|
+
paperclipApiBaseUrl,
|
|
13980
|
+
pullRequestStatusCache,
|
|
13981
|
+
failureContext,
|
|
13982
|
+
recoverableFailures,
|
|
13983
|
+
throwIfSyncCancelled,
|
|
13984
|
+
async (progress) => {
|
|
13985
|
+
completedTrackedIssueCount += 1;
|
|
13986
|
+
const pullRequestRepository = requireRepositoryReference(progress.pullRequestLink.data.repositoryUrl);
|
|
13987
|
+
currentProgress = {
|
|
13988
|
+
phase: "syncing",
|
|
13989
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13990
|
+
currentRepositoryUrl: pullRequestRepository.url,
|
|
13991
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13992
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13993
|
+
detailLabel: `Synced pull request #${progress.pullRequestLink.data.githubPullRequestNumber} in ${pullRequestRepository.owner}/${pullRequestRepository.repo}.`
|
|
13994
|
+
};
|
|
13995
|
+
await persistRunningProgress(progress.completedIssueCount === progress.totalIssueCount);
|
|
13996
|
+
}
|
|
13997
|
+
);
|
|
13998
|
+
updatedStatusesCount += synchronizationResult.updatedStatusesCount;
|
|
13999
|
+
} catch (error) {
|
|
14000
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
14001
|
+
throw error;
|
|
14002
|
+
}
|
|
14003
|
+
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
14004
|
+
continue;
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
13416
14007
|
if (recoverableFailures.length > 0) {
|
|
13417
14008
|
const primaryFailure = recoverableFailures[0];
|
|
13418
14009
|
const errorDetails = buildSyncErrorDetails(primaryFailure.error, primaryFailure.context);
|
|
@@ -13541,10 +14132,12 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13541
14132
|
if (getActiveGitHubRateLimitPause(currentSettings.syncState)) {
|
|
13542
14133
|
return currentSettings;
|
|
13543
14134
|
}
|
|
13544
|
-
if (trigger !== "manual" &&
|
|
14135
|
+
if (trigger !== "manual" && !token.trim()) {
|
|
13545
14136
|
return currentSettings;
|
|
13546
14137
|
}
|
|
13547
|
-
|
|
14138
|
+
const externalSyncWork = await listExternalGitHubLinkSyncWork(ctx, currentSettings.mappings, options.target);
|
|
14139
|
+
const externalLinkCount = externalSyncWork.issueLinks.length + externalSyncWork.pullRequestLinks.length;
|
|
14140
|
+
if (trigger !== "manual" && getSyncableMappingsForTarget(currentSettings.mappings, options.target).length === 0 && externalLinkCount === 0) {
|
|
13548
14141
|
return currentSettings;
|
|
13549
14142
|
}
|
|
13550
14143
|
await setSyncCancellationRequest(ctx, null);
|
|
@@ -13558,7 +14151,7 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13558
14151
|
message: getSyncTargetRunningMessage(options.target),
|
|
13559
14152
|
progress: {
|
|
13560
14153
|
phase: "preparing",
|
|
13561
|
-
totalRepositoryCount: syncableMappings.length
|
|
14154
|
+
totalRepositoryCount: syncableMappings.length + countExternalLinkRepositories(externalSyncWork)
|
|
13562
14155
|
}
|
|
13563
14156
|
});
|
|
13564
14157
|
activeRunningSyncState = {
|
|
@@ -13573,7 +14166,8 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13573
14166
|
await runningStatePromise;
|
|
13574
14167
|
return await performSync(ctx, trigger, {
|
|
13575
14168
|
resolvedToken: token,
|
|
13576
|
-
target: options.target
|
|
14169
|
+
target: options.target,
|
|
14170
|
+
externalSyncWork
|
|
13577
14171
|
});
|
|
13578
14172
|
} catch (error) {
|
|
13579
14173
|
return await createUnexpectedSyncErrorResult(ctx, trigger, error, targetCompanyId);
|
|
@@ -14468,6 +15062,42 @@ function registerGitHubAgentTools(ctx) {
|
|
|
14468
15062
|
);
|
|
14469
15063
|
})
|
|
14470
15064
|
);
|
|
15065
|
+
ctx.tools.register(
|
|
15066
|
+
"link_github_item",
|
|
15067
|
+
getGitHubAgentToolDeclaration("link_github_item"),
|
|
15068
|
+
async (params, runCtx) => executeGitHubTool(async () => {
|
|
15069
|
+
const input = getToolInputRecord(params);
|
|
15070
|
+
const kind = normalizeIssueGitHubLinkKind(input.kind);
|
|
15071
|
+
if (!kind) {
|
|
15072
|
+
throw new Error('kind must be "issue" or "pull_request".');
|
|
15073
|
+
}
|
|
15074
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
15075
|
+
if (!paperclipIssueId) {
|
|
15076
|
+
throw new Error("paperclipIssueId is required.");
|
|
15077
|
+
}
|
|
15078
|
+
const linkResult = kind === "issue" ? await linkPaperclipIssueToGitHubIssue(ctx, {
|
|
15079
|
+
companyId: runCtx.companyId,
|
|
15080
|
+
issueId: paperclipIssueId,
|
|
15081
|
+
reference: input.reference,
|
|
15082
|
+
repositoryUrl: normalizeOptionalToolString(input.repository),
|
|
15083
|
+
issueNumber: input.issueNumber,
|
|
15084
|
+
allowUnmapped: true
|
|
15085
|
+
}) : await linkPaperclipIssueToGitHubPullRequest(ctx, {
|
|
15086
|
+
companyId: runCtx.companyId,
|
|
15087
|
+
issueId: paperclipIssueId,
|
|
15088
|
+
reference: input.reference,
|
|
15089
|
+
repositoryUrl: normalizeOptionalToolString(input.repository),
|
|
15090
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
15091
|
+
pullRequestUrl: input.pullRequestUrl,
|
|
15092
|
+
allowUnmapped: true
|
|
15093
|
+
});
|
|
15094
|
+
const itemLabel = kind === "issue" ? `issue #${normalizeToolPositiveInteger(linkResult.githubIssueNumber) ?? ""}`.trim() : `pull request #${normalizeToolPositiveInteger(linkResult.githubPullRequestNumber) ?? ""}`.trim();
|
|
15095
|
+
return buildToolSuccessResult(
|
|
15096
|
+
`Linked Paperclip issue ${paperclipIssueId} to GitHub ${itemLabel}.`,
|
|
15097
|
+
linkResult
|
|
15098
|
+
);
|
|
15099
|
+
})
|
|
15100
|
+
);
|
|
14471
15101
|
}
|
|
14472
15102
|
function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
14473
15103
|
if (typeof entry !== "string" || !entry.trim()) {
|
|
@@ -14876,7 +15506,7 @@ var plugin = definePlugin({
|
|
|
14876
15506
|
ctx.jobs.register("sync.github-issues", async (job) => {
|
|
14877
15507
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
14878
15508
|
const trigger = job.trigger === "retry" ? "retry" : "schedule";
|
|
14879
|
-
const scheduledTargets = listScheduledSyncTargets(settings);
|
|
15509
|
+
const scheduledTargets = await listScheduledSyncTargets(ctx, settings);
|
|
14880
15510
|
if (scheduledTargets.length === 0) {
|
|
14881
15511
|
const reconciledSettings = await reconcileOrphanedRunningSyncState(ctx);
|
|
14882
15512
|
if (job.trigger === "schedule" && !shouldRunScheduledSync(reconciledSettings, job.scheduledAt)) {
|
|
@@ -14898,7 +15528,18 @@ var plugin = definePlugin({
|
|
|
14898
15528
|
if (!pluginRuntimeContext) {
|
|
14899
15529
|
throw new Error("GitHub Sync worker is not ready to handle API routes yet.");
|
|
14900
15530
|
}
|
|
14901
|
-
|
|
15531
|
+
if (input.routeKey === COMPANY_METRIC_API_ROUTE_KEY) {
|
|
15532
|
+
return handleCompanyMetricApiRoute(pluginRuntimeContext, input);
|
|
15533
|
+
}
|
|
15534
|
+
if (input.routeKey === ISSUE_LINK_API_ROUTE_KEY) {
|
|
15535
|
+
return handleIssueLinkApiRoute(pluginRuntimeContext, input);
|
|
15536
|
+
}
|
|
15537
|
+
return {
|
|
15538
|
+
status: 404,
|
|
15539
|
+
body: {
|
|
15540
|
+
error: `Unsupported plugin API route: ${input.routeKey}.`
|
|
15541
|
+
}
|
|
15542
|
+
};
|
|
14902
15543
|
},
|
|
14903
15544
|
async onShutdown() {
|
|
14904
15545
|
pluginRuntimeContext = null;
|