@mastra/factory 0.0.0-fix-thread-subscription-multi-instance-redis-streams-20260730204150
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/CHANGELOG.md +695 -0
- package/LICENSE.md +30 -0
- package/README.md +27 -0
- package/dist/auth.d.ts +181 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +505 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities/connection.d.ts +9 -0
- package/dist/capabilities/connection.d.ts.map +1 -0
- package/dist/capabilities/connection.js +1 -0
- package/dist/capabilities/intake.d.ts +140 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/version-control.d.ts +245 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/factory.d.ts +156 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +483 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/integrations/base.d.ts +210 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +1 -0
- package/dist/integrations/github/config.d.ts +60 -0
- package/dist/integrations/github/config.d.ts.map +1 -0
- package/dist/integrations/github/config.js +42 -0
- package/dist/integrations/github/config.js.map +1 -0
- package/dist/integrations/github/factory-session.d.ts +12 -0
- package/dist/integrations/github/factory-session.d.ts.map +1 -0
- package/dist/integrations/github/factory-session.js +37 -0
- package/dist/integrations/github/factory-session.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +191 -0
- package/dist/integrations/github/integration.d.ts.map +1 -0
- package/dist/integrations/github/integration.js +1077 -0
- package/dist/integrations/github/integration.js.map +1 -0
- package/dist/integrations/github/issue-triage.d.ts +27 -0
- package/dist/integrations/github/issue-triage.d.ts.map +1 -0
- package/dist/integrations/github/issue-triage.js +82 -0
- package/dist/integrations/github/issue-triage.js.map +1 -0
- package/dist/integrations/github/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +59 -0
- package/dist/integrations/github/pat.js.map +1 -0
- package/dist/integrations/github/provenance.d.ts +27 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -0
- package/dist/integrations/github/provenance.js +75 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +69 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1324 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/rules.d.ts +78 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/integrations/github/rules.js +362 -0
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/github/sandbox-release.d.ts +38 -0
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -0
- package/dist/integrations/github/sandbox-release.js +74 -0
- package/dist/integrations/github/sandbox-release.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +281 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +547 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +26 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +192 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -0
- package/dist/integrations/github/subscriptions.d.ts +54 -0
- package/dist/integrations/github/subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/subscriptions.js +62 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/token-refresh.d.ts +12 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -0
- package/dist/integrations/github/token-refresh.js +24 -0
- package/dist/integrations/github/token-refresh.js.map +1 -0
- package/dist/integrations/github/webhook.d.ts +87 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -0
- package/dist/integrations/github/webhook.js +397 -0
- package/dist/integrations/github/webhook.js.map +1 -0
- package/dist/integrations/linear/agent-tools.d.ts +34 -0
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
- package/dist/integrations/linear/agent-tools.js +85 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +233 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -0
- package/dist/integrations/linear/integration.js +672 -0
- package/dist/integrations/linear/integration.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts +61 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -0
- package/dist/integrations/linear/routes.js +239 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/rules.d.ts +42 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/integrations/linear/rules.js +133 -0
- package/dist/integrations/linear/rules.js.map +1 -0
- package/dist/integrations/linear/storage.d.ts +65 -0
- package/dist/integrations/linear/storage.d.ts.map +1 -0
- package/dist/integrations/linear/storage.js +1 -0
- package/dist/integrations/platform/api-client.d.ts +26 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +156 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +49 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +348 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +50 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +961 -0
- package/dist/integrations/platform/github/integration.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +46 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
- package/dist/integrations/platform/linear/integration.js +483 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -0
- package/dist/integrations/workos/integration.d.ts +39 -0
- package/dist/integrations/workos/integration.d.ts.map +1 -0
- package/dist/integrations/workos/integration.js +91 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +235 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +972 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/custom-provider-source.d.ts +52 -0
- package/dist/routes/custom-provider-source.d.ts.map +1 -0
- package/dist/routes/custom-provider-source.js +124 -0
- package/dist/routes/custom-provider-source.js.map +1 -0
- package/dist/routes/fs.d.ts +151 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +708 -0
- package/dist/routes/fs.js.map +1 -0
- package/dist/routes/intake.d.ts +23 -0
- package/dist/routes/intake.d.ts.map +1 -0
- package/dist/routes/intake.js +179 -0
- package/dist/routes/intake.js.map +1 -0
- package/dist/routes/oauth.d.ts +43 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +463 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/projects.d.ts +19 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +461 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/provider-credentials.d.ts +81 -0
- package/dist/routes/provider-credentials.d.ts.map +1 -0
- package/dist/routes/provider-credentials.js +86 -0
- package/dist/routes/provider-credentials.js.map +1 -0
- package/dist/routes/route.d.ts +48 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +15 -0
- package/dist/routes/route.js.map +1 -0
- package/dist/routes/skills.d.ts +30 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +168 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +91 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +260 -0
- package/dist/routes/surface.js.map +1 -0
- package/dist/routes/tenant-credentials.d.ts +74 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -0
- package/dist/routes/tenant-credentials.js +156 -0
- package/dist/routes/tenant-credentials.js.map +1 -0
- package/dist/routes/work-items.d.ts +40 -0
- package/dist/routes/work-items.d.ts.map +1 -0
- package/dist/routes/work-items.js +695 -0
- package/dist/routes/work-items.js.map +1 -0
- package/dist/rules/binding-context.d.ts +5 -0
- package/dist/rules/binding-context.d.ts.map +1 -0
- package/dist/rules/binding-context.js +28 -0
- package/dist/rules/binding-context.js.map +1 -0
- package/dist/rules/defaults.d.ts +9 -0
- package/dist/rules/defaults.d.ts.map +1 -0
- package/dist/rules/defaults.js +260 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +42 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +518 -0
- package/dist/rules/dispatcher.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +378 -0
- package/dist/rules/processor.js.map +1 -0
- package/dist/rules/resolve.d.ts +17 -0
- package/dist/rules/resolve.d.ts.map +1 -0
- package/dist/rules/resolve.js +30 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +60 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +194 -0
- package/dist/rules/start-coordinator.js.map +1 -0
- package/dist/rules/tools.d.ts +10 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +58 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +49 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +232 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +234 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +38 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/validation.d.ts +11 -0
- package/dist/rules/validation.d.ts.map +1 -0
- package/dist/rules/validation.js +308 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +222 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +322 -0
- package/dist/sandbox/fleet.js.map +1 -0
- package/dist/sandbox/reattach.d.ts +3 -0
- package/dist/sandbox/reattach.d.ts.map +1 -0
- package/dist/sandbox/reattach.js +16 -0
- package/dist/sandbox/reattach.js.map +1 -0
- package/dist/server-error.d.ts +12 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +16 -0
- package/dist/server-error.js.map +1 -0
- package/dist/skills/service.d.ts +48 -0
- package/dist/skills/service.d.ts.map +1 -0
- package/dist/skills/service.js +40 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +33 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +104 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +59 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +83 -0
- package/dist/state-signing.js.map +1 -0
- package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
- package/dist/storage/domains/audit/agent-audit.js +66 -0
- package/dist/storage/domains/audit/agent-audit.js.map +1 -0
- package/dist/storage/domains/audit/base.d.ts +140 -0
- package/dist/storage/domains/audit/base.d.ts.map +1 -0
- package/dist/storage/domains/audit/base.js +184 -0
- package/dist/storage/domains/audit/base.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +69 -0
- package/dist/storage/domains/audit/domain.d.ts.map +1 -0
- package/dist/storage/domains/audit/domain.js +167 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/channel-identity/base.d.ts +96 -0
- package/dist/storage/domains/channel-identity/base.d.ts.map +1 -0
- package/dist/storage/domains/channel-identity/base.js +149 -0
- package/dist/storage/domains/channel-identity/base.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +134 -0
- package/dist/storage/domains/credentials/base.d.ts.map +1 -0
- package/dist/storage/domains/credentials/base.js +273 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/custom-providers/base.d.ts +57 -0
- package/dist/storage/domains/custom-providers/base.d.ts.map +1 -0
- package/dist/storage/domains/custom-providers/base.js +145 -0
- package/dist/storage/domains/custom-providers/base.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +38 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -0
- package/dist/storage/domains/intake/base.js +80 -0
- package/dist/storage/domains/intake/base.js.map +1 -0
- package/dist/storage/domains/integrations/base.d.ts +126 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -0
- package/dist/storage/domains/integrations/base.js +317 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/memory-settings/base.d.ts +62 -0
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -0
- package/dist/storage/domains/memory-settings/base.js +126 -0
- package/dist/storage/domains/memory-settings/base.js.map +1 -0
- package/dist/storage/domains/model-packs/base.d.ts +49 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
- package/dist/storage/domains/model-packs/base.js +96 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +58 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +111 -0
- package/dist/storage/domains/projects/base.js.map +1 -0
- package/dist/storage/domains/queue-health/base.d.ts +52 -0
- package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
- package/dist/storage/domains/queue-health/base.js +122 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +358 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +908 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +175 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +377 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +412 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -0
- package/dist/storage/domains/work-items/base.js +1309 -0
- package/dist/storage/domains/work-items/base.js.map +1 -0
- package/dist/storage/domains/work-items/metrics.d.ts +101 -0
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
- package/dist/storage/domains/work-items/metrics.js +221 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +24 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +286 -0
- package/dist/workspace.js.map +1 -0
- package/factory-skills/configure-factory-rules/SKILL.md +67 -0
- package/factory-skills/factory-plan/SKILL.md +61 -0
- package/factory-skills/factory-review/SKILL.md +155 -0
- package/factory-skills/factory-triage/SKILL.md +77 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "./session-subscriptions.js";
|
|
2
|
+
import { runGithubIssueTriage } from "./issue-triage.js";
|
|
3
|
+
import { buildGithubRoutes } from "./routes.js";
|
|
4
|
+
import { attachGithubRules } from "./rules.js";
|
|
5
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
6
|
+
import { Octokit } from "@octokit/rest";
|
|
7
|
+
//#region src/integrations/github/integration.ts
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a PEM private key supplied via env. Env tooling tends to mangle
|
|
10
|
+
* multi-line PEMs, so two single-line forms are supported:
|
|
11
|
+
* - `\n`-escaped: literal `\n` sequences become real newlines
|
|
12
|
+
* - fully flattened: newlines stripped entirely — the PEM is rebuilt by
|
|
13
|
+
* re-wrapping the base64 body (Node's decoder rejects header/body/footer
|
|
14
|
+
* on one line with `error:1E08010C:DECODER routines::unsupported`)
|
|
15
|
+
*/
|
|
16
|
+
function normalizePrivateKey(raw) {
|
|
17
|
+
const key = raw.replace(/\\n/g, "\n");
|
|
18
|
+
if (key.includes("\n")) return key;
|
|
19
|
+
const flattened = key.trim().match(/^(-----BEGIN [A-Z0-9 ]+-----)\s*(.+?)\s*(-----END [A-Z0-9 ]+-----)$/);
|
|
20
|
+
if (!flattened) return key;
|
|
21
|
+
const body = flattened[2].replace(/\s+/g, "");
|
|
22
|
+
return `${flattened[1]}\n${body.match(/.{1,64}/g).join("\n")}\n${flattened[3]}\n`;
|
|
23
|
+
}
|
|
24
|
+
/** Page size for issue/PR listings; one GitHub API call per page. */
|
|
25
|
+
const LIST_PAGE_SIZE = 30;
|
|
26
|
+
/** Human-readable names of the required config fields, for construction errors. */
|
|
27
|
+
const REQUIRED_FIELDS = [
|
|
28
|
+
"appId",
|
|
29
|
+
"privateKey",
|
|
30
|
+
"clientId",
|
|
31
|
+
"clientSecret",
|
|
32
|
+
"slug"
|
|
33
|
+
];
|
|
34
|
+
var GithubIntegration = class {
|
|
35
|
+
/** Stable integration identifier (see `../factory-integration.ts`). */
|
|
36
|
+
id = "github";
|
|
37
|
+
intake = {
|
|
38
|
+
resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
|
|
39
|
+
listSources: async ({ orgId }) => {
|
|
40
|
+
const installations = await this.sourceControlStorage.installations.list({ orgId });
|
|
41
|
+
return (await Promise.all(installations.map((installation) => this.sourceControlStorage.repositories.list({
|
|
42
|
+
orgId,
|
|
43
|
+
installationId: installation.id
|
|
44
|
+
})))).flat().map((repository) => ({
|
|
45
|
+
id: repository.id,
|
|
46
|
+
name: repository.slug,
|
|
47
|
+
type: "repository",
|
|
48
|
+
metadata: { defaultBranch: repository.defaultBranch }
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
listItems: async ({ orgId, sourceIds, cursor }) => {
|
|
52
|
+
const page = cursor ? Number.parseInt(cursor, 10) : 1;
|
|
53
|
+
const requestedPage = Number.isSafeInteger(page) && page > 0 ? page : 1;
|
|
54
|
+
const pages = await Promise.all(sourceIds.map(async (sourceId) => {
|
|
55
|
+
const repository = await this.sourceControlStorage.repositories.get({
|
|
56
|
+
orgId,
|
|
57
|
+
id: sourceId
|
|
58
|
+
});
|
|
59
|
+
if (!repository) return {
|
|
60
|
+
items: [],
|
|
61
|
+
hasNextPage: false
|
|
62
|
+
};
|
|
63
|
+
const installation = await this.sourceControlStorage.installations.get({
|
|
64
|
+
orgId,
|
|
65
|
+
id: repository.installationId
|
|
66
|
+
});
|
|
67
|
+
if (!installation) return {
|
|
68
|
+
items: [],
|
|
69
|
+
hasNextPage: false
|
|
70
|
+
};
|
|
71
|
+
const installationId = Number.parseInt(installation.externalId, 10);
|
|
72
|
+
if (!Number.isSafeInteger(installationId)) return {
|
|
73
|
+
items: [],
|
|
74
|
+
hasNextPage: false
|
|
75
|
+
};
|
|
76
|
+
const [issues, pullRequests] = await Promise.all([this.listRepoOpenIssues(installationId, repository.slug, requestedPage), this.versionControl.listPullRequests({
|
|
77
|
+
connection: {
|
|
78
|
+
type: "app-installation",
|
|
79
|
+
installationId
|
|
80
|
+
},
|
|
81
|
+
sourceId: repository.slug,
|
|
82
|
+
includeDrafts: false,
|
|
83
|
+
cursor: String(requestedPage)
|
|
84
|
+
})]);
|
|
85
|
+
return {
|
|
86
|
+
items: [...issues.issues.map((issue) => ({
|
|
87
|
+
source: {
|
|
88
|
+
type: "issue",
|
|
89
|
+
externalId: `${repository.externalId}:${issue.number}`,
|
|
90
|
+
url: issue.url
|
|
91
|
+
},
|
|
92
|
+
sourceId: repository.id,
|
|
93
|
+
title: issue.title,
|
|
94
|
+
status: "open",
|
|
95
|
+
labels: issue.labels,
|
|
96
|
+
createdAt: issue.createdAt,
|
|
97
|
+
updatedAt: issue.updatedAt,
|
|
98
|
+
metadata: {
|
|
99
|
+
repository: repository.slug,
|
|
100
|
+
number: issue.number,
|
|
101
|
+
author: issue.author
|
|
102
|
+
}
|
|
103
|
+
})), ...pullRequests.pullRequests.map((pullRequest) => ({
|
|
104
|
+
source: {
|
|
105
|
+
type: "pull-request",
|
|
106
|
+
externalId: `${repository.externalId}:${pullRequest.id}`,
|
|
107
|
+
url: pullRequest.url
|
|
108
|
+
},
|
|
109
|
+
sourceId: repository.id,
|
|
110
|
+
title: pullRequest.title,
|
|
111
|
+
status: "open",
|
|
112
|
+
createdAt: pullRequest.createdAt,
|
|
113
|
+
updatedAt: pullRequest.updatedAt,
|
|
114
|
+
metadata: {
|
|
115
|
+
repository: repository.slug,
|
|
116
|
+
number: Number(pullRequest.id),
|
|
117
|
+
author: pullRequest.author,
|
|
118
|
+
baseBranch: pullRequest.baseBranch,
|
|
119
|
+
headBranch: pullRequest.headBranch
|
|
120
|
+
}
|
|
121
|
+
}))],
|
|
122
|
+
hasNextPage: issues.nextPage !== null || pullRequests.nextCursor !== null
|
|
123
|
+
};
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
items: pages.flatMap((result) => result.items),
|
|
127
|
+
nextCursor: pages.some((result) => result.hasNextPage) ? String(requestedPage + 1) : null
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
listIssues: (input) => this.#listIntakeIssues(input),
|
|
131
|
+
getIssue: (input) => this.#getIntakeIssue(input),
|
|
132
|
+
createComment: (input) => this.#createIntakeComment(input),
|
|
133
|
+
updateIssue: (input) => this.#updateIntakeIssue(input)
|
|
134
|
+
};
|
|
135
|
+
versionControl = {
|
|
136
|
+
initialize: ({ storage }) => {
|
|
137
|
+
this.#sourceControlStorage = storage;
|
|
138
|
+
},
|
|
139
|
+
registerInstallation: ({ orgId, userId, installation }) => this.sourceControlStorage.installations.upsert({
|
|
140
|
+
orgId,
|
|
141
|
+
connectedByUserId: userId,
|
|
142
|
+
externalId: installation.externalId,
|
|
143
|
+
accountName: installation.accountName,
|
|
144
|
+
accountType: installation.accountType,
|
|
145
|
+
providerMetadata: installation.metadata
|
|
146
|
+
}),
|
|
147
|
+
registerRepositories: ({ orgId, installationId, repositories }) => Promise.all(repositories.map((repository) => this.sourceControlStorage.repositories.upsert({
|
|
148
|
+
orgId,
|
|
149
|
+
input: {
|
|
150
|
+
installationId,
|
|
151
|
+
externalId: repository.externalId,
|
|
152
|
+
slug: repository.slug,
|
|
153
|
+
defaultBranch: repository.defaultBranch,
|
|
154
|
+
providerMetadata: repository.metadata
|
|
155
|
+
}
|
|
156
|
+
}))),
|
|
157
|
+
getRepositoryAccess: async ({ orgId, repositoryId }) => {
|
|
158
|
+
const repository = await this.sourceControlStorage.repositories.get({
|
|
159
|
+
orgId,
|
|
160
|
+
id: repositoryId
|
|
161
|
+
});
|
|
162
|
+
if (!repository) throw new Error("Version-control repository not found.");
|
|
163
|
+
const installation = await this.sourceControlStorage.installations.get({
|
|
164
|
+
orgId,
|
|
165
|
+
id: repository.installationId
|
|
166
|
+
});
|
|
167
|
+
if (!installation) throw new Error("Version-control installation not found.");
|
|
168
|
+
const installationId = Number.parseInt(installation.externalId, 10);
|
|
169
|
+
if (!Number.isSafeInteger(installationId)) throw new Error("GitHub installation id is invalid.");
|
|
170
|
+
return {
|
|
171
|
+
cloneUrl: `https://github.com/${repository.slug}.git`,
|
|
172
|
+
authorization: {
|
|
173
|
+
scheme: "bearer",
|
|
174
|
+
token: await this.mintInstallationToken(installationId)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
listPullRequests: (input) => this.#listPullRequests(input),
|
|
179
|
+
getPullRequest: (input) => this.#getPullRequest(input),
|
|
180
|
+
createPullRequest: (input) => this.#createPullRequest(input),
|
|
181
|
+
updatePullRequest: (input) => this.#updatePullRequest(input),
|
|
182
|
+
closePullRequest: (input) => this.#closePullRequest(input),
|
|
183
|
+
mergePullRequest: (input) => this.#mergePullRequest(input),
|
|
184
|
+
listComments: (input) => this.#listComments(input),
|
|
185
|
+
createComment: (input) => this.#createComment(input),
|
|
186
|
+
updateComment: (input) => this.#updateComment(input),
|
|
187
|
+
deleteComment: (input) => this.#deleteComment(input),
|
|
188
|
+
listReviews: (input) => this.#listReviews(input),
|
|
189
|
+
getReview: (input) => this.#getReview(input),
|
|
190
|
+
createReview: (input) => this.#createReview(input),
|
|
191
|
+
updateReview: (input) => this.#updateReview(input),
|
|
192
|
+
submitReview: (input) => this.#submitReview(input),
|
|
193
|
+
dismissReview: (input) => this.#dismissReview(input),
|
|
194
|
+
deletePendingReview: (input) => this.#deletePendingReview(input),
|
|
195
|
+
listReviewComments: (input) => this.#listReviewComments(input),
|
|
196
|
+
createReviewComment: (input) => this.#createReviewComment(input),
|
|
197
|
+
updateReviewComment: (input) => this.#updateReviewComment(input),
|
|
198
|
+
deleteReviewComment: (input) => this.#deleteReviewComment(input),
|
|
199
|
+
listRequestedReviewers: (input) => this.#listRequestedReviewers(input),
|
|
200
|
+
requestReviewers: (input) => this.#requestReviewers(input),
|
|
201
|
+
removeRequestedReviewers: (input) => this.#removeReviewers(input)
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* The OAuth/install flow round-trips a signed `state` through GitHub, so a
|
|
205
|
+
* multi-replica deploy needs a deployment-stable state secret.
|
|
206
|
+
*/
|
|
207
|
+
requiresStableStateSigner = true;
|
|
208
|
+
#appId;
|
|
209
|
+
#privateKey;
|
|
210
|
+
#clientId;
|
|
211
|
+
#clientSecret;
|
|
212
|
+
#slug;
|
|
213
|
+
#webhookSecret;
|
|
214
|
+
#storage;
|
|
215
|
+
#sourceControlStorage;
|
|
216
|
+
constructor(config) {
|
|
217
|
+
const missing = REQUIRED_FIELDS.filter((field) => !config[field]);
|
|
218
|
+
if (missing.length > 0) throw new Error(`GithubIntegration: missing required config field(s): ${missing.join(", ")}. Provide the full GitHub App credentials (appId, privateKey, clientId, clientSecret, slug) or omit the integration to disable GitHub-backed repositories.`);
|
|
219
|
+
this.#appId = config.appId;
|
|
220
|
+
this.#privateKey = normalizePrivateKey(config.privateKey);
|
|
221
|
+
this.#clientId = config.clientId;
|
|
222
|
+
this.#clientSecret = config.clientSecret;
|
|
223
|
+
this.#slug = config.slug;
|
|
224
|
+
this.#webhookSecret = config.webhookSecret || void 0;
|
|
225
|
+
}
|
|
226
|
+
/** App slug — the URL name used to build the install URL. */
|
|
227
|
+
get slug() {
|
|
228
|
+
return this.#slug;
|
|
229
|
+
}
|
|
230
|
+
get genericStorage() {
|
|
231
|
+
if (!this.#storage) throw new Error("GithubIntegration storage has not been initialized.");
|
|
232
|
+
return this.#storage.generic;
|
|
233
|
+
}
|
|
234
|
+
get sourceControlStorage() {
|
|
235
|
+
const storage = this.#sourceControlStorage ?? this.#storage?.sourceControl;
|
|
236
|
+
if (!storage) throw new Error("GithubIntegration source-control storage has not been initialized.");
|
|
237
|
+
return storage;
|
|
238
|
+
}
|
|
239
|
+
get integrationStorage() {
|
|
240
|
+
if (!this.#storage) throw new Error("GithubIntegration storage has not been initialized.");
|
|
241
|
+
return this.#storage.generic;
|
|
242
|
+
}
|
|
243
|
+
/** Resolve a stored GitHub locator without scanning installations or repositories. */
|
|
244
|
+
async #resolveIntakeDispatch({ orgId, externalSource }) {
|
|
245
|
+
const target = parseGithubExternalTarget(externalSource.externalId);
|
|
246
|
+
if (!target) return null;
|
|
247
|
+
const repository = await this.sourceControlStorage.repositories.findByExternalId({
|
|
248
|
+
orgId,
|
|
249
|
+
externalId: target.repositoryId
|
|
250
|
+
});
|
|
251
|
+
if (!repository) return null;
|
|
252
|
+
const installation = await this.sourceControlStorage.installations.get({
|
|
253
|
+
orgId,
|
|
254
|
+
id: repository.installationId
|
|
255
|
+
});
|
|
256
|
+
if (!installation) return null;
|
|
257
|
+
const installationId = Number.parseInt(installation.externalId, 10);
|
|
258
|
+
if (!Number.isSafeInteger(installationId)) return null;
|
|
259
|
+
return {
|
|
260
|
+
connection: {
|
|
261
|
+
type: "app-installation",
|
|
262
|
+
installationId
|
|
263
|
+
},
|
|
264
|
+
sourceId: repository.slug,
|
|
265
|
+
issueId: target.issueId
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** Secret GitHub uses to sign webhook deliveries, when configured. */
|
|
269
|
+
get webhookSecret() {
|
|
270
|
+
return this.#webhookSecret;
|
|
271
|
+
}
|
|
272
|
+
#appAuth() {
|
|
273
|
+
return {
|
|
274
|
+
appId: this.#appId,
|
|
275
|
+
privateKey: this.#privateKey,
|
|
276
|
+
clientId: this.#clientId,
|
|
277
|
+
clientSecret: this.#clientSecret
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Octokit authenticated as the GitHub App itself (app JWT). Used for
|
|
282
|
+
* app-level operations and to mint installation tokens.
|
|
283
|
+
*/
|
|
284
|
+
getAppOctokit() {
|
|
285
|
+
return new Octokit({
|
|
286
|
+
authStrategy: createAppAuth,
|
|
287
|
+
auth: this.#appAuth()
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Octokit authenticated as a specific installation (installation access
|
|
292
|
+
* token). Used to list repos and to operate on a repo on the user's behalf.
|
|
293
|
+
*/
|
|
294
|
+
getInstallationOctokit(installationId) {
|
|
295
|
+
return new Octokit({
|
|
296
|
+
authStrategy: createAppAuth,
|
|
297
|
+
auth: {
|
|
298
|
+
...this.#appAuth(),
|
|
299
|
+
installationId
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Octokit authenticated as a user via their OAuth token (the identify step). */
|
|
304
|
+
getUserOctokit(userToken) {
|
|
305
|
+
return new Octokit({ auth: userToken });
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Mint a short-lived installation access token. Returned token is used only
|
|
309
|
+
* server-side / inside the sandbox clone URL and never sent to the browser.
|
|
310
|
+
*/
|
|
311
|
+
async mintInstallationToken(installationId) {
|
|
312
|
+
return (await createAppAuth(this.#appAuth())({
|
|
313
|
+
type: "installation",
|
|
314
|
+
installationId
|
|
315
|
+
})).token;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* List the installations the authenticated user can access, via their OAuth
|
|
319
|
+
* token (`GET /user/installations`).
|
|
320
|
+
*/
|
|
321
|
+
async listUserInstallations(userToken) {
|
|
322
|
+
const octokit = this.getUserOctokit(userToken);
|
|
323
|
+
return (await octokit.paginate(octokit.apps.listInstallationsForAuthenticatedUser, { per_page: 100 })).map((inst) => ({
|
|
324
|
+
installationId: inst.id,
|
|
325
|
+
accountLogin: inst.account && "login" in inst.account ? inst.account.login : null,
|
|
326
|
+
accountType: inst.account && "type" in inst.account ? inst.account.type : null
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
/** List repos accessible to an installation (paginated). */
|
|
330
|
+
async listInstallationRepos(installationId) {
|
|
331
|
+
const octokit = this.getInstallationOctokit(installationId);
|
|
332
|
+
return (await octokit.paginate(octokit.apps.listReposAccessibleToInstallation, { per_page: 100 })).map((repo) => ({
|
|
333
|
+
id: repo.id,
|
|
334
|
+
fullName: repo.full_name,
|
|
335
|
+
name: repo.name,
|
|
336
|
+
owner: repo.owner.login,
|
|
337
|
+
defaultBranch: repo.default_branch,
|
|
338
|
+
private: repo.private,
|
|
339
|
+
installationId
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The authenticated user's permission level on a repo, through the
|
|
344
|
+
* installation. `undefined` when the repo/user is not accessible.
|
|
345
|
+
*/
|
|
346
|
+
async getRepositoryCollaboratorPermission(installationId, repoFullName, username, signal) {
|
|
347
|
+
const parts = splitRepoFullName(repoFullName);
|
|
348
|
+
if (!parts) return void 0;
|
|
349
|
+
try {
|
|
350
|
+
const { data } = await this.getInstallationOctokit(installationId).repos.getCollaboratorPermissionLevel({
|
|
351
|
+
...parts,
|
|
352
|
+
username,
|
|
353
|
+
request: { signal }
|
|
354
|
+
});
|
|
355
|
+
return data.permission;
|
|
356
|
+
} catch {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Fetch a single repo's metadata through an installation token and confirm
|
|
362
|
+
* the installation actually has access to it. Returns `null` when the repo
|
|
363
|
+
* is not accessible to the installation (so a client can't create a project
|
|
364
|
+
* for an arbitrary repo under an installation id it merely owns).
|
|
365
|
+
*/
|
|
366
|
+
async getInstallationRepo(installationId, repoFullName) {
|
|
367
|
+
const parts = splitRepoFullName(repoFullName);
|
|
368
|
+
if (!parts) return null;
|
|
369
|
+
const octokit = this.getInstallationOctokit(installationId);
|
|
370
|
+
try {
|
|
371
|
+
const { data } = await octokit.repos.get(parts);
|
|
372
|
+
return {
|
|
373
|
+
id: data.id,
|
|
374
|
+
fullName: data.full_name,
|
|
375
|
+
name: data.name,
|
|
376
|
+
owner: data.owner.login,
|
|
377
|
+
defaultBranch: data.default_branch,
|
|
378
|
+
private: data.private,
|
|
379
|
+
installationId
|
|
380
|
+
};
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async #listIntakeIssues(input) {
|
|
386
|
+
const installationId = getGithubInstallationId(input.connection);
|
|
387
|
+
const repoFullName = getSingleSourceId(input.sourceIds, "GitHub Intake requires exactly one repository source.");
|
|
388
|
+
const page = parsePositiveCursor(input.cursor);
|
|
389
|
+
const labels = normalizeLabels(input.labels);
|
|
390
|
+
const result = await this.listRepoOpenIssues(installationId, repoFullName, page, { label: labels.length > 0 ? labels.join(",") : void 0 });
|
|
391
|
+
return {
|
|
392
|
+
issues: result.issues.map((issue) => ({
|
|
393
|
+
id: String(issue.number),
|
|
394
|
+
identifier: `#${issue.number}`,
|
|
395
|
+
title: issue.title,
|
|
396
|
+
url: issue.url,
|
|
397
|
+
author: issue.author,
|
|
398
|
+
state: "open",
|
|
399
|
+
stateType: "open",
|
|
400
|
+
priority: null,
|
|
401
|
+
assignee: null,
|
|
402
|
+
source: repoFullName,
|
|
403
|
+
labels: issue.labels,
|
|
404
|
+
commentCount: issue.comments,
|
|
405
|
+
createdAt: issue.createdAt,
|
|
406
|
+
updatedAt: issue.updatedAt
|
|
407
|
+
})),
|
|
408
|
+
nextCursor: result.nextPage === null ? null : String(result.nextPage)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async #getIntakeIssue(input) {
|
|
412
|
+
const installationId = getGithubInstallationId(input.connection);
|
|
413
|
+
const repoFullName = requireSourceId(input.sourceId, "GitHub Intake requires a repository source.");
|
|
414
|
+
const parts = splitRepoFullName(repoFullName);
|
|
415
|
+
const issueNumber = parsePositiveInteger(input.issueId);
|
|
416
|
+
if (!parts || issueNumber === null) return null;
|
|
417
|
+
const octokit = this.getInstallationOctokit(installationId);
|
|
418
|
+
try {
|
|
419
|
+
const [{ data: issue }, comments] = await Promise.all([octokit.issues.get({
|
|
420
|
+
owner: parts.owner,
|
|
421
|
+
repo: parts.repo,
|
|
422
|
+
issue_number: issueNumber
|
|
423
|
+
}), octokit.paginate(octokit.issues.listComments, {
|
|
424
|
+
owner: parts.owner,
|
|
425
|
+
repo: parts.repo,
|
|
426
|
+
issue_number: issueNumber,
|
|
427
|
+
per_page: 100
|
|
428
|
+
})]);
|
|
429
|
+
if (issue.pull_request) return null;
|
|
430
|
+
return {
|
|
431
|
+
id: String(issue.number),
|
|
432
|
+
identifier: `#${issue.number}`,
|
|
433
|
+
title: issue.title,
|
|
434
|
+
url: issue.html_url,
|
|
435
|
+
author: issue.user?.login ?? null,
|
|
436
|
+
state: issue.state,
|
|
437
|
+
stateType: issue.state,
|
|
438
|
+
priority: null,
|
|
439
|
+
assignee: issue.assignee?.login ?? null,
|
|
440
|
+
source: repoFullName,
|
|
441
|
+
labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
|
|
442
|
+
commentCount: issue.comments,
|
|
443
|
+
createdAt: issue.created_at,
|
|
444
|
+
updatedAt: issue.updated_at,
|
|
445
|
+
description: issue.body?.trim() ? issue.body : null,
|
|
446
|
+
comments: comments.map((comment) => ({
|
|
447
|
+
author: comment.user?.login ?? null,
|
|
448
|
+
body: comment.body ?? "",
|
|
449
|
+
createdAt: comment.created_at
|
|
450
|
+
}))
|
|
451
|
+
};
|
|
452
|
+
} catch (err) {
|
|
453
|
+
if (isNotFoundError(err)) return null;
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async #updateIntakeIssue(input) {
|
|
458
|
+
const installationId = getGithubInstallationId(input.connection);
|
|
459
|
+
const repoFullName = requireSourceId(input.sourceId, "GitHub Intake requires a repository source.");
|
|
460
|
+
const parts = splitRepoFullName(repoFullName);
|
|
461
|
+
const issueNumber = parsePositiveInteger(input.issueId);
|
|
462
|
+
if (!parts || issueNumber === null) return null;
|
|
463
|
+
if (input.state.kind === "byName") {
|
|
464
|
+
console.warn(`GitHub integration: updateIssue byName is not supported (name=${input.state.name}); ignoring.`);
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const targetState = input.state.stateType === "unstarted" || input.state.stateType === "started" ? "open" : "closed";
|
|
468
|
+
const stateReason = targetState === "closed" ? input.state.stateType === "canceled" ? "not_planned" : "completed" : null;
|
|
469
|
+
const octokit = this.getInstallationOctokit(installationId);
|
|
470
|
+
try {
|
|
471
|
+
const { data: current } = await octokit.issues.get({
|
|
472
|
+
owner: parts.owner,
|
|
473
|
+
repo: parts.repo,
|
|
474
|
+
issue_number: issueNumber
|
|
475
|
+
});
|
|
476
|
+
if (current.pull_request) {
|
|
477
|
+
console.warn(`GitHub integration: updateIssue rejected — target ${repoFullName}#${issueNumber} is a pull request.`);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
const { data: issue } = await octokit.issues.update({
|
|
481
|
+
owner: parts.owner,
|
|
482
|
+
repo: parts.repo,
|
|
483
|
+
issue_number: issueNumber,
|
|
484
|
+
state: targetState,
|
|
485
|
+
...stateReason ? { state_reason: stateReason } : {}
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
id: String(issue.number),
|
|
489
|
+
identifier: `#${issue.number}`,
|
|
490
|
+
title: issue.title,
|
|
491
|
+
url: issue.html_url,
|
|
492
|
+
author: issue.user?.login ?? null,
|
|
493
|
+
state: issue.state,
|
|
494
|
+
stateType: issue.state,
|
|
495
|
+
priority: null,
|
|
496
|
+
assignee: issue.assignee?.login ?? null,
|
|
497
|
+
source: repoFullName,
|
|
498
|
+
labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
|
|
499
|
+
commentCount: issue.comments,
|
|
500
|
+
createdAt: issue.created_at,
|
|
501
|
+
updatedAt: issue.updated_at
|
|
502
|
+
};
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (isNotFoundError(err)) return null;
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async #createIntakeComment(input) {
|
|
509
|
+
const installationId = getGithubInstallationId(input.connection);
|
|
510
|
+
const parts = splitRepoFullName(requireSourceId(input.sourceId, "GitHub Intake requires a repository source."));
|
|
511
|
+
const issueNumber = parsePositiveInteger(input.issueId);
|
|
512
|
+
if (!parts || issueNumber === null) return null;
|
|
513
|
+
const octokit = this.getInstallationOctokit(installationId);
|
|
514
|
+
try {
|
|
515
|
+
const { data } = await octokit.issues.createComment({
|
|
516
|
+
owner: parts.owner,
|
|
517
|
+
repo: parts.repo,
|
|
518
|
+
issue_number: issueNumber,
|
|
519
|
+
body: input.body
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
id: String(data.id),
|
|
523
|
+
url: data.html_url
|
|
524
|
+
};
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (isNotFoundError(err)) return null;
|
|
527
|
+
throw err;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
#repositoryClient(connection, sourceId) {
|
|
531
|
+
const installationId = getGithubInstallationId(connection);
|
|
532
|
+
const parts = splitRepoFullName(sourceId);
|
|
533
|
+
if (!parts) throw new Error("GitHub pull requests require an owner/repository source.");
|
|
534
|
+
return {
|
|
535
|
+
octokit: this.getInstallationOctokit(installationId),
|
|
536
|
+
parts
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async #listPullRequests(input) {
|
|
540
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
541
|
+
const page = parsePositiveCursor(input.cursor);
|
|
542
|
+
const response = await octokit.pulls.list({
|
|
543
|
+
...parts,
|
|
544
|
+
state: input.state ?? "open",
|
|
545
|
+
per_page: 30,
|
|
546
|
+
page
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
pullRequests: response.data.filter((pr) => input.includeDrafts !== false || !pr.draft).map((pr) => parsePullRequest(pr)),
|
|
550
|
+
nextCursor: response.data.length === 30 ? String(page + 1) : null
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async #getPullRequest(input) {
|
|
554
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
555
|
+
const pullNumber = requirePullRequestNumber(input.pullRequestId);
|
|
556
|
+
try {
|
|
557
|
+
const { data } = await octokit.pulls.get({
|
|
558
|
+
...parts,
|
|
559
|
+
pull_number: pullNumber
|
|
560
|
+
});
|
|
561
|
+
return parsePullRequest(data);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (isNotFoundError(err)) return null;
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async #createPullRequest(input) {
|
|
568
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
569
|
+
const { data } = await octokit.pulls.create({
|
|
570
|
+
...parts,
|
|
571
|
+
title: input.title,
|
|
572
|
+
body: input.body,
|
|
573
|
+
base: input.baseBranch,
|
|
574
|
+
head: input.headBranch,
|
|
575
|
+
draft: input.draft
|
|
576
|
+
});
|
|
577
|
+
return parsePullRequest(data);
|
|
578
|
+
}
|
|
579
|
+
async #updatePullRequest(input) {
|
|
580
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
581
|
+
const { data } = await octokit.pulls.update({
|
|
582
|
+
...parts,
|
|
583
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
584
|
+
title: input.title,
|
|
585
|
+
body: input.body === null ? "" : input.body,
|
|
586
|
+
base: input.baseBranch,
|
|
587
|
+
state: input.state
|
|
588
|
+
});
|
|
589
|
+
return parsePullRequest(data);
|
|
590
|
+
}
|
|
591
|
+
async #closePullRequest(input) {
|
|
592
|
+
return this.#updatePullRequest({
|
|
593
|
+
...input,
|
|
594
|
+
state: "closed"
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async #mergePullRequest(input) {
|
|
598
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
599
|
+
const { data } = await octokit.pulls.merge({
|
|
600
|
+
...parts,
|
|
601
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
602
|
+
commit_title: input.commitTitle,
|
|
603
|
+
commit_message: input.commitMessage,
|
|
604
|
+
merge_method: input.method
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
merged: data.merged,
|
|
608
|
+
message: data.message,
|
|
609
|
+
sha: data.sha ?? null
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
async #listComments(input) {
|
|
613
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
614
|
+
const page = parsePositiveCursor(input.cursor);
|
|
615
|
+
const response = await octokit.issues.listComments({
|
|
616
|
+
...parts,
|
|
617
|
+
issue_number: requirePullRequestNumber(input.pullRequestId),
|
|
618
|
+
per_page: 30,
|
|
619
|
+
page
|
|
620
|
+
});
|
|
621
|
+
return {
|
|
622
|
+
comments: response.data.map((comment) => parsePullRequestComment(comment)),
|
|
623
|
+
nextCursor: response.data.length === 30 ? String(page + 1) : null
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async #createComment(input) {
|
|
627
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
628
|
+
const { data } = await octokit.issues.createComment({
|
|
629
|
+
...parts,
|
|
630
|
+
issue_number: requirePullRequestNumber(input.pullRequestId),
|
|
631
|
+
body: input.body
|
|
632
|
+
});
|
|
633
|
+
return parsePullRequestComment(data);
|
|
634
|
+
}
|
|
635
|
+
async #updateComment(input) {
|
|
636
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
637
|
+
const { data } = await octokit.issues.updateComment({
|
|
638
|
+
...parts,
|
|
639
|
+
comment_id: requirePositiveId(input.commentId, "comment"),
|
|
640
|
+
body: input.body
|
|
641
|
+
});
|
|
642
|
+
return parsePullRequestComment(data);
|
|
643
|
+
}
|
|
644
|
+
async #deleteComment(input) {
|
|
645
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
646
|
+
await octokit.issues.deleteComment({
|
|
647
|
+
...parts,
|
|
648
|
+
comment_id: requirePositiveId(input.commentId, "comment")
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async #listReviews(input) {
|
|
652
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
653
|
+
const page = parsePositiveCursor(input.cursor);
|
|
654
|
+
const response = await octokit.pulls.listReviews({
|
|
655
|
+
...parts,
|
|
656
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
657
|
+
per_page: 30,
|
|
658
|
+
page
|
|
659
|
+
});
|
|
660
|
+
return {
|
|
661
|
+
reviews: response.data.map((review) => parseReview(review)),
|
|
662
|
+
nextCursor: response.data.length === 30 ? String(page + 1) : null
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async #getReview(input) {
|
|
666
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
667
|
+
try {
|
|
668
|
+
const { data } = await octokit.pulls.getReview({
|
|
669
|
+
...parts,
|
|
670
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
671
|
+
review_id: requirePositiveId(input.reviewId, "review")
|
|
672
|
+
});
|
|
673
|
+
return parseReview(data);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
if (isNotFoundError(err)) return null;
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async #createReview(input) {
|
|
680
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
681
|
+
const { data } = await octokit.pulls.createReview({
|
|
682
|
+
...parts,
|
|
683
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
684
|
+
body: input.body,
|
|
685
|
+
commit_id: input.commitId,
|
|
686
|
+
event: input.event ? reviewEventToGithub(input.event) : void 0
|
|
687
|
+
});
|
|
688
|
+
return parseReview(data);
|
|
689
|
+
}
|
|
690
|
+
async #updateReview(input) {
|
|
691
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
692
|
+
const { data } = await octokit.pulls.updateReview({
|
|
693
|
+
...parts,
|
|
694
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
695
|
+
review_id: requirePositiveId(input.reviewId, "review"),
|
|
696
|
+
body: input.body
|
|
697
|
+
});
|
|
698
|
+
return parseReview(data);
|
|
699
|
+
}
|
|
700
|
+
async #submitReview(input) {
|
|
701
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
702
|
+
const { data } = await octokit.pulls.submitReview({
|
|
703
|
+
...parts,
|
|
704
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
705
|
+
review_id: requirePositiveId(input.reviewId, "review"),
|
|
706
|
+
body: input.body,
|
|
707
|
+
event: reviewEventToGithub(input.event)
|
|
708
|
+
});
|
|
709
|
+
return parseReview(data);
|
|
710
|
+
}
|
|
711
|
+
async #dismissReview(input) {
|
|
712
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
713
|
+
const { data } = await octokit.pulls.dismissReview({
|
|
714
|
+
...parts,
|
|
715
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
716
|
+
review_id: requirePositiveId(input.reviewId, "review"),
|
|
717
|
+
message: input.message
|
|
718
|
+
});
|
|
719
|
+
return parseReview(data);
|
|
720
|
+
}
|
|
721
|
+
async #deletePendingReview(input) {
|
|
722
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
723
|
+
await octokit.pulls.deletePendingReview({
|
|
724
|
+
...parts,
|
|
725
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
726
|
+
review_id: requirePositiveId(input.reviewId, "review")
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
async #listReviewComments(input) {
|
|
730
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
731
|
+
const page = parsePositiveCursor(input.cursor);
|
|
732
|
+
const response = await octokit.pulls.listReviewComments({
|
|
733
|
+
...parts,
|
|
734
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
735
|
+
per_page: 30,
|
|
736
|
+
page
|
|
737
|
+
});
|
|
738
|
+
return {
|
|
739
|
+
comments: response.data.map((comment) => parseReviewComment(comment)),
|
|
740
|
+
nextCursor: response.data.length === 30 ? String(page + 1) : null
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async #createReviewComment(input) {
|
|
744
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
745
|
+
const pullNumber = requirePullRequestNumber(input.pullRequestId);
|
|
746
|
+
if (input.replyToId) {
|
|
747
|
+
const { data } = await octokit.pulls.createReplyForReviewComment({
|
|
748
|
+
...parts,
|
|
749
|
+
pull_number: pullNumber,
|
|
750
|
+
comment_id: requirePositiveId(input.replyToId, "review comment"),
|
|
751
|
+
body: input.body
|
|
752
|
+
});
|
|
753
|
+
return parseReviewComment(data);
|
|
754
|
+
}
|
|
755
|
+
if (!input.commitId || !input.path || input.line === void 0 || !input.side) throw new Error("A review comment requires commitId, path, line, and side unless it is a reply.");
|
|
756
|
+
if (input.startLine === void 0 !== (input.startSide === void 0)) throw new Error("A multi-line review comment requires both startLine and startSide.");
|
|
757
|
+
const { data } = await octokit.pulls.createReviewComment({
|
|
758
|
+
...parts,
|
|
759
|
+
pull_number: pullNumber,
|
|
760
|
+
body: input.body,
|
|
761
|
+
commit_id: input.commitId,
|
|
762
|
+
path: input.path,
|
|
763
|
+
line: input.line,
|
|
764
|
+
side: input.side.toUpperCase(),
|
|
765
|
+
start_line: input.startLine,
|
|
766
|
+
start_side: input.startSide?.toUpperCase()
|
|
767
|
+
});
|
|
768
|
+
return parseReviewComment(data);
|
|
769
|
+
}
|
|
770
|
+
async #updateReviewComment(input) {
|
|
771
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
772
|
+
const { data } = await octokit.pulls.updateReviewComment({
|
|
773
|
+
...parts,
|
|
774
|
+
comment_id: requirePositiveId(input.commentId, "review comment"),
|
|
775
|
+
body: input.body
|
|
776
|
+
});
|
|
777
|
+
return parseReviewComment(data);
|
|
778
|
+
}
|
|
779
|
+
async #deleteReviewComment(input) {
|
|
780
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
781
|
+
await octokit.pulls.deleteReviewComment({
|
|
782
|
+
...parts,
|
|
783
|
+
comment_id: requirePositiveId(input.commentId, "review comment")
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async #listRequestedReviewers(input) {
|
|
787
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
788
|
+
const { data } = await octokit.pulls.listRequestedReviewers({
|
|
789
|
+
...parts,
|
|
790
|
+
pull_number: requirePullRequestNumber(input.pullRequestId)
|
|
791
|
+
});
|
|
792
|
+
return parseRequestedReviewers(data);
|
|
793
|
+
}
|
|
794
|
+
async #requestReviewers(input) {
|
|
795
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
796
|
+
const { data } = await octokit.pulls.requestReviewers({
|
|
797
|
+
...parts,
|
|
798
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
799
|
+
reviewers: input.users ?? [],
|
|
800
|
+
team_reviewers: input.teams ?? []
|
|
801
|
+
});
|
|
802
|
+
return parseRequestedReviewers(data);
|
|
803
|
+
}
|
|
804
|
+
async #removeReviewers(input) {
|
|
805
|
+
const { octokit, parts } = this.#repositoryClient(input.connection, input.sourceId);
|
|
806
|
+
const { data } = await octokit.pulls.removeRequestedReviewers({
|
|
807
|
+
...parts,
|
|
808
|
+
pull_number: requirePullRequestNumber(input.pullRequestId),
|
|
809
|
+
reviewers: input.users ?? [],
|
|
810
|
+
team_reviewers: input.teams ?? []
|
|
811
|
+
});
|
|
812
|
+
return parseRequestedReviewers(data);
|
|
813
|
+
}
|
|
814
|
+
/** Add labels to an issue (deduplicated; no-op on empty/malformed input). */
|
|
815
|
+
async addIssueLabels(installationId, repoFullName, issueNumber, labels) {
|
|
816
|
+
const parts = splitRepoFullName(repoFullName);
|
|
817
|
+
if (!parts) return;
|
|
818
|
+
const uniqueLabels = [...new Set(labels.map((label) => label.trim()).filter(Boolean))];
|
|
819
|
+
if (uniqueLabels.length === 0) return;
|
|
820
|
+
await this.getInstallationOctokit(installationId).issues.addLabels({
|
|
821
|
+
owner: parts.owner,
|
|
822
|
+
repo: parts.repo,
|
|
823
|
+
issue_number: issueNumber,
|
|
824
|
+
labels: uniqueLabels
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* List one page of a repo's open issues through an installation token. The
|
|
829
|
+
* issues API also returns pull requests, so those are filtered out (the
|
|
830
|
+
* filter can make a non-final page shorter than the page size — `nextPage`
|
|
831
|
+
* is derived from the raw response length, not the filtered one).
|
|
832
|
+
*/
|
|
833
|
+
async listRepoOpenIssues(installationId, repoFullName, page, options = {}) {
|
|
834
|
+
const parts = splitRepoFullName(repoFullName);
|
|
835
|
+
if (!parts) return {
|
|
836
|
+
issues: [],
|
|
837
|
+
nextPage: null
|
|
838
|
+
};
|
|
839
|
+
const response = await this.getInstallationOctokit(installationId).issues.listForRepo({
|
|
840
|
+
owner: parts.owner,
|
|
841
|
+
repo: parts.repo,
|
|
842
|
+
state: "open",
|
|
843
|
+
labels: options.label,
|
|
844
|
+
per_page: 30,
|
|
845
|
+
page
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
issues: response.data.filter((issue) => !issue.pull_request).map((issue) => ({
|
|
849
|
+
number: issue.number,
|
|
850
|
+
title: issue.title,
|
|
851
|
+
url: issue.html_url,
|
|
852
|
+
author: issue.user?.login ?? null,
|
|
853
|
+
labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").filter(Boolean),
|
|
854
|
+
comments: issue.comments,
|
|
855
|
+
createdAt: issue.created_at,
|
|
856
|
+
updatedAt: issue.updated_at
|
|
857
|
+
})),
|
|
858
|
+
nextPage: response.data.length === 30 ? page + 1 : null
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Build the GitHub App install URL. `state` is carried through the install
|
|
863
|
+
* flow and validated on callback.
|
|
864
|
+
*/
|
|
865
|
+
buildInstallUrl(state) {
|
|
866
|
+
const url = new URL(`https://github.com/apps/${this.#slug}/installations/new`);
|
|
867
|
+
url.searchParams.set("state", state);
|
|
868
|
+
return url.toString();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Build the OAuth identify URL (authorize) used to confirm the user's
|
|
872
|
+
* identity and obtain a user token for listing their installations.
|
|
873
|
+
*/
|
|
874
|
+
buildOAuthIdentifyUrl(state, redirectUri) {
|
|
875
|
+
const url = new URL("https://github.com/login/oauth/authorize");
|
|
876
|
+
url.searchParams.set("client_id", this.#clientId);
|
|
877
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
878
|
+
url.searchParams.set("state", state);
|
|
879
|
+
return url.toString();
|
|
880
|
+
}
|
|
881
|
+
/** Exchange an OAuth `code` for a user access token. */
|
|
882
|
+
async exchangeOAuthCode(code, redirectUri) {
|
|
883
|
+
const res = await fetch("https://github.com/login/oauth/access_token", {
|
|
884
|
+
method: "POST",
|
|
885
|
+
signal: AbortSignal.timeout(1e4),
|
|
886
|
+
headers: {
|
|
887
|
+
"content-type": "application/json",
|
|
888
|
+
accept: "application/json"
|
|
889
|
+
},
|
|
890
|
+
body: JSON.stringify({
|
|
891
|
+
client_id: this.#clientId,
|
|
892
|
+
client_secret: this.#clientSecret,
|
|
893
|
+
code,
|
|
894
|
+
redirect_uri: redirectUri
|
|
895
|
+
})
|
|
896
|
+
});
|
|
897
|
+
if (!res.ok) throw new Error(`GitHub OAuth token exchange failed: ${res.status}`);
|
|
898
|
+
const data = await res.json();
|
|
899
|
+
if (!data.access_token) throw new Error(`GitHub OAuth token exchange returned no token: ${data.error_description ?? data.error ?? "unknown"}`);
|
|
900
|
+
return data.access_token;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* The integration's HTTP surface: the `/web/github/*` + `/auth/github/*`
|
|
904
|
+
* Mastra `apiRoutes` (webhook handler, install/OAuth flow, project +
|
|
905
|
+
* worktree + session operations). The factory folds these into the server's
|
|
906
|
+
* `apiRoutes` when the feature is ready. Handlers operate on this instance.
|
|
907
|
+
*/
|
|
908
|
+
routes(ctx) {
|
|
909
|
+
this.#storage = ctx.storage;
|
|
910
|
+
const ingestFactoryEvent = attachGithubRules(this, ctx);
|
|
911
|
+
return buildGithubRoutes({
|
|
912
|
+
github: this,
|
|
913
|
+
auth: ctx.auth,
|
|
914
|
+
fleet: ctx.fleet,
|
|
915
|
+
storage: ctx.factoryStorage,
|
|
916
|
+
stateSigner: ctx.stateSigner,
|
|
917
|
+
baseUrl: ctx.baseUrl,
|
|
918
|
+
controller: ctx.controller,
|
|
919
|
+
runIssueTriage: ctx.controller ? (input) => runGithubIssueTriage({
|
|
920
|
+
controller: ctx.controller,
|
|
921
|
+
input
|
|
922
|
+
}) : void 0,
|
|
923
|
+
emitAudit: ctx.hooks?.emitAudit,
|
|
924
|
+
projects: ctx.storage.projects,
|
|
925
|
+
ingestFactoryEvent
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Session-scoped agent tools for token refresh and PR subscriptions in
|
|
930
|
+
* sessions bound to a GitHub-backed project. Empty elsewhere.
|
|
931
|
+
*/
|
|
932
|
+
sessionTools({ requestContext }) {
|
|
933
|
+
return createGithubSubscriptionTools(requestContext, this);
|
|
934
|
+
}
|
|
935
|
+
async postToolObserver({ toolContext, requestContext }) {
|
|
936
|
+
const pullRequestUrl = parseCreatedPullRequest(toolContext);
|
|
937
|
+
if (!pullRequestUrl || !requestContext) return;
|
|
938
|
+
await subscribeCurrentSessionToPullRequest(requestContext, pullRequestUrl, "auto-gh-pr-create", this);
|
|
939
|
+
}
|
|
940
|
+
/** Non-secret config snapshot for system diagnostics/startup logs. */
|
|
941
|
+
diagnostics() {
|
|
942
|
+
return {
|
|
943
|
+
slug: this.#slug,
|
|
944
|
+
webhookSecretConfigured: this.#webhookSecret !== void 0
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
function parsePullRequest(pr) {
|
|
949
|
+
return {
|
|
950
|
+
id: String(pr.number),
|
|
951
|
+
title: pr.title,
|
|
952
|
+
url: pr.html_url,
|
|
953
|
+
author: pr.user?.login ?? null,
|
|
954
|
+
body: pr.body?.trim() ? pr.body : null,
|
|
955
|
+
state: pr.state === "closed" ? "closed" : "open",
|
|
956
|
+
draft: pr.draft ?? false,
|
|
957
|
+
merged: pr.merged ?? Boolean(pr.merged_at),
|
|
958
|
+
mergeable: typeof pr.mergeable === "boolean" ? pr.mergeable : null,
|
|
959
|
+
baseBranch: pr.base.ref,
|
|
960
|
+
headBranch: pr.head.ref,
|
|
961
|
+
headSha: pr.head.sha,
|
|
962
|
+
createdAt: pr.created_at,
|
|
963
|
+
updatedAt: pr.updated_at
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function parsePullRequestComment(comment) {
|
|
967
|
+
return {
|
|
968
|
+
id: String(comment.id),
|
|
969
|
+
url: comment.html_url,
|
|
970
|
+
author: comment.user?.login ?? null,
|
|
971
|
+
body: comment.body ?? "",
|
|
972
|
+
createdAt: comment.created_at,
|
|
973
|
+
updatedAt: comment.updated_at
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function parseReview(review) {
|
|
977
|
+
return {
|
|
978
|
+
id: String(review.id),
|
|
979
|
+
url: review.html_url ?? null,
|
|
980
|
+
author: review.user?.login ?? null,
|
|
981
|
+
body: review.body?.trim() ? review.body : null,
|
|
982
|
+
state: parseReviewState(review.state),
|
|
983
|
+
commitId: review.commit_id ?? null,
|
|
984
|
+
submittedAt: review.submitted_at ?? null
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function parseReviewComment(comment) {
|
|
988
|
+
const parsed = parsePullRequestComment(comment);
|
|
989
|
+
const side = comment.side?.toLowerCase();
|
|
990
|
+
return {
|
|
991
|
+
...parsed,
|
|
992
|
+
path: comment.path,
|
|
993
|
+
line: comment.line ?? null,
|
|
994
|
+
side: side === "left" || side === "right" ? side : null,
|
|
995
|
+
commitId: comment.commit_id,
|
|
996
|
+
replyToId: comment.in_reply_to_id ? String(comment.in_reply_to_id) : null
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function parseRequestedReviewers(data) {
|
|
1000
|
+
return {
|
|
1001
|
+
users: (data.users ?? data.requested_reviewers ?? []).flatMap((user) => user.login ? [user.login] : []),
|
|
1002
|
+
teams: (data.teams ?? data.requested_teams ?? []).flatMap((team) => team.slug ? [team.slug] : [])
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function parseReviewState(state) {
|
|
1006
|
+
if (state === "PENDING") return "pending";
|
|
1007
|
+
if (state === "COMMENTED") return "commented";
|
|
1008
|
+
if (state === "APPROVED") return "approved";
|
|
1009
|
+
if (state === "CHANGES_REQUESTED") return "changes-requested";
|
|
1010
|
+
if (state === "DISMISSED") return "dismissed";
|
|
1011
|
+
throw new Error(`Unsupported GitHub review state: ${state}`);
|
|
1012
|
+
}
|
|
1013
|
+
function reviewEventToGithub(event) {
|
|
1014
|
+
if (event === "approve") return "APPROVE";
|
|
1015
|
+
if (event === "request-changes") return "REQUEST_CHANGES";
|
|
1016
|
+
return "COMMENT";
|
|
1017
|
+
}
|
|
1018
|
+
function requirePullRequestNumber(value) {
|
|
1019
|
+
return requirePositiveId(value, "pull request");
|
|
1020
|
+
}
|
|
1021
|
+
function requirePositiveId(value, resource) {
|
|
1022
|
+
const parsed = parsePositiveInteger(value);
|
|
1023
|
+
if (parsed === null) throw new Error(`GitHub ${resource} id must be a positive integer.`);
|
|
1024
|
+
return parsed;
|
|
1025
|
+
}
|
|
1026
|
+
function getGithubInstallationId(connection) {
|
|
1027
|
+
if (connection.type !== "app-installation") throw new Error("GitHub capabilities require an app-installation connection.");
|
|
1028
|
+
return connection.installationId;
|
|
1029
|
+
}
|
|
1030
|
+
function getSingleSourceId(sourceIds, message) {
|
|
1031
|
+
if (sourceIds.length !== 1) throw new Error(message);
|
|
1032
|
+
return sourceIds[0];
|
|
1033
|
+
}
|
|
1034
|
+
function normalizeLabels(labels) {
|
|
1035
|
+
return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
|
|
1036
|
+
}
|
|
1037
|
+
function requireSourceId(sourceId, message) {
|
|
1038
|
+
if (!sourceId) throw new Error(message);
|
|
1039
|
+
return sourceId;
|
|
1040
|
+
}
|
|
1041
|
+
function parsePositiveCursor(cursor) {
|
|
1042
|
+
if (cursor === void 0) return 1;
|
|
1043
|
+
const page = parsePositiveInteger(cursor);
|
|
1044
|
+
if (page === null) throw new Error("GitHub cursor must be a positive page number.");
|
|
1045
|
+
return page;
|
|
1046
|
+
}
|
|
1047
|
+
function parsePositiveInteger(value) {
|
|
1048
|
+
if (!/^\d+$/.test(value)) return null;
|
|
1049
|
+
const parsed = Number(value);
|
|
1050
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
1051
|
+
}
|
|
1052
|
+
function isNotFoundError(error) {
|
|
1053
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
1054
|
+
}
|
|
1055
|
+
/** Split an `owner/name` full name into its parts, or `null` when malformed. */
|
|
1056
|
+
function splitRepoFullName(repoFullName) {
|
|
1057
|
+
const slash = repoFullName.indexOf("/");
|
|
1058
|
+
if (slash <= 0 || slash === repoFullName.length - 1) return null;
|
|
1059
|
+
return {
|
|
1060
|
+
owner: repoFullName.slice(0, slash),
|
|
1061
|
+
repo: repoFullName.slice(slash + 1)
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function parseGithubExternalTarget(externalId) {
|
|
1065
|
+
const match = externalId.match(/^github:(\d+):(?:issue|pull-request):(\d+)$/) ?? externalId.match(/^(\d+):(\d+)$/);
|
|
1066
|
+
if (!match?.[1] || !match[2]) return null;
|
|
1067
|
+
const repositoryId = parsePositiveInteger(match[1]);
|
|
1068
|
+
const issueId = parsePositiveInteger(match[2]);
|
|
1069
|
+
return repositoryId && issueId ? {
|
|
1070
|
+
repositoryId: String(repositoryId),
|
|
1071
|
+
issueId: String(issueId)
|
|
1072
|
+
} : null;
|
|
1073
|
+
}
|
|
1074
|
+
//#endregion
|
|
1075
|
+
export { GithubIntegration, LIST_PAGE_SIZE, normalizePrivateKey };
|
|
1076
|
+
|
|
1077
|
+
//# sourceMappingURL=integration.js.map
|