paperclip-github-plugin 0.8.7 → 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 +31 -1
- package/dist/manifest.js +60 -1
- package/dist/worker.js +673 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -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) {
|
|
@@ -6967,6 +7062,126 @@ async function listGitHubPullRequestIssueLinksForMapping(ctx, mapping, target) {
|
|
|
6967
7062
|
}
|
|
6968
7063
|
return [...recordsByKey.values()];
|
|
6969
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
|
+
}
|
|
6970
7185
|
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
6971
7186
|
const issueId = params.issueId.trim();
|
|
6972
7187
|
const commentId = params.commentId.trim();
|
|
@@ -7052,7 +7267,7 @@ async function upsertGitHubPullRequestLinkRecord(ctx, params) {
|
|
|
7052
7267
|
status: params.pullRequestState,
|
|
7053
7268
|
data: {
|
|
7054
7269
|
companyId: params.companyId,
|
|
7055
|
-
paperclipProjectId: params.projectId,
|
|
7270
|
+
...params.projectId ? { paperclipProjectId: params.projectId } : {},
|
|
7056
7271
|
repositoryUrl: getNormalizedMappingRepositoryUrl({
|
|
7057
7272
|
repositoryUrl: params.repositoryUrl
|
|
7058
7273
|
}),
|
|
@@ -7091,6 +7306,56 @@ async function assertPaperclipIssueHasNoManualGitHubLink(ctx, params) {
|
|
|
7091
7306
|
throw new Error("This Paperclip issue is already linked to a GitHub pull request.");
|
|
7092
7307
|
}
|
|
7093
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
|
+
}
|
|
7094
7359
|
async function resolveIssueGitHubLinkMapping(ctx, params) {
|
|
7095
7360
|
const issue = await ctx.issues.get(params.issueId, params.companyId);
|
|
7096
7361
|
if (!issue) {
|
|
@@ -7213,10 +7478,11 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7213
7478
|
repositoryUrl: params.repositoryUrl,
|
|
7214
7479
|
issueNumber: params.issueNumber
|
|
7215
7480
|
});
|
|
7216
|
-
const scope = await
|
|
7481
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7217
7482
|
companyId,
|
|
7218
7483
|
issueId,
|
|
7219
|
-
repositoryUrl: reference.repositoryUrl
|
|
7484
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7485
|
+
allowUnmapped: params.allowUnmapped
|
|
7220
7486
|
});
|
|
7221
7487
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7222
7488
|
const response = await octokit.rest.issues.get({
|
|
@@ -7233,18 +7499,29 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7233
7499
|
}
|
|
7234
7500
|
const githubIssue = normalizeGitHubIssueRecord(rawIssue);
|
|
7235
7501
|
const linkedPullRequests = await listLinkedPullRequestsForIssue(octokit, scope.repository, githubIssue.number);
|
|
7236
|
-
|
|
7237
|
-
const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
|
|
7238
|
-
upsertImportedIssueRecord(
|
|
7239
|
-
importRegistry,
|
|
7240
|
-
buildImportedIssueRecord(scope.mapping, githubIssue, issueId, (/* @__PURE__ */ new Date()).toISOString())
|
|
7241
|
-
);
|
|
7242
|
-
await ctx.state.set(IMPORT_REGISTRY_SCOPE, importRegistry);
|
|
7243
|
-
invalidateProjectPullRequestCaches({
|
|
7502
|
+
const linkTarget = {
|
|
7244
7503
|
companyId,
|
|
7245
|
-
projectId: scope.mapping
|
|
7246
|
-
|
|
7247
|
-
}
|
|
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
|
+
}
|
|
7248
7525
|
return {
|
|
7249
7526
|
kind: "issue",
|
|
7250
7527
|
paperclipIssueId: issueId,
|
|
@@ -7272,10 +7549,11 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7272
7549
|
pullRequestNumber: params.pullRequestNumber,
|
|
7273
7550
|
pullRequestUrl: params.pullRequestUrl
|
|
7274
7551
|
});
|
|
7275
|
-
const scope = await
|
|
7552
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7276
7553
|
companyId,
|
|
7277
7554
|
issueId,
|
|
7278
|
-
repositoryUrl: reference.repositoryUrl
|
|
7555
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7556
|
+
allowUnmapped: params.allowUnmapped
|
|
7279
7557
|
});
|
|
7280
7558
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7281
7559
|
const response = await octokit.rest.pulls.get({
|
|
@@ -7293,7 +7571,7 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7293
7571
|
});
|
|
7294
7572
|
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
7295
7573
|
companyId,
|
|
7296
|
-
projectId: scope.mapping
|
|
7574
|
+
projectId: scope.mapping?.paperclipProjectId ?? scope.projectId,
|
|
7297
7575
|
issueId,
|
|
7298
7576
|
repositoryUrl: scope.repository.url,
|
|
7299
7577
|
pullRequestNumber: reference.pullRequestNumber,
|
|
@@ -7301,11 +7579,15 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7301
7579
|
pullRequestTitle: response.data.title || `Pull request #${reference.pullRequestNumber}`,
|
|
7302
7580
|
pullRequestState
|
|
7303
7581
|
});
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
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
|
+
}
|
|
7309
7591
|
return {
|
|
7310
7592
|
kind: "pull_request",
|
|
7311
7593
|
paperclipIssueId: issueId,
|
|
@@ -7433,6 +7715,7 @@ async function unlinkPaperclipIssueFromGitHub(ctx, params) {
|
|
|
7433
7715
|
if (Object.keys(issuePatch).length > 0) {
|
|
7434
7716
|
await ctx.issues.update(issueId, issuePatch, companyId);
|
|
7435
7717
|
}
|
|
7718
|
+
await forgetExternalGitHubLinkCompanyIfEmpty(ctx, companyId);
|
|
7436
7719
|
return {
|
|
7437
7720
|
paperclipIssueId: issueId,
|
|
7438
7721
|
unlinked: issueLinkRecords.length > 0 || pullRequestLinkRecords.length > 0 || removedImportRegistryEntries.length > 0 || shouldClearGitHubOrigin || nextDescription !== void 0,
|
|
@@ -9329,7 +9612,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9329
9612
|
};
|
|
9330
9613
|
}
|
|
9331
9614
|
async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
9332
|
-
if (!mapping.companyId || !
|
|
9615
|
+
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
9333
9616
|
return {
|
|
9334
9617
|
updatedStatusesCount: 0
|
|
9335
9618
|
};
|
|
@@ -9778,14 +10061,6 @@ function parseCompanyMetricApiRouteBody(input) {
|
|
|
9778
10061
|
return input.body;
|
|
9779
10062
|
}
|
|
9780
10063
|
async function handleCompanyMetricApiRoute(ctx, input) {
|
|
9781
|
-
if (input.routeKey !== COMPANY_METRIC_API_ROUTE_KEY) {
|
|
9782
|
-
return {
|
|
9783
|
-
status: 404,
|
|
9784
|
-
body: {
|
|
9785
|
-
error: `Unsupported plugin API route: ${input.routeKey}.`
|
|
9786
|
-
}
|
|
9787
|
-
};
|
|
9788
|
-
}
|
|
9789
10064
|
if (input.actor.actorType !== "agent") {
|
|
9790
10065
|
throw new Error("Company KPI metric events must be recorded by an authenticated Paperclip agent.");
|
|
9791
10066
|
}
|
|
@@ -9885,6 +10160,83 @@ async function handleCompanyMetricApiRoute(ctx, input) {
|
|
|
9885
10160
|
}
|
|
9886
10161
|
};
|
|
9887
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
|
+
}
|
|
9888
10240
|
async function createGitHubToolOctokit(ctx, companyId, context = {}) {
|
|
9889
10241
|
const token = (await resolveGithubToken(ctx, { companyId })).trim();
|
|
9890
10242
|
if (!token) {
|
|
@@ -12836,10 +13188,14 @@ function shouldRunScheduledSync(settings, scheduledAt) {
|
|
|
12836
13188
|
}
|
|
12837
13189
|
return now - lastCheckedAt >= settings.scheduleFrequencyMinutes * 6e4;
|
|
12838
13190
|
}
|
|
12839
|
-
function listScheduledSyncTargets(settings) {
|
|
13191
|
+
async function listScheduledSyncTargets(ctx, settings) {
|
|
13192
|
+
const externalLinkCompanyIds = await getExternalGitHubLinkCompanyIds(ctx);
|
|
12840
13193
|
const companyIds = [
|
|
12841
|
-
|
|
12842
|
-
|
|
13194
|
+
.../* @__PURE__ */ new Set(
|
|
13195
|
+
[
|
|
13196
|
+
...settings.mappings.map((mapping) => normalizeCompanyId(mapping.companyId)).filter((companyId) => Boolean(companyId)),
|
|
13197
|
+
...externalLinkCompanyIds
|
|
13198
|
+
]
|
|
12843
13199
|
)
|
|
12844
13200
|
];
|
|
12845
13201
|
if (companyIds.length === 0) {
|
|
@@ -12858,6 +13214,9 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12858
13214
|
const token = typeof options.resolvedToken === "string" ? options.resolvedToken : await resolveGithubToken(ctx, { companyId: targetCompanyId });
|
|
12859
13215
|
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(baseSettings, config, targetCompanyId);
|
|
12860
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;
|
|
12861
13220
|
activePaperclipApiAuthTokensByCompanyId = null;
|
|
12862
13221
|
const failureContext = {
|
|
12863
13222
|
phase: "configuration"
|
|
@@ -12869,14 +13228,18 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12869
13228
|
};
|
|
12870
13229
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12871
13230
|
}
|
|
12872
|
-
if (mappings.length === 0) {
|
|
13231
|
+
if (mappings.length === 0 && externalLinkCount === 0) {
|
|
12873
13232
|
const next = {
|
|
12874
13233
|
...settings,
|
|
12875
13234
|
syncState: createSetupConfigurationErrorSyncState("missing_mapping", trigger)
|
|
12876
13235
|
};
|
|
12877
13236
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12878
13237
|
}
|
|
12879
|
-
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13238
|
+
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13239
|
+
settings,
|
|
13240
|
+
config,
|
|
13241
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13242
|
+
);
|
|
12880
13243
|
if (mappingsMissingBoardAccess.length > 0 && await detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl)) {
|
|
12881
13244
|
const next = {
|
|
12882
13245
|
...settings,
|
|
@@ -12884,7 +13247,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12884
13247
|
};
|
|
12885
13248
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12886
13249
|
}
|
|
12887
|
-
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
13250
|
+
if (mappings.length > 0 && (!ctx.issues || typeof ctx.issues.create !== "function")) {
|
|
12888
13251
|
const errorDetails = {
|
|
12889
13252
|
phase: "configuration",
|
|
12890
13253
|
suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
|
|
@@ -12910,7 +13273,12 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12910
13273
|
};
|
|
12911
13274
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12912
13275
|
}
|
|
12913
|
-
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13276
|
+
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13277
|
+
ctx,
|
|
13278
|
+
settings,
|
|
13279
|
+
config,
|
|
13280
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13281
|
+
);
|
|
12914
13282
|
const octokitLogContext = {
|
|
12915
13283
|
companyId: targetCompanyId,
|
|
12916
13284
|
operation: "sync.github-issues",
|
|
@@ -13162,6 +13530,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13162
13530
|
continue;
|
|
13163
13531
|
}
|
|
13164
13532
|
}
|
|
13533
|
+
totalTrackedIssueCount += externalLinkCount;
|
|
13534
|
+
syncedIssuesCount = totalTrackedIssueCount;
|
|
13165
13535
|
recordCompanyBacklogSnapshotsFromPlans(repositoryPlans);
|
|
13166
13536
|
if (repositoryPlans.length > 0) {
|
|
13167
13537
|
const firstPlan = repositoryPlans[0];
|
|
@@ -13174,6 +13544,14 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13174
13544
|
totalIssueCount: totalTrackedIssueCount,
|
|
13175
13545
|
detailLabel: "Loading linked pull requests, review threads, and CI status before syncing."
|
|
13176
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
|
+
};
|
|
13177
13555
|
} else {
|
|
13178
13556
|
currentProgress = {
|
|
13179
13557
|
phase: "preparing",
|
|
@@ -13438,6 +13816,194 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13438
13816
|
continue;
|
|
13439
13817
|
}
|
|
13440
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
|
+
}
|
|
13441
14007
|
if (recoverableFailures.length > 0) {
|
|
13442
14008
|
const primaryFailure = recoverableFailures[0];
|
|
13443
14009
|
const errorDetails = buildSyncErrorDetails(primaryFailure.error, primaryFailure.context);
|
|
@@ -13566,10 +14132,12 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13566
14132
|
if (getActiveGitHubRateLimitPause(currentSettings.syncState)) {
|
|
13567
14133
|
return currentSettings;
|
|
13568
14134
|
}
|
|
13569
|
-
if (trigger !== "manual" &&
|
|
14135
|
+
if (trigger !== "manual" && !token.trim()) {
|
|
13570
14136
|
return currentSettings;
|
|
13571
14137
|
}
|
|
13572
|
-
|
|
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) {
|
|
13573
14141
|
return currentSettings;
|
|
13574
14142
|
}
|
|
13575
14143
|
await setSyncCancellationRequest(ctx, null);
|
|
@@ -13583,7 +14151,7 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13583
14151
|
message: getSyncTargetRunningMessage(options.target),
|
|
13584
14152
|
progress: {
|
|
13585
14153
|
phase: "preparing",
|
|
13586
|
-
totalRepositoryCount: syncableMappings.length
|
|
14154
|
+
totalRepositoryCount: syncableMappings.length + countExternalLinkRepositories(externalSyncWork)
|
|
13587
14155
|
}
|
|
13588
14156
|
});
|
|
13589
14157
|
activeRunningSyncState = {
|
|
@@ -13598,7 +14166,8 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13598
14166
|
await runningStatePromise;
|
|
13599
14167
|
return await performSync(ctx, trigger, {
|
|
13600
14168
|
resolvedToken: token,
|
|
13601
|
-
target: options.target
|
|
14169
|
+
target: options.target,
|
|
14170
|
+
externalSyncWork
|
|
13602
14171
|
});
|
|
13603
14172
|
} catch (error) {
|
|
13604
14173
|
return await createUnexpectedSyncErrorResult(ctx, trigger, error, targetCompanyId);
|
|
@@ -14493,6 +15062,42 @@ function registerGitHubAgentTools(ctx) {
|
|
|
14493
15062
|
);
|
|
14494
15063
|
})
|
|
14495
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
|
+
);
|
|
14496
15101
|
}
|
|
14497
15102
|
function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
14498
15103
|
if (typeof entry !== "string" || !entry.trim()) {
|
|
@@ -14901,7 +15506,7 @@ var plugin = definePlugin({
|
|
|
14901
15506
|
ctx.jobs.register("sync.github-issues", async (job) => {
|
|
14902
15507
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
14903
15508
|
const trigger = job.trigger === "retry" ? "retry" : "schedule";
|
|
14904
|
-
const scheduledTargets = listScheduledSyncTargets(settings);
|
|
15509
|
+
const scheduledTargets = await listScheduledSyncTargets(ctx, settings);
|
|
14905
15510
|
if (scheduledTargets.length === 0) {
|
|
14906
15511
|
const reconciledSettings = await reconcileOrphanedRunningSyncState(ctx);
|
|
14907
15512
|
if (job.trigger === "schedule" && !shouldRunScheduledSync(reconciledSettings, job.scheduledAt)) {
|
|
@@ -14923,7 +15528,18 @@ var plugin = definePlugin({
|
|
|
14923
15528
|
if (!pluginRuntimeContext) {
|
|
14924
15529
|
throw new Error("GitHub Sync worker is not ready to handle API routes yet.");
|
|
14925
15530
|
}
|
|
14926
|
-
|
|
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
|
+
};
|
|
14927
15543
|
},
|
|
14928
15544
|
async onShutdown() {
|
|
14929
15545
|
pluginRuntimeContext = null;
|