paperclip-github-plugin 0.8.7 → 0.8.9
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 +40 -3
- package/dist/manifest.js +60 -1
- package/dist/ui/index.js +71 -10
- package/dist/ui/index.js.map +2 -2
- package/dist/worker.js +722 -81
- package/package.json +1 -1
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"];
|
|
@@ -726,15 +782,15 @@ var PaperclipLabelSyncError = class extends Error {
|
|
|
726
782
|
const location = params.paperclipApiBaseUrl ? ` at ${params.paperclipApiBaseUrl}` : "";
|
|
727
783
|
let message;
|
|
728
784
|
if (failure?.requiresAuthentication) {
|
|
729
|
-
message = `Could not map ${labelSubject} because the worker reached an authenticated Paperclip API response${location} instead of JSON. Connect Paperclip board access in plugin settings, set
|
|
785
|
+
message = `Could not map ${labelSubject} because the worker reached an authenticated Paperclip API response${location} instead of JSON. Connect Paperclip board access in plugin settings, set Worker Paperclip API URL to a worker-accessible Paperclip API origin, or expose the local Paperclip API to the worker without browser-session auth.`;
|
|
730
786
|
} else if (failure?.status === 404 || failure?.status === 405) {
|
|
731
|
-
message = `Could not map ${labelSubject} because the Paperclip label API${location} is not available to the worker. Set
|
|
787
|
+
message = `Could not map ${labelSubject} because the Paperclip label API${location} is not available to the worker. Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.`;
|
|
732
788
|
} else if (failure?.errorMessage) {
|
|
733
789
|
message = `Could not map ${labelSubject} because the Paperclip label API${location} failed: ${failure.errorMessage}`;
|
|
734
790
|
} else if (params.paperclipApiBaseUrl) {
|
|
735
791
|
message = `Could not map ${labelSubject} because the Paperclip label API at ${params.paperclipApiBaseUrl} is unavailable to the worker.`;
|
|
736
792
|
} else {
|
|
737
|
-
message = `Could not map ${labelSubject} because no worker-accessible Paperclip label API is configured. Set
|
|
793
|
+
message = `Could not map ${labelSubject} because no worker-accessible Paperclip label API is configured. Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.`;
|
|
738
794
|
}
|
|
739
795
|
super(message);
|
|
740
796
|
this.status = failure?.status;
|
|
@@ -1458,6 +1514,39 @@ function getErrorMessage(error) {
|
|
|
1458
1514
|
}
|
|
1459
1515
|
return String(error);
|
|
1460
1516
|
}
|
|
1517
|
+
function getErrorCause(error) {
|
|
1518
|
+
if (!error || typeof error !== "object" || !("cause" in error)) {
|
|
1519
|
+
return void 0;
|
|
1520
|
+
}
|
|
1521
|
+
return error.cause;
|
|
1522
|
+
}
|
|
1523
|
+
function getErrorCode(error) {
|
|
1524
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
1525
|
+
return void 0;
|
|
1526
|
+
}
|
|
1527
|
+
const code = error.code;
|
|
1528
|
+
return typeof code === "string" && code.trim() ? code.trim() : void 0;
|
|
1529
|
+
}
|
|
1530
|
+
function getErrorDiagnosticMessage(error) {
|
|
1531
|
+
const primaryMessage = getErrorMessage(error).trim();
|
|
1532
|
+
const cause = getErrorCause(error);
|
|
1533
|
+
const causeMessage = cause ? getErrorMessage(cause).trim() : "";
|
|
1534
|
+
const errorCode = getErrorCode(error);
|
|
1535
|
+
const causeCode = cause ? getErrorCode(cause) : void 0;
|
|
1536
|
+
const code = errorCode ?? causeCode;
|
|
1537
|
+
const parts = [primaryMessage || String(error)];
|
|
1538
|
+
if (causeMessage && causeMessage !== primaryMessage) {
|
|
1539
|
+
parts.push(`cause: ${causeMessage}`);
|
|
1540
|
+
}
|
|
1541
|
+
if (code) {
|
|
1542
|
+
parts.push(`code: ${code}`);
|
|
1543
|
+
}
|
|
1544
|
+
return parts.join(" | ");
|
|
1545
|
+
}
|
|
1546
|
+
function formatPaperclipApiFetchErrorMessage(error, url, init) {
|
|
1547
|
+
const method = typeof init?.method === "string" && init.method.trim() ? init.method.trim().toUpperCase() : "GET";
|
|
1548
|
+
return `Paperclip API fetch failed (${method} ${url}): ${getErrorDiagnosticMessage(error)}`;
|
|
1549
|
+
}
|
|
1461
1550
|
function isPaperclipLabelSyncError(error) {
|
|
1462
1551
|
return error instanceof PaperclipLabelSyncError;
|
|
1463
1552
|
}
|
|
@@ -2079,12 +2168,12 @@ function getSyncFailureSuggestedAction(error, context) {
|
|
|
2079
2168
|
}
|
|
2080
2169
|
if (isPaperclipLabelSyncError(error)) {
|
|
2081
2170
|
if (error.requiresAuthentication || error.status === 401 || error.status === 403) {
|
|
2082
|
-
return "The worker could not reuse the board login session for the Paperclip label API. Connect Paperclip board access in settings, or set
|
|
2171
|
+
return "The worker could not reuse the board login session for the Paperclip label API. Connect Paperclip board access in settings, or set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.";
|
|
2083
2172
|
}
|
|
2084
2173
|
if (error.paperclipApiBaseUrl) {
|
|
2085
2174
|
return `Confirm that the Paperclip label API at ${error.paperclipApiBaseUrl} is reachable from the plugin worker and returns JSON, then retry sync.`;
|
|
2086
2175
|
}
|
|
2087
|
-
return "Set
|
|
2176
|
+
return "Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.";
|
|
2088
2177
|
}
|
|
2089
2178
|
const rawMessage = getErrorMessage(error).trim().toLowerCase();
|
|
2090
2179
|
if (rawMessage.includes("could not resolve to a pullrequest")) {
|
|
@@ -2544,19 +2633,6 @@ async function resolveManualSyncTarget(ctx, settings, input) {
|
|
|
2544
2633
|
if (!pullRequestLink) {
|
|
2545
2634
|
throw new Error("This Paperclip issue is not linked to a GitHub issue or pull request yet. Run a broader sync first.");
|
|
2546
2635
|
}
|
|
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
2636
|
return {
|
|
2561
2637
|
kind: "issue",
|
|
2562
2638
|
companyId: companyId2,
|
|
@@ -3912,17 +3988,7 @@ function normalizePaperclipApiBaseUrlByCompanyId(value) {
|
|
|
3912
3988
|
}).filter((entry) => entry !== null);
|
|
3913
3989
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3914
3990
|
}
|
|
3915
|
-
function getRuntimePaperclipApiBaseUrl() {
|
|
3916
|
-
if (typeof process === "undefined" || !process?.env) {
|
|
3917
|
-
return void 0;
|
|
3918
|
-
}
|
|
3919
|
-
return normalizePaperclipApiBaseUrl(process.env.PAPERCLIP_API_URL);
|
|
3920
|
-
}
|
|
3921
3991
|
function resolvePaperclipApiBaseUrl(...values) {
|
|
3922
|
-
const runtimePaperclipApiBaseUrl = getRuntimePaperclipApiBaseUrl();
|
|
3923
|
-
if (runtimePaperclipApiBaseUrl) {
|
|
3924
|
-
return runtimePaperclipApiBaseUrl;
|
|
3925
|
-
}
|
|
3926
3992
|
for (const value of values) {
|
|
3927
3993
|
const normalizedValue = normalizePaperclipApiBaseUrl(value);
|
|
3928
3994
|
if (normalizedValue) {
|
|
@@ -3940,10 +4006,6 @@ function getConfiguredPaperclipApiBaseUrl(settings, config, companyId) {
|
|
|
3940
4006
|
) : resolvePaperclipApiBaseUrl(config?.paperclipApiBaseUrl, settings?.paperclipApiBaseUrl);
|
|
3941
4007
|
}
|
|
3942
4008
|
function resolveTrustedPaperclipApiBaseUrlInput(value, settings, config, companyId) {
|
|
3943
|
-
const runtimePaperclipApiBaseUrl = getRuntimePaperclipApiBaseUrl();
|
|
3944
|
-
if (runtimePaperclipApiBaseUrl) {
|
|
3945
|
-
return runtimePaperclipApiBaseUrl;
|
|
3946
|
-
}
|
|
3947
4009
|
const requestedPaperclipApiBaseUrl = normalizePaperclipApiBaseUrl(value);
|
|
3948
4010
|
const configuredPaperclipApiBaseUrl = normalizePaperclipApiBaseUrl(config?.paperclipApiBaseUrl);
|
|
3949
4011
|
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
@@ -4481,6 +4543,58 @@ function normalizeImportRegistry(value) {
|
|
|
4481
4543
|
};
|
|
4482
4544
|
}).filter((entry) => entry !== null);
|
|
4483
4545
|
}
|
|
4546
|
+
function normalizeExternalGitHubLinkCompanyIndex(value) {
|
|
4547
|
+
const rawCompanyIds = Array.isArray(value) ? value : value && typeof value === "object" && Array.isArray(value.companyIds) ? value.companyIds : [];
|
|
4548
|
+
const companyIds = [
|
|
4549
|
+
...new Set(
|
|
4550
|
+
rawCompanyIds.map((entry) => normalizeCompanyId(entry)).filter((companyId) => Boolean(companyId))
|
|
4551
|
+
)
|
|
4552
|
+
].sort();
|
|
4553
|
+
const updatedAt = value && typeof value === "object" && typeof value.updatedAt === "string" ? value.updatedAt.trim() : void 0;
|
|
4554
|
+
return {
|
|
4555
|
+
companyIds,
|
|
4556
|
+
...updatedAt ? { updatedAt } : {}
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4559
|
+
async function getExternalGitHubLinkCompanyIds(ctx) {
|
|
4560
|
+
return normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE)).companyIds;
|
|
4561
|
+
}
|
|
4562
|
+
async function rememberExternalGitHubLinkCompany(ctx, companyId) {
|
|
4563
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4564
|
+
if (!normalizedCompanyId) {
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
const index = normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE));
|
|
4568
|
+
if (index.companyIds.includes(normalizedCompanyId)) {
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
await ctx.state.set(EXTERNAL_LINK_COMPANY_INDEX_SCOPE, {
|
|
4572
|
+
companyIds: [...index.companyIds, normalizedCompanyId].sort(),
|
|
4573
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4574
|
+
});
|
|
4575
|
+
}
|
|
4576
|
+
async function forgetExternalGitHubLinkCompanyIfEmpty(ctx, companyId) {
|
|
4577
|
+
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
4578
|
+
if (!normalizedCompanyId) {
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
const [issueLinks, pullRequestLinks] = await Promise.all([
|
|
4582
|
+
listGitHubIssueLinkRecords(ctx),
|
|
4583
|
+
listGitHubPullRequestLinkRecords(ctx)
|
|
4584
|
+
]);
|
|
4585
|
+
const hasRemainingLinks = issueLinks.some((record) => record.data.companyId === normalizedCompanyId) || pullRequestLinks.some((record) => record.data.companyId === normalizedCompanyId);
|
|
4586
|
+
if (hasRemainingLinks) {
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
const index = normalizeExternalGitHubLinkCompanyIndex(await ctx.state.get(EXTERNAL_LINK_COMPANY_INDEX_SCOPE));
|
|
4590
|
+
if (!index.companyIds.includes(normalizedCompanyId)) {
|
|
4591
|
+
return;
|
|
4592
|
+
}
|
|
4593
|
+
await ctx.state.set(EXTERNAL_LINK_COMPANY_INDEX_SCOPE, {
|
|
4594
|
+
companyIds: index.companyIds.filter((entry) => entry !== normalizedCompanyId),
|
|
4595
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4484
4598
|
function requireRepositoryReference(repositoryInput) {
|
|
4485
4599
|
const parsed = parseRepositoryReference(repositoryInput);
|
|
4486
4600
|
if (!parsed) {
|
|
@@ -5542,9 +5656,6 @@ function resolvePaperclipIssueStatus(params) {
|
|
|
5542
5656
|
}
|
|
5543
5657
|
function resolvePaperclipPullRequestIssueStatus(params) {
|
|
5544
5658
|
const { currentStatus, pullRequest, hasExecutorHandoffTarget } = params;
|
|
5545
|
-
if (currentStatus === "done" || currentStatus === "cancelled") {
|
|
5546
|
-
return currentStatus;
|
|
5547
|
-
}
|
|
5548
5659
|
if (shouldPreserveBlockedExternalPullRequestWait({
|
|
5549
5660
|
currentStatus,
|
|
5550
5661
|
linkedPullRequests: [pullRequest]
|
|
@@ -5553,7 +5664,7 @@ function resolvePaperclipPullRequestIssueStatus(params) {
|
|
|
5553
5664
|
}
|
|
5554
5665
|
return resolvePaperclipStatusFromLinkedPullRequests([pullRequest], {
|
|
5555
5666
|
preferInProgress: hasExecutorHandoffTarget,
|
|
5556
|
-
preserveTransientUnknownMergeabilityWait: currentStatus === "in_review"
|
|
5667
|
+
preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
|
|
5557
5668
|
});
|
|
5558
5669
|
}
|
|
5559
5670
|
async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
|
|
@@ -6967,6 +7078,126 @@ async function listGitHubPullRequestIssueLinksForMapping(ctx, mapping, target) {
|
|
|
6967
7078
|
}
|
|
6968
7079
|
return [...recordsByKey.values()];
|
|
6969
7080
|
}
|
|
7081
|
+
function doesGitHubIssueLinkRecordMatchMapping(record, mapping) {
|
|
7082
|
+
if (record.data.repositoryUrl !== getNormalizedMappingRepositoryUrl(mapping)) {
|
|
7083
|
+
return false;
|
|
7084
|
+
}
|
|
7085
|
+
if (record.data.companyId && record.data.companyId !== mapping.companyId) {
|
|
7086
|
+
return false;
|
|
7087
|
+
}
|
|
7088
|
+
if (record.data.paperclipProjectId && record.data.paperclipProjectId !== mapping.paperclipProjectId) {
|
|
7089
|
+
return false;
|
|
7090
|
+
}
|
|
7091
|
+
return Boolean(mapping.companyId && mapping.paperclipProjectId);
|
|
7092
|
+
}
|
|
7093
|
+
function doesGitHubIssueLinkRecordMatchTarget(record, target) {
|
|
7094
|
+
if (!target) {
|
|
7095
|
+
return true;
|
|
7096
|
+
}
|
|
7097
|
+
switch (target.kind) {
|
|
7098
|
+
case "company":
|
|
7099
|
+
return !record.data.companyId || record.data.companyId === target.companyId;
|
|
7100
|
+
case "project":
|
|
7101
|
+
return (!record.data.companyId || record.data.companyId === target.companyId) && (!record.data.paperclipProjectId || record.data.paperclipProjectId === target.projectId);
|
|
7102
|
+
case "issue":
|
|
7103
|
+
return Boolean(target.issueId && record.paperclipIssueId === target.issueId) && (!record.data.companyId || record.data.companyId === target.companyId);
|
|
7104
|
+
default:
|
|
7105
|
+
return true;
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
function isGitHubIssueLinkCoveredByMappings(record, mappings) {
|
|
7109
|
+
return mappings.some((mapping) => doesGitHubIssueLinkRecordMatchMapping(record, mapping));
|
|
7110
|
+
}
|
|
7111
|
+
function isGitHubPullRequestLinkCoveredByMappings(record, mappings) {
|
|
7112
|
+
return mappings.some((mapping) => doesGitHubPullRequestLinkRecordMatchMapping(record, mapping));
|
|
7113
|
+
}
|
|
7114
|
+
async function listExternalGitHubLinkSyncWork(ctx, mappings, target) {
|
|
7115
|
+
const syncableMappings = getSyncableMappingsForTarget(mappings, target);
|
|
7116
|
+
const issueLinksByKey = /* @__PURE__ */ new Map();
|
|
7117
|
+
const pullRequestLinksByKey = /* @__PURE__ */ new Map();
|
|
7118
|
+
const [issueLinks, pullRequestLinks] = await Promise.all([
|
|
7119
|
+
listGitHubIssueLinkRecords(ctx, {
|
|
7120
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
7121
|
+
}),
|
|
7122
|
+
listGitHubPullRequestLinkRecords(ctx, {
|
|
7123
|
+
...target?.kind === "issue" && target.issueId ? { paperclipIssueId: target.issueId } : {}
|
|
7124
|
+
})
|
|
7125
|
+
]);
|
|
7126
|
+
for (const record of issueLinks) {
|
|
7127
|
+
if (!doesGitHubIssueLinkRecordMatchTarget(record, target) || isGitHubIssueLinkCoveredByMappings(record, syncableMappings)) {
|
|
7128
|
+
continue;
|
|
7129
|
+
}
|
|
7130
|
+
issueLinksByKey.set(
|
|
7131
|
+
`${record.paperclipIssueId}:${record.data.githubIssueUrl}`,
|
|
7132
|
+
record
|
|
7133
|
+
);
|
|
7134
|
+
}
|
|
7135
|
+
for (const record of pullRequestLinks) {
|
|
7136
|
+
if (!doesGitHubPullRequestLinkRecordMatchTarget(record, target) || isGitHubPullRequestLinkCoveredByMappings(record, syncableMappings)) {
|
|
7137
|
+
continue;
|
|
7138
|
+
}
|
|
7139
|
+
pullRequestLinksByKey.set(
|
|
7140
|
+
`${record.paperclipIssueId}:${buildGitHubPullRequestReferenceKey({
|
|
7141
|
+
number: record.data.githubPullRequestNumber,
|
|
7142
|
+
repositoryUrl: record.data.repositoryUrl
|
|
7143
|
+
})}`,
|
|
7144
|
+
record
|
|
7145
|
+
);
|
|
7146
|
+
}
|
|
7147
|
+
return {
|
|
7148
|
+
issueLinks: [...issueLinksByKey.values()],
|
|
7149
|
+
pullRequestLinks: [...pullRequestLinksByKey.values()]
|
|
7150
|
+
};
|
|
7151
|
+
}
|
|
7152
|
+
function buildExternalLinkAuthMappings(work) {
|
|
7153
|
+
const mappingsByKey = /* @__PURE__ */ new Map();
|
|
7154
|
+
const addMapping = (params) => {
|
|
7155
|
+
const companyId = normalizeCompanyId(params.companyId);
|
|
7156
|
+
if (!companyId) {
|
|
7157
|
+
return;
|
|
7158
|
+
}
|
|
7159
|
+
const repositoryUrl = getNormalizedMappingRepositoryUrl({
|
|
7160
|
+
repositoryUrl: params.repositoryUrl
|
|
7161
|
+
});
|
|
7162
|
+
const key = `${companyId}:${params.projectId ?? ""}:${repositoryUrl}`;
|
|
7163
|
+
mappingsByKey.set(key, {
|
|
7164
|
+
id: `external-link:${key}`,
|
|
7165
|
+
repositoryUrl,
|
|
7166
|
+
paperclipProjectName: "",
|
|
7167
|
+
...params.projectId ? { paperclipProjectId: params.projectId } : {},
|
|
7168
|
+
companyId
|
|
7169
|
+
});
|
|
7170
|
+
};
|
|
7171
|
+
for (const link of work.issueLinks) {
|
|
7172
|
+
addMapping({
|
|
7173
|
+
companyId: link.data.companyId,
|
|
7174
|
+
projectId: link.data.paperclipProjectId,
|
|
7175
|
+
repositoryUrl: link.data.repositoryUrl
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
for (const link of work.pullRequestLinks) {
|
|
7179
|
+
addMapping({
|
|
7180
|
+
companyId: link.data.companyId,
|
|
7181
|
+
projectId: link.data.paperclipProjectId,
|
|
7182
|
+
repositoryUrl: link.data.repositoryUrl
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
return [...mappingsByKey.values()];
|
|
7186
|
+
}
|
|
7187
|
+
function countExternalLinkRepositories(work) {
|
|
7188
|
+
const repositories = /* @__PURE__ */ new Set();
|
|
7189
|
+
for (const link of work.issueLinks) {
|
|
7190
|
+
if (link.data.companyId) {
|
|
7191
|
+
repositories.add(`${link.data.companyId}:${link.data.repositoryUrl}`);
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
for (const link of work.pullRequestLinks) {
|
|
7195
|
+
if (link.data.companyId) {
|
|
7196
|
+
repositories.add(`${link.data.companyId}:${link.data.repositoryUrl}`);
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
return repositories.size;
|
|
7200
|
+
}
|
|
6970
7201
|
async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
|
|
6971
7202
|
const issueId = params.issueId.trim();
|
|
6972
7203
|
const commentId = params.commentId.trim();
|
|
@@ -7052,7 +7283,7 @@ async function upsertGitHubPullRequestLinkRecord(ctx, params) {
|
|
|
7052
7283
|
status: params.pullRequestState,
|
|
7053
7284
|
data: {
|
|
7054
7285
|
companyId: params.companyId,
|
|
7055
|
-
paperclipProjectId: params.projectId,
|
|
7286
|
+
...params.projectId ? { paperclipProjectId: params.projectId } : {},
|
|
7056
7287
|
repositoryUrl: getNormalizedMappingRepositoryUrl({
|
|
7057
7288
|
repositoryUrl: params.repositoryUrl
|
|
7058
7289
|
}),
|
|
@@ -7091,6 +7322,56 @@ async function assertPaperclipIssueHasNoManualGitHubLink(ctx, params) {
|
|
|
7091
7322
|
throw new Error("This Paperclip issue is already linked to a GitHub pull request.");
|
|
7092
7323
|
}
|
|
7093
7324
|
}
|
|
7325
|
+
async function resolveIssueGitHubLinkScope(ctx, params) {
|
|
7326
|
+
if (!params.allowUnmapped) {
|
|
7327
|
+
const mappedScope = await resolveIssueGitHubLinkMapping(ctx, params);
|
|
7328
|
+
return {
|
|
7329
|
+
issue: mappedScope.issue,
|
|
7330
|
+
projectId: mappedScope.projectId,
|
|
7331
|
+
mapping: mappedScope.mapping,
|
|
7332
|
+
repository: mappedScope.repository
|
|
7333
|
+
};
|
|
7334
|
+
}
|
|
7335
|
+
const issue = await ctx.issues.get(params.issueId, params.companyId);
|
|
7336
|
+
if (!issue) {
|
|
7337
|
+
throw new Error("Paperclip issue was not found.");
|
|
7338
|
+
}
|
|
7339
|
+
const projectId = typeof issue.projectId === "string" && issue.projectId.trim() ? issue.projectId.trim() : void 0;
|
|
7340
|
+
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
7341
|
+
const requestedRepository = params.repositoryUrl ? parseRepositoryReference(params.repositoryUrl) : null;
|
|
7342
|
+
if (params.repositoryUrl && !requestedRepository) {
|
|
7343
|
+
throw new Error(`Invalid GitHub repository: ${params.repositoryUrl}. Use owner/repo or https://github.com/owner/repo.`);
|
|
7344
|
+
}
|
|
7345
|
+
const candidateMappings = projectId ? await resolveProjectScopedMappings(ctx, settings.mappings, {
|
|
7346
|
+
companyId: params.companyId,
|
|
7347
|
+
projectId
|
|
7348
|
+
}) : [];
|
|
7349
|
+
const matchingMappings = requestedRepository ? candidateMappings.filter(
|
|
7350
|
+
(mapping) => areRepositoriesEqual(requireRepositoryReference(mapping.repositoryUrl), requestedRepository)
|
|
7351
|
+
) : candidateMappings;
|
|
7352
|
+
if (matchingMappings.length === 1) {
|
|
7353
|
+
const mapping = matchingMappings[0];
|
|
7354
|
+
return {
|
|
7355
|
+
issue,
|
|
7356
|
+
projectId,
|
|
7357
|
+
mapping,
|
|
7358
|
+
repository: requestedRepository ?? requireRepositoryReference(mapping.repositoryUrl)
|
|
7359
|
+
};
|
|
7360
|
+
}
|
|
7361
|
+
if (matchingMappings.length > 1 && !requestedRepository) {
|
|
7362
|
+
throw new Error("This Paperclip issue project has multiple GitHub repositories. Enter the full GitHub URL.");
|
|
7363
|
+
}
|
|
7364
|
+
if (!requestedRepository) {
|
|
7365
|
+
throw new Error(
|
|
7366
|
+
"Enter a full GitHub URL or repository because this Paperclip issue project is not mapped to that repository."
|
|
7367
|
+
);
|
|
7368
|
+
}
|
|
7369
|
+
return {
|
|
7370
|
+
issue,
|
|
7371
|
+
...projectId ? { projectId } : {},
|
|
7372
|
+
repository: requestedRepository
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7094
7375
|
async function resolveIssueGitHubLinkMapping(ctx, params) {
|
|
7095
7376
|
const issue = await ctx.issues.get(params.issueId, params.companyId);
|
|
7096
7377
|
if (!issue) {
|
|
@@ -7213,10 +7494,11 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7213
7494
|
repositoryUrl: params.repositoryUrl,
|
|
7214
7495
|
issueNumber: params.issueNumber
|
|
7215
7496
|
});
|
|
7216
|
-
const scope = await
|
|
7497
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7217
7498
|
companyId,
|
|
7218
7499
|
issueId,
|
|
7219
|
-
repositoryUrl: reference.repositoryUrl
|
|
7500
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7501
|
+
allowUnmapped: params.allowUnmapped
|
|
7220
7502
|
});
|
|
7221
7503
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7222
7504
|
const response = await octokit.rest.issues.get({
|
|
@@ -7233,18 +7515,29 @@ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
|
|
|
7233
7515
|
}
|
|
7234
7516
|
const githubIssue = normalizeGitHubIssueRecord(rawIssue);
|
|
7235
7517
|
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({
|
|
7518
|
+
const linkTarget = {
|
|
7244
7519
|
companyId,
|
|
7245
|
-
projectId: scope.mapping
|
|
7246
|
-
|
|
7247
|
-
}
|
|
7520
|
+
...scope.mapping?.paperclipProjectId ?? scope.projectId ? { paperclipProjectId: scope.mapping?.paperclipProjectId ?? scope.projectId } : {},
|
|
7521
|
+
repositoryUrl: scope.repository.url
|
|
7522
|
+
};
|
|
7523
|
+
await upsertGitHubIssueLinkRecord(ctx, linkTarget, issueId, githubIssue, linkedPullRequests);
|
|
7524
|
+
await rememberExternalGitHubLinkCompany(ctx, companyId);
|
|
7525
|
+
if (scope.mapping) {
|
|
7526
|
+
const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
|
|
7527
|
+
upsertImportedIssueRecord(
|
|
7528
|
+
importRegistry,
|
|
7529
|
+
buildImportedIssueRecord(scope.mapping, githubIssue, issueId, (/* @__PURE__ */ new Date()).toISOString())
|
|
7530
|
+
);
|
|
7531
|
+
await ctx.state.set(IMPORT_REGISTRY_SCOPE, importRegistry);
|
|
7532
|
+
}
|
|
7533
|
+
const projectIdForCacheInvalidation = scope.mapping?.paperclipProjectId ?? scope.projectId;
|
|
7534
|
+
if (projectIdForCacheInvalidation) {
|
|
7535
|
+
invalidateProjectPullRequestCaches({
|
|
7536
|
+
companyId,
|
|
7537
|
+
projectId: projectIdForCacheInvalidation,
|
|
7538
|
+
repository: scope.repository
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7248
7541
|
return {
|
|
7249
7542
|
kind: "issue",
|
|
7250
7543
|
paperclipIssueId: issueId,
|
|
@@ -7272,10 +7565,11 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7272
7565
|
pullRequestNumber: params.pullRequestNumber,
|
|
7273
7566
|
pullRequestUrl: params.pullRequestUrl
|
|
7274
7567
|
});
|
|
7275
|
-
const scope = await
|
|
7568
|
+
const scope = await resolveIssueGitHubLinkScope(ctx, {
|
|
7276
7569
|
companyId,
|
|
7277
7570
|
issueId,
|
|
7278
|
-
repositoryUrl: reference.repositoryUrl
|
|
7571
|
+
repositoryUrl: reference.repositoryUrl,
|
|
7572
|
+
allowUnmapped: params.allowUnmapped
|
|
7279
7573
|
});
|
|
7280
7574
|
const octokit = await createGitHubToolOctokit(ctx, companyId);
|
|
7281
7575
|
const response = await octokit.rest.pulls.get({
|
|
@@ -7293,7 +7587,7 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7293
7587
|
});
|
|
7294
7588
|
await upsertGitHubPullRequestLinkRecord(ctx, {
|
|
7295
7589
|
companyId,
|
|
7296
|
-
projectId: scope.mapping
|
|
7590
|
+
projectId: scope.mapping?.paperclipProjectId ?? scope.projectId,
|
|
7297
7591
|
issueId,
|
|
7298
7592
|
repositoryUrl: scope.repository.url,
|
|
7299
7593
|
pullRequestNumber: reference.pullRequestNumber,
|
|
@@ -7301,11 +7595,15 @@ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
|
|
|
7301
7595
|
pullRequestTitle: response.data.title || `Pull request #${reference.pullRequestNumber}`,
|
|
7302
7596
|
pullRequestState
|
|
7303
7597
|
});
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7598
|
+
await rememberExternalGitHubLinkCompany(ctx, companyId);
|
|
7599
|
+
const projectIdForCacheInvalidation = scope.mapping?.paperclipProjectId ?? scope.projectId;
|
|
7600
|
+
if (projectIdForCacheInvalidation) {
|
|
7601
|
+
invalidateProjectPullRequestCaches({
|
|
7602
|
+
companyId,
|
|
7603
|
+
projectId: projectIdForCacheInvalidation,
|
|
7604
|
+
repository: scope.repository
|
|
7605
|
+
});
|
|
7606
|
+
}
|
|
7309
7607
|
return {
|
|
7310
7608
|
kind: "pull_request",
|
|
7311
7609
|
paperclipIssueId: issueId,
|
|
@@ -7433,6 +7731,7 @@ async function unlinkPaperclipIssueFromGitHub(ctx, params) {
|
|
|
7433
7731
|
if (Object.keys(issuePatch).length > 0) {
|
|
7434
7732
|
await ctx.issues.update(issueId, issuePatch, companyId);
|
|
7435
7733
|
}
|
|
7734
|
+
await forgetExternalGitHubLinkCompanyIfEmpty(ctx, companyId);
|
|
7436
7735
|
return {
|
|
7437
7736
|
paperclipIssueId: issueId,
|
|
7438
7737
|
unlinked: issueLinkRecords.length > 0 || pullRequestLinkRecords.length > 0 || removedImportRegistryEntries.length > 0 || shouldClearGitHubOrigin || nextDescription !== void 0,
|
|
@@ -7545,7 +7844,14 @@ function applyPaperclipApiAuthentication(init, companyId) {
|
|
|
7545
7844
|
};
|
|
7546
7845
|
}
|
|
7547
7846
|
async function fetchPaperclipApi(url, init, options) {
|
|
7548
|
-
|
|
7847
|
+
const authenticatedInit = applyPaperclipApiAuthentication(init, options?.companyId);
|
|
7848
|
+
try {
|
|
7849
|
+
return await fetch(url, authenticatedInit);
|
|
7850
|
+
} catch (error) {
|
|
7851
|
+
throw new Error(formatPaperclipApiFetchErrorMessage(error, url, authenticatedInit), {
|
|
7852
|
+
cause: error
|
|
7853
|
+
});
|
|
7854
|
+
}
|
|
7549
7855
|
}
|
|
7550
7856
|
async function detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl) {
|
|
7551
7857
|
if (!paperclipApiBaseUrl) {
|
|
@@ -9329,7 +9635,7 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
|
|
|
9329
9635
|
};
|
|
9330
9636
|
}
|
|
9331
9637
|
async function synchronizePaperclipPullRequestIssueStatuses(ctx, octokit, mapping, advancedSettings, pullRequestLinks, paperclipApiBaseUrl, pullRequestStatusCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
|
|
9332
|
-
if (!mapping.companyId || !
|
|
9638
|
+
if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
|
|
9333
9639
|
return {
|
|
9334
9640
|
updatedStatusesCount: 0
|
|
9335
9641
|
};
|
|
@@ -9778,14 +10084,6 @@ function parseCompanyMetricApiRouteBody(input) {
|
|
|
9778
10084
|
return input.body;
|
|
9779
10085
|
}
|
|
9780
10086
|
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
10087
|
if (input.actor.actorType !== "agent") {
|
|
9790
10088
|
throw new Error("Company KPI metric events must be recorded by an authenticated Paperclip agent.");
|
|
9791
10089
|
}
|
|
@@ -9885,6 +10183,83 @@ async function handleCompanyMetricApiRoute(ctx, input) {
|
|
|
9885
10183
|
}
|
|
9886
10184
|
};
|
|
9887
10185
|
}
|
|
10186
|
+
function parseIssueLinkApiRouteBody(input) {
|
|
10187
|
+
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) {
|
|
10188
|
+
throw new Error("Issue link route body must be a JSON object.");
|
|
10189
|
+
}
|
|
10190
|
+
return input.body;
|
|
10191
|
+
}
|
|
10192
|
+
function normalizeIssueLinkApiRouteKind(payload) {
|
|
10193
|
+
const explicitKind = normalizeIssueGitHubLinkKind(payload.kind);
|
|
10194
|
+
if (explicitKind) {
|
|
10195
|
+
return explicitKind;
|
|
10196
|
+
}
|
|
10197
|
+
const reference = normalizeOptionalString2(payload.reference);
|
|
10198
|
+
if (normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(payload.pullRequestUrl)) || normalizeToolPositiveInteger(payload.pullRequestNumber) || reference && parseGitHubPullRequestHtmlUrl(reference)) {
|
|
10199
|
+
return "pull_request";
|
|
10200
|
+
}
|
|
10201
|
+
if (normalizeToolPositiveInteger(payload.issueNumber) || reference && parseGitHubIssueHtmlUrl(reference)) {
|
|
10202
|
+
return "issue";
|
|
10203
|
+
}
|
|
10204
|
+
return null;
|
|
10205
|
+
}
|
|
10206
|
+
async function handleIssueLinkApiRoute(ctx, input) {
|
|
10207
|
+
if (input.actor.actorType !== "agent") {
|
|
10208
|
+
throw new Error("GitHub issue links must be recorded by an authenticated Paperclip agent.");
|
|
10209
|
+
}
|
|
10210
|
+
const companyId = normalizeCompanyId(input.companyId);
|
|
10211
|
+
if (!companyId) {
|
|
10212
|
+
throw new Error("GitHub issue links require the host to provide the authenticated agent company.");
|
|
10213
|
+
}
|
|
10214
|
+
const payload = parseIssueLinkApiRouteBody(input);
|
|
10215
|
+
const requestedCompanyId = normalizeCompanyId(payload.companyId);
|
|
10216
|
+
if (requestedCompanyId && requestedCompanyId !== companyId) {
|
|
10217
|
+
throw new Error("companyId must match the authenticated Paperclip agent company.");
|
|
10218
|
+
}
|
|
10219
|
+
const kind = normalizeIssueLinkApiRouteKind(payload);
|
|
10220
|
+
if (!kind) {
|
|
10221
|
+
throw new Error('kind must be "issue" or "pull_request", or the payload must include a full GitHub URL.');
|
|
10222
|
+
}
|
|
10223
|
+
const paperclipIssueId = normalizeOptionalString2(payload.paperclipIssueId) ?? normalizeOptionalString2(payload.issueId);
|
|
10224
|
+
if (!paperclipIssueId) {
|
|
10225
|
+
throw new Error("paperclipIssueId is required.");
|
|
10226
|
+
}
|
|
10227
|
+
const linkResult = kind === "issue" ? await linkPaperclipIssueToGitHubIssue(ctx, {
|
|
10228
|
+
companyId,
|
|
10229
|
+
issueId: paperclipIssueId,
|
|
10230
|
+
reference: payload.reference,
|
|
10231
|
+
repositoryUrl: normalizeOptionalString2(payload.repository),
|
|
10232
|
+
issueNumber: payload.issueNumber,
|
|
10233
|
+
allowUnmapped: true
|
|
10234
|
+
}) : await linkPaperclipIssueToGitHubPullRequest(ctx, {
|
|
10235
|
+
companyId,
|
|
10236
|
+
issueId: paperclipIssueId,
|
|
10237
|
+
reference: payload.reference,
|
|
10238
|
+
repositoryUrl: normalizeOptionalString2(payload.repository),
|
|
10239
|
+
pullRequestNumber: payload.pullRequestNumber,
|
|
10240
|
+
pullRequestUrl: payload.pullRequestUrl,
|
|
10241
|
+
allowUnmapped: true
|
|
10242
|
+
});
|
|
10243
|
+
ctx.logger.info("GitHub Sync recorded a GitHub issue link API route event.", {
|
|
10244
|
+
routeKey: input.routeKey,
|
|
10245
|
+
companyId,
|
|
10246
|
+
kind,
|
|
10247
|
+
paperclipIssueId,
|
|
10248
|
+
repositoryUrl: normalizeOptionalString2(linkResult.repositoryUrl),
|
|
10249
|
+
githubIssueNumber: normalizeToolPositiveInteger(linkResult.githubIssueNumber),
|
|
10250
|
+
githubPullRequestNumber: normalizeToolPositiveInteger(linkResult.githubPullRequestNumber),
|
|
10251
|
+
agentId: input.actor.agentId ?? null,
|
|
10252
|
+
runId: input.actor.runId ?? null
|
|
10253
|
+
});
|
|
10254
|
+
return {
|
|
10255
|
+
status: 201,
|
|
10256
|
+
body: {
|
|
10257
|
+
status: "linked",
|
|
10258
|
+
companyId,
|
|
10259
|
+
...linkResult
|
|
10260
|
+
}
|
|
10261
|
+
};
|
|
10262
|
+
}
|
|
9888
10263
|
async function createGitHubToolOctokit(ctx, companyId, context = {}) {
|
|
9889
10264
|
const token = (await resolveGithubToken(ctx, { companyId })).trim();
|
|
9890
10265
|
if (!token) {
|
|
@@ -12836,10 +13211,14 @@ function shouldRunScheduledSync(settings, scheduledAt) {
|
|
|
12836
13211
|
}
|
|
12837
13212
|
return now - lastCheckedAt >= settings.scheduleFrequencyMinutes * 6e4;
|
|
12838
13213
|
}
|
|
12839
|
-
function listScheduledSyncTargets(settings) {
|
|
13214
|
+
async function listScheduledSyncTargets(ctx, settings) {
|
|
13215
|
+
const externalLinkCompanyIds = await getExternalGitHubLinkCompanyIds(ctx);
|
|
12840
13216
|
const companyIds = [
|
|
12841
|
-
|
|
12842
|
-
|
|
13217
|
+
.../* @__PURE__ */ new Set(
|
|
13218
|
+
[
|
|
13219
|
+
...settings.mappings.map((mapping) => normalizeCompanyId(mapping.companyId)).filter((companyId) => Boolean(companyId)),
|
|
13220
|
+
...externalLinkCompanyIds
|
|
13221
|
+
]
|
|
12843
13222
|
)
|
|
12844
13223
|
];
|
|
12845
13224
|
if (companyIds.length === 0) {
|
|
@@ -12858,6 +13237,9 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12858
13237
|
const token = typeof options.resolvedToken === "string" ? options.resolvedToken : await resolveGithubToken(ctx, { companyId: targetCompanyId });
|
|
12859
13238
|
const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(baseSettings, config, targetCompanyId);
|
|
12860
13239
|
const mappings = getSyncableMappingsForTarget(settings.mappings, options.target);
|
|
13240
|
+
const externalSyncWork = options.externalSyncWork ?? await listExternalGitHubLinkSyncWork(ctx, settings.mappings, options.target);
|
|
13241
|
+
const externalLinkAuthMappings = buildExternalLinkAuthMappings(externalSyncWork);
|
|
13242
|
+
const externalLinkCount = externalSyncWork.issueLinks.length + externalSyncWork.pullRequestLinks.length;
|
|
12861
13243
|
activePaperclipApiAuthTokensByCompanyId = null;
|
|
12862
13244
|
const failureContext = {
|
|
12863
13245
|
phase: "configuration"
|
|
@@ -12869,14 +13251,18 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12869
13251
|
};
|
|
12870
13252
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12871
13253
|
}
|
|
12872
|
-
if (mappings.length === 0) {
|
|
13254
|
+
if (mappings.length === 0 && externalLinkCount === 0) {
|
|
12873
13255
|
const next = {
|
|
12874
13256
|
...settings,
|
|
12875
13257
|
syncState: createSetupConfigurationErrorSyncState("missing_mapping", trigger)
|
|
12876
13258
|
};
|
|
12877
13259
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12878
13260
|
}
|
|
12879
|
-
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13261
|
+
const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(
|
|
13262
|
+
settings,
|
|
13263
|
+
config,
|
|
13264
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13265
|
+
);
|
|
12880
13266
|
if (mappingsMissingBoardAccess.length > 0 && await detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl)) {
|
|
12881
13267
|
const next = {
|
|
12882
13268
|
...settings,
|
|
@@ -12884,7 +13270,7 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12884
13270
|
};
|
|
12885
13271
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12886
13272
|
}
|
|
12887
|
-
if (!ctx.issues || typeof ctx.issues.create !== "function") {
|
|
13273
|
+
if (mappings.length > 0 && (!ctx.issues || typeof ctx.issues.create !== "function")) {
|
|
12888
13274
|
const errorDetails = {
|
|
12889
13275
|
phase: "configuration",
|
|
12890
13276
|
suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
|
|
@@ -12910,7 +13296,12 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
12910
13296
|
};
|
|
12911
13297
|
return saveSettingsSyncState(ctx, settings, next.syncState, targetCompanyId);
|
|
12912
13298
|
}
|
|
12913
|
-
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13299
|
+
activePaperclipApiAuthTokensByCompanyId = await resolvePaperclipApiAuthTokens(
|
|
13300
|
+
ctx,
|
|
13301
|
+
settings,
|
|
13302
|
+
config,
|
|
13303
|
+
[...mappings, ...externalLinkAuthMappings]
|
|
13304
|
+
);
|
|
12914
13305
|
const octokitLogContext = {
|
|
12915
13306
|
companyId: targetCompanyId,
|
|
12916
13307
|
operation: "sync.github-issues",
|
|
@@ -13162,6 +13553,8 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13162
13553
|
continue;
|
|
13163
13554
|
}
|
|
13164
13555
|
}
|
|
13556
|
+
totalTrackedIssueCount += externalLinkCount;
|
|
13557
|
+
syncedIssuesCount = totalTrackedIssueCount;
|
|
13165
13558
|
recordCompanyBacklogSnapshotsFromPlans(repositoryPlans);
|
|
13166
13559
|
if (repositoryPlans.length > 0) {
|
|
13167
13560
|
const firstPlan = repositoryPlans[0];
|
|
@@ -13174,6 +13567,14 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13174
13567
|
totalIssueCount: totalTrackedIssueCount,
|
|
13175
13568
|
detailLabel: "Loading linked pull requests, review threads, and CI status before syncing."
|
|
13176
13569
|
};
|
|
13570
|
+
} else if (externalLinkCount > 0) {
|
|
13571
|
+
currentProgress = {
|
|
13572
|
+
phase: "preparing",
|
|
13573
|
+
totalRepositoryCount: countExternalLinkRepositories(externalSyncWork),
|
|
13574
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13575
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13576
|
+
detailLabel: "Loading linked GitHub issues and pull requests before syncing."
|
|
13577
|
+
};
|
|
13177
13578
|
} else {
|
|
13178
13579
|
currentProgress = {
|
|
13179
13580
|
phase: "preparing",
|
|
@@ -13438,6 +13839,194 @@ async function performSync(ctx, trigger, options = {}) {
|
|
|
13438
13839
|
continue;
|
|
13439
13840
|
}
|
|
13440
13841
|
}
|
|
13842
|
+
for (const [externalIssueIndex, issueLink] of externalSyncWork.issueLinks.entries()) {
|
|
13843
|
+
await throwIfSyncCancelled();
|
|
13844
|
+
try {
|
|
13845
|
+
const companyId = normalizeCompanyId(issueLink.data.companyId);
|
|
13846
|
+
if (!companyId) {
|
|
13847
|
+
continue;
|
|
13848
|
+
}
|
|
13849
|
+
const repository = requireRepositoryReference(issueLink.data.repositoryUrl);
|
|
13850
|
+
octokitLogContext.repositoryUrl = repository.url;
|
|
13851
|
+
const advancedSettings = getCompanyAdvancedSettings(settings, companyId);
|
|
13852
|
+
let availableLabels = companyLabelDirectoryCache.get(companyId);
|
|
13853
|
+
if (!availableLabels) {
|
|
13854
|
+
updateSyncFailureContext(failureContext, {
|
|
13855
|
+
phase: "loading_paperclip_labels",
|
|
13856
|
+
repositoryUrl: repository.url,
|
|
13857
|
+
githubIssueNumber: issueLink.data.githubIssueNumber
|
|
13858
|
+
});
|
|
13859
|
+
availableLabels = supportsPaperclipLabelMapping ? await buildPaperclipLabelDirectory(ctx, companyId, paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
|
|
13860
|
+
companyLabelDirectoryCache.set(companyId, availableLabels);
|
|
13861
|
+
}
|
|
13862
|
+
currentProgress = {
|
|
13863
|
+
phase: "syncing",
|
|
13864
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13865
|
+
currentRepositoryUrl: repository.url,
|
|
13866
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13867
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13868
|
+
currentIssueNumber: issueLink.data.githubIssueNumber,
|
|
13869
|
+
detailLabel: `Syncing linked GitHub issue #${issueLink.data.githubIssueNumber} in ${repository.owner}/${repository.repo}.`
|
|
13870
|
+
};
|
|
13871
|
+
await persistRunningProgress(true);
|
|
13872
|
+
updateSyncFailureContext(failureContext, {
|
|
13873
|
+
phase: "evaluating_github_status",
|
|
13874
|
+
repositoryUrl: repository.url,
|
|
13875
|
+
githubIssueNumber: issueLink.data.githubIssueNumber
|
|
13876
|
+
});
|
|
13877
|
+
const response = await octokit.rest.issues.get({
|
|
13878
|
+
owner: repository.owner,
|
|
13879
|
+
repo: repository.repo,
|
|
13880
|
+
issue_number: issueLink.data.githubIssueNumber,
|
|
13881
|
+
headers: {
|
|
13882
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
13883
|
+
}
|
|
13884
|
+
});
|
|
13885
|
+
const githubIssue = normalizeGitHubIssueRecord(response.data);
|
|
13886
|
+
if (response.data.pull_request) {
|
|
13887
|
+
continue;
|
|
13888
|
+
}
|
|
13889
|
+
const virtualMapping = {
|
|
13890
|
+
id: `external-link:${companyId}:${repository.url}`,
|
|
13891
|
+
repositoryUrl: repository.url,
|
|
13892
|
+
paperclipProjectName: "",
|
|
13893
|
+
...issueLink.data.paperclipProjectId ? { paperclipProjectId: issueLink.data.paperclipProjectId } : {},
|
|
13894
|
+
companyId
|
|
13895
|
+
};
|
|
13896
|
+
const importedIssue = {
|
|
13897
|
+
mappingId: virtualMapping.id,
|
|
13898
|
+
githubIssueId: githubIssue.id,
|
|
13899
|
+
githubIssueNumber: githubIssue.number,
|
|
13900
|
+
paperclipIssueId: issueLink.paperclipIssueId,
|
|
13901
|
+
importedAt: issueLink.createdAt ?? issueLink.data.syncedAt,
|
|
13902
|
+
lastSeenCommentCount: issueLink.data.commentsCount,
|
|
13903
|
+
lastSeenGitHubState: issueLink.data.githubIssueState,
|
|
13904
|
+
linkedPullRequestCommentCounts: [],
|
|
13905
|
+
repositoryUrl: repository.url,
|
|
13906
|
+
...issueLink.data.paperclipProjectId ? { paperclipProjectId: issueLink.data.paperclipProjectId } : {},
|
|
13907
|
+
companyId
|
|
13908
|
+
};
|
|
13909
|
+
const synchronizationResult = await synchronizePaperclipIssueStatuses(
|
|
13910
|
+
ctx,
|
|
13911
|
+
octokit,
|
|
13912
|
+
repository,
|
|
13913
|
+
virtualMapping,
|
|
13914
|
+
advancedSettings,
|
|
13915
|
+
/* @__PURE__ */ new Map([[githubIssue.id, githubIssue]]),
|
|
13916
|
+
[importedIssue],
|
|
13917
|
+
/* @__PURE__ */ new Set(),
|
|
13918
|
+
availableLabels,
|
|
13919
|
+
paperclipApiBaseUrl,
|
|
13920
|
+
/* @__PURE__ */ new Map(),
|
|
13921
|
+
/* @__PURE__ */ new Map(),
|
|
13922
|
+
/* @__PURE__ */ new Map(),
|
|
13923
|
+
repositoryMaintainerCache,
|
|
13924
|
+
failureContext,
|
|
13925
|
+
recoverableFailures,
|
|
13926
|
+
throwIfSyncCancelled,
|
|
13927
|
+
async (params) => {
|
|
13928
|
+
const recorded = recordCompanyActivityMetricEvent(companyKpiState, {
|
|
13929
|
+
companyId: params.companyId,
|
|
13930
|
+
metric: "githubIssuesClosedCount",
|
|
13931
|
+
eventKey: `${params.repositoryUrl}/issues/${params.githubIssueNumber}:${coerceDate(params.occurredAt).toISOString()}`,
|
|
13932
|
+
repositoryUrl: params.repositoryUrl,
|
|
13933
|
+
occurredAt: params.occurredAt
|
|
13934
|
+
});
|
|
13935
|
+
companyKpiState = recorded.state;
|
|
13936
|
+
companyKpiStateDirty = companyKpiStateDirty || recorded.recorded;
|
|
13937
|
+
},
|
|
13938
|
+
async (progress) => {
|
|
13939
|
+
completedTrackedIssueCount += 1;
|
|
13940
|
+
currentProgress = {
|
|
13941
|
+
phase: "syncing",
|
|
13942
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13943
|
+
currentRepositoryUrl: repository.url,
|
|
13944
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13945
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13946
|
+
...progress.currentIssueNumber !== void 0 ? { currentIssueNumber: progress.currentIssueNumber } : {}
|
|
13947
|
+
};
|
|
13948
|
+
await persistRunningProgress(externalIssueIndex === externalSyncWork.issueLinks.length - 1);
|
|
13949
|
+
}
|
|
13950
|
+
);
|
|
13951
|
+
updatedStatusesCount += synchronizationResult.updatedStatusesCount;
|
|
13952
|
+
updatedLabelsCount += synchronizationResult.updatedLabelsCount;
|
|
13953
|
+
updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
|
|
13954
|
+
} catch (error) {
|
|
13955
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
13956
|
+
throw error;
|
|
13957
|
+
}
|
|
13958
|
+
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
13959
|
+
continue;
|
|
13960
|
+
}
|
|
13961
|
+
}
|
|
13962
|
+
const externalPullRequestLinksByCompanyId = /* @__PURE__ */ new Map();
|
|
13963
|
+
for (const pullRequestLink of externalSyncWork.pullRequestLinks) {
|
|
13964
|
+
const companyId = normalizeCompanyId(pullRequestLink.data.companyId);
|
|
13965
|
+
if (!companyId) {
|
|
13966
|
+
continue;
|
|
13967
|
+
}
|
|
13968
|
+
const companyLinks = externalPullRequestLinksByCompanyId.get(companyId) ?? [];
|
|
13969
|
+
companyLinks.push(pullRequestLink);
|
|
13970
|
+
externalPullRequestLinksByCompanyId.set(companyId, companyLinks);
|
|
13971
|
+
}
|
|
13972
|
+
for (const [companyId, pullRequestLinks] of externalPullRequestLinksByCompanyId.entries()) {
|
|
13973
|
+
await throwIfSyncCancelled();
|
|
13974
|
+
try {
|
|
13975
|
+
const firstRepositoryUrl = pullRequestLinks[0]?.data.repositoryUrl;
|
|
13976
|
+
if (!firstRepositoryUrl) {
|
|
13977
|
+
continue;
|
|
13978
|
+
}
|
|
13979
|
+
const repository = requireRepositoryReference(firstRepositoryUrl);
|
|
13980
|
+
octokitLogContext.repositoryUrl = repository.url;
|
|
13981
|
+
currentProgress = {
|
|
13982
|
+
phase: "syncing",
|
|
13983
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
13984
|
+
currentRepositoryUrl: repository.url,
|
|
13985
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
13986
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
13987
|
+
detailLabel: `Syncing ${pullRequestLinks.length} linked GitHub pull request${pullRequestLinks.length === 1 ? "" : "s"}.`
|
|
13988
|
+
};
|
|
13989
|
+
await persistRunningProgress(true);
|
|
13990
|
+
const pullRequestStatusCache = /* @__PURE__ */ new Map();
|
|
13991
|
+
const synchronizationResult = await synchronizePaperclipPullRequestIssueStatuses(
|
|
13992
|
+
ctx,
|
|
13993
|
+
octokit,
|
|
13994
|
+
{
|
|
13995
|
+
id: `external-link:${companyId}:${repository.url}`,
|
|
13996
|
+
repositoryUrl: repository.url,
|
|
13997
|
+
paperclipProjectName: "",
|
|
13998
|
+
companyId
|
|
13999
|
+
},
|
|
14000
|
+
getCompanyAdvancedSettings(settings, companyId),
|
|
14001
|
+
pullRequestLinks,
|
|
14002
|
+
paperclipApiBaseUrl,
|
|
14003
|
+
pullRequestStatusCache,
|
|
14004
|
+
failureContext,
|
|
14005
|
+
recoverableFailures,
|
|
14006
|
+
throwIfSyncCancelled,
|
|
14007
|
+
async (progress) => {
|
|
14008
|
+
completedTrackedIssueCount += 1;
|
|
14009
|
+
const pullRequestRepository = requireRepositoryReference(progress.pullRequestLink.data.repositoryUrl);
|
|
14010
|
+
currentProgress = {
|
|
14011
|
+
phase: "syncing",
|
|
14012
|
+
totalRepositoryCount: Math.max(mappings.length, countExternalLinkRepositories(externalSyncWork)),
|
|
14013
|
+
currentRepositoryUrl: pullRequestRepository.url,
|
|
14014
|
+
completedIssueCount: completedTrackedIssueCount,
|
|
14015
|
+
totalIssueCount: totalTrackedIssueCount,
|
|
14016
|
+
detailLabel: `Synced pull request #${progress.pullRequestLink.data.githubPullRequestNumber} in ${pullRequestRepository.owner}/${pullRequestRepository.repo}.`
|
|
14017
|
+
};
|
|
14018
|
+
await persistRunningProgress(progress.completedIssueCount === progress.totalIssueCount);
|
|
14019
|
+
}
|
|
14020
|
+
);
|
|
14021
|
+
updatedStatusesCount += synchronizationResult.updatedStatusesCount;
|
|
14022
|
+
} catch (error) {
|
|
14023
|
+
if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
|
|
14024
|
+
throw error;
|
|
14025
|
+
}
|
|
14026
|
+
recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
|
|
14027
|
+
continue;
|
|
14028
|
+
}
|
|
14029
|
+
}
|
|
13441
14030
|
if (recoverableFailures.length > 0) {
|
|
13442
14031
|
const primaryFailure = recoverableFailures[0];
|
|
13443
14032
|
const errorDetails = buildSyncErrorDetails(primaryFailure.error, primaryFailure.context);
|
|
@@ -13566,10 +14155,12 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13566
14155
|
if (getActiveGitHubRateLimitPause(currentSettings.syncState)) {
|
|
13567
14156
|
return currentSettings;
|
|
13568
14157
|
}
|
|
13569
|
-
if (trigger !== "manual" &&
|
|
14158
|
+
if (trigger !== "manual" && !token.trim()) {
|
|
13570
14159
|
return currentSettings;
|
|
13571
14160
|
}
|
|
13572
|
-
|
|
14161
|
+
const externalSyncWork = await listExternalGitHubLinkSyncWork(ctx, currentSettings.mappings, options.target);
|
|
14162
|
+
const externalLinkCount = externalSyncWork.issueLinks.length + externalSyncWork.pullRequestLinks.length;
|
|
14163
|
+
if (trigger !== "manual" && getSyncableMappingsForTarget(currentSettings.mappings, options.target).length === 0 && externalLinkCount === 0) {
|
|
13573
14164
|
return currentSettings;
|
|
13574
14165
|
}
|
|
13575
14166
|
await setSyncCancellationRequest(ctx, null);
|
|
@@ -13583,7 +14174,7 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13583
14174
|
message: getSyncTargetRunningMessage(options.target),
|
|
13584
14175
|
progress: {
|
|
13585
14176
|
phase: "preparing",
|
|
13586
|
-
totalRepositoryCount: syncableMappings.length
|
|
14177
|
+
totalRepositoryCount: syncableMappings.length + countExternalLinkRepositories(externalSyncWork)
|
|
13587
14178
|
}
|
|
13588
14179
|
});
|
|
13589
14180
|
activeRunningSyncState = {
|
|
@@ -13598,7 +14189,8 @@ async function startSync(ctx, trigger, options = {}) {
|
|
|
13598
14189
|
await runningStatePromise;
|
|
13599
14190
|
return await performSync(ctx, trigger, {
|
|
13600
14191
|
resolvedToken: token,
|
|
13601
|
-
target: options.target
|
|
14192
|
+
target: options.target,
|
|
14193
|
+
externalSyncWork
|
|
13602
14194
|
});
|
|
13603
14195
|
} catch (error) {
|
|
13604
14196
|
return await createUnexpectedSyncErrorResult(ctx, trigger, error, targetCompanyId);
|
|
@@ -14493,6 +15085,42 @@ function registerGitHubAgentTools(ctx) {
|
|
|
14493
15085
|
);
|
|
14494
15086
|
})
|
|
14495
15087
|
);
|
|
15088
|
+
ctx.tools.register(
|
|
15089
|
+
"link_github_item",
|
|
15090
|
+
getGitHubAgentToolDeclaration("link_github_item"),
|
|
15091
|
+
async (params, runCtx) => executeGitHubTool(async () => {
|
|
15092
|
+
const input = getToolInputRecord(params);
|
|
15093
|
+
const kind = normalizeIssueGitHubLinkKind(input.kind);
|
|
15094
|
+
if (!kind) {
|
|
15095
|
+
throw new Error('kind must be "issue" or "pull_request".');
|
|
15096
|
+
}
|
|
15097
|
+
const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
|
|
15098
|
+
if (!paperclipIssueId) {
|
|
15099
|
+
throw new Error("paperclipIssueId is required.");
|
|
15100
|
+
}
|
|
15101
|
+
const linkResult = kind === "issue" ? await linkPaperclipIssueToGitHubIssue(ctx, {
|
|
15102
|
+
companyId: runCtx.companyId,
|
|
15103
|
+
issueId: paperclipIssueId,
|
|
15104
|
+
reference: input.reference,
|
|
15105
|
+
repositoryUrl: normalizeOptionalToolString(input.repository),
|
|
15106
|
+
issueNumber: input.issueNumber,
|
|
15107
|
+
allowUnmapped: true
|
|
15108
|
+
}) : await linkPaperclipIssueToGitHubPullRequest(ctx, {
|
|
15109
|
+
companyId: runCtx.companyId,
|
|
15110
|
+
issueId: paperclipIssueId,
|
|
15111
|
+
reference: input.reference,
|
|
15112
|
+
repositoryUrl: normalizeOptionalToolString(input.repository),
|
|
15113
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
15114
|
+
pullRequestUrl: input.pullRequestUrl,
|
|
15115
|
+
allowUnmapped: true
|
|
15116
|
+
});
|
|
15117
|
+
const itemLabel = kind === "issue" ? `issue #${normalizeToolPositiveInteger(linkResult.githubIssueNumber) ?? ""}`.trim() : `pull request #${normalizeToolPositiveInteger(linkResult.githubPullRequestNumber) ?? ""}`.trim();
|
|
15118
|
+
return buildToolSuccessResult(
|
|
15119
|
+
`Linked Paperclip issue ${paperclipIssueId} to GitHub ${itemLabel}.`,
|
|
15120
|
+
linkResult
|
|
15121
|
+
);
|
|
15122
|
+
})
|
|
15123
|
+
);
|
|
14496
15124
|
}
|
|
14497
15125
|
function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
14498
15126
|
if (typeof entry !== "string" || !entry.trim()) {
|
|
@@ -14508,6 +15136,7 @@ function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
|
14508
15136
|
var __testing = {
|
|
14509
15137
|
buildSyncFallbackExecutionStatePatch,
|
|
14510
15138
|
createGitHubToolOctokit,
|
|
15139
|
+
formatPaperclipApiFetchErrorMessage,
|
|
14511
15140
|
hasUnresolvedPaperclipIssueBlocker,
|
|
14512
15141
|
isHealthyMaintainerWaitTransition,
|
|
14513
15142
|
resolveSyncTransitionAssignee
|
|
@@ -14543,6 +15172,7 @@ var plugin = definePlugin({
|
|
|
14543
15172
|
...getPublicSettingsForScope(settingsForResponse, requestedCompanyId),
|
|
14544
15173
|
...includeAssignees ? { availableAssignees } : {},
|
|
14545
15174
|
totalSyncedIssuesCount: countImportedIssuesForMappings(importRegistry, scopedMappings),
|
|
15175
|
+
paperclipApiBaseUrlConfigured: Boolean(normalizePaperclipApiBaseUrl(config.paperclipApiBaseUrl)),
|
|
14546
15176
|
githubTokenConfigured,
|
|
14547
15177
|
paperclipBoardAccessConfigured: requestedCompanyId ? hasConfiguredPaperclipBoardAccess(settingsForResponse, config, requestedCompanyId) : hasConfiguredPaperclipBoardAccessForMappings(settingsForResponse, config, scopedMappings),
|
|
14548
15178
|
...savedBoardTokenRef ? { paperclipBoardAccessConfigSyncRef: savedBoardTokenRef } : {},
|
|
@@ -14901,7 +15531,7 @@ var plugin = definePlugin({
|
|
|
14901
15531
|
ctx.jobs.register("sync.github-issues", async (job) => {
|
|
14902
15532
|
const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
|
|
14903
15533
|
const trigger = job.trigger === "retry" ? "retry" : "schedule";
|
|
14904
|
-
const scheduledTargets = listScheduledSyncTargets(settings);
|
|
15534
|
+
const scheduledTargets = await listScheduledSyncTargets(ctx, settings);
|
|
14905
15535
|
if (scheduledTargets.length === 0) {
|
|
14906
15536
|
const reconciledSettings = await reconcileOrphanedRunningSyncState(ctx);
|
|
14907
15537
|
if (job.trigger === "schedule" && !shouldRunScheduledSync(reconciledSettings, job.scheduledAt)) {
|
|
@@ -14923,7 +15553,18 @@ var plugin = definePlugin({
|
|
|
14923
15553
|
if (!pluginRuntimeContext) {
|
|
14924
15554
|
throw new Error("GitHub Sync worker is not ready to handle API routes yet.");
|
|
14925
15555
|
}
|
|
14926
|
-
|
|
15556
|
+
if (input.routeKey === COMPANY_METRIC_API_ROUTE_KEY) {
|
|
15557
|
+
return handleCompanyMetricApiRoute(pluginRuntimeContext, input);
|
|
15558
|
+
}
|
|
15559
|
+
if (input.routeKey === ISSUE_LINK_API_ROUTE_KEY) {
|
|
15560
|
+
return handleIssueLinkApiRoute(pluginRuntimeContext, input);
|
|
15561
|
+
}
|
|
15562
|
+
return {
|
|
15563
|
+
status: 404,
|
|
15564
|
+
body: {
|
|
15565
|
+
error: `Unsupported plugin API route: ${input.routeKey}.`
|
|
15566
|
+
}
|
|
15567
|
+
};
|
|
14927
15568
|
},
|
|
14928
15569
|
async onShutdown() {
|
|
14929
15570
|
pluginRuntimeContext = null;
|