@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2
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 +36 -0
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +401 -314
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +441 -39455
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39466
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3624
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1833
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +401 -326
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -3942
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -915
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28618
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +438 -677
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.d.ts.map +1 -1
- package/dist/spa-static.js +91 -64
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +9 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -702
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -344
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1132
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- 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 +2 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +236 -438
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +9 -2
- package/package.json +9 -9
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
|
@@ -1,3989 +1,892 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "../../github/session-subscriptions.js";
|
|
2
|
+
import { buildGithubRoutes } from "../../github/routes.js";
|
|
3
|
+
import { attachGithubRules } from "../../github/rules.js";
|
|
4
|
+
import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
|
|
5
|
+
import { PlatformGithubEventWorker } from "./event-worker.js";
|
|
6
6
|
import { registerApiRoute } from "@mastra/core/server";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
//#region src/integrations/platform/github/integration.ts
|
|
8
|
+
const PAGE_SIZE = 30;
|
|
9
|
+
const API_PREFIX = "/v1/server";
|
|
10
|
+
/**
|
|
11
|
+
* How long an installation's repository listing may be reused. The repos
|
|
12
|
+
* route and token minting both call it — often several times within one UI
|
|
13
|
+
* interaction — and each call is a full Platform round trip.
|
|
14
|
+
*/
|
|
15
|
+
const INSTALLATION_REPOS_CACHE_TTL_MS = 3e4;
|
|
16
|
+
/**
|
|
17
|
+
* How long a minted repository-scoped token may be reused. GitHub
|
|
18
|
+
* installation tokens live ~60 minutes; 5 minutes keeps a wide validity
|
|
19
|
+
* margin while collapsing the per-session-materialization mint round trip.
|
|
20
|
+
*/
|
|
21
|
+
const REPOSITORY_ACCESS_CACHE_TTL_MS = 5 * 6e4;
|
|
22
|
+
/**
|
|
23
|
+
* Upper bound for both TTL caches. Entries expire lazily on re-access, so
|
|
24
|
+
* without a hard cap keys that stop being queried would accumulate for the
|
|
25
|
+
* integration's (long) lifetime. Insertion-order eviction — matches the
|
|
26
|
+
* bounded verification cache in `@mastra/auth-studio`.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_CACHE_ENTRIES = 1e3;
|
|
29
|
+
function setBounded(cache, key, value) {
|
|
30
|
+
cache.delete(key);
|
|
31
|
+
cache.set(key, value);
|
|
32
|
+
if (cache.size > MAX_CACHE_ENTRIES) {
|
|
33
|
+
const oldest = cache.keys().next().value;
|
|
34
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const REPOSITORY_TOKEN_PERMISSIONS = {
|
|
38
|
+
contents: "write",
|
|
39
|
+
issues: "write",
|
|
40
|
+
pull_requests: "write"
|
|
28
41
|
};
|
|
29
|
-
|
|
30
|
-
// src/integrations/github/config.ts
|
|
31
|
-
var GITHUB_APP_ENV_VARS = [
|
|
32
|
-
"GITHUB_APP_ID",
|
|
33
|
-
"GITHUB_APP_PRIVATE_KEY",
|
|
34
|
-
"GITHUB_APP_CLIENT_ID",
|
|
35
|
-
"GITHUB_APP_CLIENT_SECRET",
|
|
36
|
-
"GITHUB_APP_SLUG"
|
|
37
|
-
];
|
|
38
|
-
function isGithubFeatureEnabled(options) {
|
|
39
|
-
return options.github !== void 0 && options.auth.enabled();
|
|
40
|
-
}
|
|
41
|
-
function getGithubFeatureDiagnostics(options) {
|
|
42
|
-
const { github, auth, appDbConfigured, stateSigner, fleet } = options;
|
|
43
|
-
return {
|
|
44
|
-
githubAppConfigured: github !== void 0,
|
|
45
|
-
factoryAuthEnabled: auth.enabled(),
|
|
46
|
-
appDbConfigured,
|
|
47
|
-
stateSecretConfigured: stateSigner?.stable ?? false,
|
|
48
|
-
sandboxEnabled: fleet?.enabled ?? false,
|
|
49
|
-
sandboxProvider: fleet?.provider ?? "none",
|
|
50
|
-
missingGithubAppEnvVars: github ? [] : [...GITHUB_APP_ENV_VARS]
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// src/integrations/github/pat.ts
|
|
55
|
-
var PAT_SETTINGS_USER_ID = "__github_org_settings__";
|
|
56
|
-
var FIELD_FOR_KIND = {
|
|
57
|
-
default: "pat",
|
|
58
|
-
reviewer: "reviewerPat"
|
|
59
|
-
};
|
|
60
|
-
function asToken(value) {
|
|
61
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
62
|
-
}
|
|
63
|
-
async function getGithubPat(getStorage, orgId, kind = "default") {
|
|
64
|
-
try {
|
|
65
|
-
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
66
|
-
if (!settings) return null;
|
|
67
|
-
if (kind === "reviewer") return asToken(settings.reviewerPat) ?? asToken(settings.pat);
|
|
68
|
-
return asToken(settings.pat);
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
async function getGithubPatStatus(getStorage, orgId) {
|
|
74
|
-
try {
|
|
75
|
-
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
76
|
-
return {
|
|
77
|
-
configured: asToken(settings?.pat) !== null,
|
|
78
|
-
reviewerConfigured: asToken(settings?.reviewerPat) !== null
|
|
79
|
-
};
|
|
80
|
-
} catch {
|
|
81
|
-
return { configured: false, reviewerConfigured: false };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async function setGithubPat(storage, orgId, pat, kind = "default") {
|
|
85
|
-
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID) ?? {};
|
|
86
|
-
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, { ...existing, [FIELD_FOR_KIND[kind]]: pat });
|
|
87
|
-
}
|
|
88
|
-
async function clearGithubPat(storage, orgId, kind = "default") {
|
|
89
|
-
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
90
|
-
const field = FIELD_FOR_KIND[kind];
|
|
91
|
-
if (!existing?.[field]) return;
|
|
92
|
-
const { [field]: _removed, ...rest } = existing;
|
|
93
|
-
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/integrations/github/sandbox.ts
|
|
97
|
-
import { createHash } from "crypto";
|
|
98
|
-
function bindingStore(row, storage) {
|
|
99
|
-
return {
|
|
100
|
-
sandboxId: row.sandboxId,
|
|
101
|
-
setSandboxId: (id) => id === null ? storage.clearBinding({ id: row.id }) : storage.setSandboxId({ id: row.id, sandboxId: id }),
|
|
102
|
-
clear: () => storage.clearBinding({ id: row.id })
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
async function ensureProjectSandbox(options) {
|
|
106
|
-
const { fleet, row, storage, token, onProgress } = options;
|
|
107
|
-
return fleet.ensureSandbox(bindingStore(row, storage), { GH_TOKEN: token }, onProgress);
|
|
108
|
-
}
|
|
109
|
-
async function teardownProjectSandbox(options) {
|
|
110
|
-
const { fleet, row, storage, sandbox } = options;
|
|
111
|
-
return fleet.teardownSandbox(bindingStore(row, storage), sandbox);
|
|
112
|
-
}
|
|
113
|
-
function shellQuote(value) {
|
|
114
|
-
return `'` + value.split(`'`).join(`'\\''`) + `'`;
|
|
115
|
-
}
|
|
116
|
-
async function sh(sandbox, script) {
|
|
117
|
-
return sandbox.executeCommand("sh", ["-c", script]);
|
|
118
|
-
}
|
|
119
|
-
var MaterializeError = class extends Error {
|
|
120
|
-
constructor(message, code) {
|
|
121
|
-
super(message);
|
|
122
|
-
this.code = code;
|
|
123
|
-
this.name = "MaterializeError";
|
|
124
|
-
}
|
|
125
|
-
code;
|
|
126
|
-
};
|
|
127
|
-
function tokenUrl(repoFullName, token) {
|
|
128
|
-
return `https://x-access-token:${token}@github.com/${repoFullName}.git`;
|
|
129
|
-
}
|
|
130
|
-
function cleanUrl(repoFullName) {
|
|
131
|
-
return `https://github.com/${repoFullName}.git`;
|
|
132
|
-
}
|
|
133
|
-
async function materializeRepo(options) {
|
|
134
|
-
const { row: sandboxRow, repoInfo, sandbox, token, storage, onProgress } = options;
|
|
135
|
-
const workdir = sandboxRow.sandboxWorkdir;
|
|
136
|
-
const repo = repoInfo.repoFullName;
|
|
137
|
-
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
138
|
-
throw new MaterializeError(`Refusing to materialize: invalid repo full name '${repo}'.`, "clone-failed");
|
|
139
|
-
}
|
|
140
|
-
if (!/^[A-Za-z0-9_./-]+$/.test(repoInfo.defaultBranch)) {
|
|
141
|
-
throw new MaterializeError(
|
|
142
|
-
`Refusing to materialize: invalid default branch '${repoInfo.defaultBranch}'.`,
|
|
143
|
-
"clone-failed"
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
const gitVersion = await sh(sandbox, "git --version");
|
|
147
|
-
if (gitVersion.exitCode !== 0) {
|
|
148
|
-
throw new MaterializeError(
|
|
149
|
-
"git is not installed in the sandbox. The sandbox template must include git.",
|
|
150
|
-
"git-missing"
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
const authUrl = tokenUrl(repo, token);
|
|
154
|
-
const alreadyMaterialized = Boolean(sandboxRow.materializedAt) || await hasExistingCheckout(sandbox, workdir, repo);
|
|
155
|
-
try {
|
|
156
|
-
if (!alreadyMaterialized) {
|
|
157
|
-
reportProgress(onProgress, {
|
|
158
|
-
phase: "cloning",
|
|
159
|
-
message: `Cloning ${repo} (first open can take a minute)\u2026`
|
|
160
|
-
});
|
|
161
|
-
const clone = await sh(
|
|
162
|
-
sandbox,
|
|
163
|
-
`git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`
|
|
164
|
-
);
|
|
165
|
-
if (clone.exitCode !== 0) {
|
|
166
|
-
throw classifyGitFailure(clone, "clone-failed");
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
reportProgress(onProgress, { phase: "pulling", message: `Updating ${repo} to the latest changes\u2026` });
|
|
170
|
-
const setUrl = await sh(sandbox, `git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(authUrl)}`);
|
|
171
|
-
if (setUrl.exitCode !== 0) {
|
|
172
|
-
throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, "pull-failed");
|
|
173
|
-
}
|
|
174
|
-
const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`);
|
|
175
|
-
if (pull.exitCode !== 0) {
|
|
176
|
-
throw classifyGitFailure(pull, "pull-failed");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} finally {
|
|
180
|
-
await scrubRemote(sandbox, workdir, repo, alreadyMaterialized);
|
|
181
|
-
}
|
|
182
|
-
reportProgress(onProgress, { phase: "finalizing", message: "Finalizing workspace\u2026" });
|
|
183
|
-
await storage.markMaterialized({ id: sandboxRow.id });
|
|
184
|
-
}
|
|
185
|
-
async function hasExistingCheckout(sandbox, workdir, repoFullName) {
|
|
186
|
-
const result = await sh(sandbox, `git -C ${shellQuote(workdir)} remote get-url origin`);
|
|
187
|
-
if (result.exitCode !== 0) return false;
|
|
188
|
-
const url = result.stdout.trim().toLowerCase();
|
|
189
|
-
const suffix = `github.com/${repoFullName.toLowerCase()}`;
|
|
190
|
-
return url.endsWith(`${suffix}.git`) || url.endsWith(suffix);
|
|
191
|
-
}
|
|
192
|
-
async function scrubRemote(sandbox, workdir, repoFullName, expectGitDir) {
|
|
193
|
-
const result = await sh(
|
|
194
|
-
sandbox,
|
|
195
|
-
`git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(cleanUrl(repoFullName))}`
|
|
196
|
-
);
|
|
197
|
-
if (result.exitCode !== 0 && expectGitDir) {
|
|
198
|
-
throw new MaterializeError(
|
|
199
|
-
`Failed to scrub installation token from git remote: ${result.stderr.trim() || result.stdout.trim()}`,
|
|
200
|
-
"pull-failed"
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
function classifyGitFailure(result, fallback) {
|
|
205
|
-
const stderr = result.stderr || "";
|
|
206
|
-
if (/could not resolve host|failed to connect|network is unreachable|Connection timed out/i.test(stderr)) {
|
|
207
|
-
return new MaterializeError(
|
|
208
|
-
"The sandbox could not reach github.com. The sandbox network must allow outbound egress to github.com.",
|
|
209
|
-
"egress-blocked"
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
const verb = fallback === "clone-failed" ? "clone" : fallback === "pull-failed" ? "pull" : "push";
|
|
213
|
-
return new MaterializeError(`git ${verb} failed: ${stderr}`, fallback);
|
|
214
|
-
}
|
|
215
|
-
function isValidGitRef(value) {
|
|
216
|
-
return typeof value === "string" && value.length > 0 && value.length <= 255 && // Reject leading-dash refs (e.g. `--mirror`) so the value can never be
|
|
217
|
-
// parsed as a git option when interpolated into a command.
|
|
218
|
-
!value.startsWith("-") && /^[A-Za-z0-9_./-]+$/.test(value);
|
|
219
|
-
}
|
|
220
|
-
function resolveGitIdentity(identity) {
|
|
221
|
-
const login = (identity.login || "").trim();
|
|
222
|
-
const name = (identity.name || "").trim() || login || "Mastra Code";
|
|
223
|
-
const email = (identity.email || "").trim() || (login ? `${login}@users.noreply.github.com` : "mastra-code@users.noreply.github.com");
|
|
224
|
-
return { name, email };
|
|
225
|
-
}
|
|
226
|
-
async function configureGitIdentity(sandbox, workdir, identity) {
|
|
227
|
-
const { name, email } = resolveGitIdentity(identity);
|
|
228
|
-
const setName = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.name ${shellQuote(name)}`);
|
|
229
|
-
if (setName.exitCode !== 0) {
|
|
230
|
-
throw new MaterializeError(`Failed to set git user.name: ${setName.stderr.trim()}`, "commit-failed");
|
|
231
|
-
}
|
|
232
|
-
const setEmail = await sh(sandbox, `git -C ${shellQuote(workdir)} config user.email ${shellQuote(email)}`);
|
|
233
|
-
if (setEmail.exitCode !== 0) {
|
|
234
|
-
throw new MaterializeError(`Failed to set git user.email: ${setEmail.stderr.trim()}`, "commit-failed");
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
async function withInstallToken(sandbox, workdir, repoFullName, token, fn) {
|
|
238
|
-
if (!/^[\w.-]+\/[\w.-]+$/.test(repoFullName)) {
|
|
239
|
-
throw new MaterializeError(`Refusing to push: invalid repo full name '${repoFullName}'.`, "push-failed");
|
|
240
|
-
}
|
|
241
|
-
const setUrl = await sh(
|
|
242
|
-
sandbox,
|
|
243
|
-
`git -C ${shellQuote(workdir)} remote set-url origin ${shellQuote(tokenUrl(repoFullName, token))}`
|
|
244
|
-
);
|
|
245
|
-
if (setUrl.exitCode !== 0) {
|
|
246
|
-
await scrubRemote(sandbox, workdir, repoFullName, false);
|
|
247
|
-
throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr.trim()}`, "push-failed");
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
return await fn();
|
|
251
|
-
} finally {
|
|
252
|
-
await scrubRemote(sandbox, workdir, repoFullName, true);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
async function pushBranch(sandbox, workdir, branch, token, repoFullName) {
|
|
256
|
-
if (!isValidGitRef(branch)) {
|
|
257
|
-
throw new MaterializeError(`Refusing to push: invalid branch name '${branch}'.`, "push-failed");
|
|
258
|
-
}
|
|
259
|
-
await withInstallToken(sandbox, workdir, repoFullName, token, async () => {
|
|
260
|
-
const push = await sh(sandbox, `git -C ${shellQuote(workdir)} push -u origin ${shellQuote(branch)}`);
|
|
261
|
-
if (push.exitCode !== 0) {
|
|
262
|
-
throw classifyGitFailure(push, "push-failed");
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
async function commitAll(sandbox, workdir, message, identity) {
|
|
267
|
-
await configureGitIdentity(sandbox, workdir, identity);
|
|
268
|
-
const add = await sh(sandbox, `git -C ${shellQuote(workdir)} add -A`);
|
|
269
|
-
if (add.exitCode !== 0) {
|
|
270
|
-
throw new MaterializeError(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`, "commit-failed");
|
|
271
|
-
}
|
|
272
|
-
const staged = await sh(sandbox, `git -C ${shellQuote(workdir)} diff --cached --quiet`);
|
|
273
|
-
if (staged.exitCode === 0) {
|
|
274
|
-
return { committed: false };
|
|
275
|
-
}
|
|
276
|
-
const commit = await sh(sandbox, `git -C ${shellQuote(workdir)} commit -m ${shellQuote(message)}`);
|
|
277
|
-
if (commit.exitCode !== 0) {
|
|
278
|
-
throw new MaterializeError(`git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`, "commit-failed");
|
|
279
|
-
}
|
|
280
|
-
return { committed: true };
|
|
281
|
-
}
|
|
282
|
-
var WorktreeError = class extends Error {
|
|
283
|
-
constructor(message, code) {
|
|
284
|
-
super(message);
|
|
285
|
-
this.code = code;
|
|
286
|
-
this.name = "WorktreeError";
|
|
287
|
-
}
|
|
288
|
-
code;
|
|
289
|
-
};
|
|
290
|
-
function safeBranchDir(branch) {
|
|
291
|
-
const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
|
|
292
|
-
if (sanitized === branch) return sanitized;
|
|
293
|
-
const hash = createHash("sha256").update(branch).digest("hex").slice(0, 8);
|
|
294
|
-
return `${sanitized}-${hash}`;
|
|
295
|
-
}
|
|
296
|
-
function computeWorktreePath(repoWorkdir, branch) {
|
|
297
|
-
const parent = repoWorkdir.replace(/\/+$/, "").split("/").slice(0, -1).join("/") || "";
|
|
298
|
-
return `${parent}/worktrees/${safeBranchDir(branch)}`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// src/integrations/github/subscriptions.ts
|
|
302
|
-
function changeRequestTargetKey(input) {
|
|
303
|
-
return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;
|
|
304
|
-
}
|
|
305
|
-
function sameSession(row, input) {
|
|
306
|
-
return row.orgId === input.orgId && row.sessionId === input.sessionId && row.resourceId === input.resourceId && row.threadId === input.threadId && (row.sessionScope ?? "") === (input.sessionScope ?? "");
|
|
307
|
-
}
|
|
308
|
-
async function subscribeToPullRequest(input, storage) {
|
|
309
|
-
const targetKey = changeRequestTargetKey(input);
|
|
310
|
-
const existing = (await storage.subscriptions.listByTarget(targetKey)).find((row) => sameSession(row, input));
|
|
311
|
-
if (existing) {
|
|
312
|
-
if (existing.status !== "open") await storage.subscriptions.updateStatus(existing.id, "open");
|
|
313
|
-
return { ...existing, status: "open" };
|
|
314
|
-
}
|
|
315
|
-
return storage.subscriptions.create({
|
|
316
|
-
orgId: input.orgId,
|
|
317
|
-
targetKey,
|
|
318
|
-
sessionId: input.sessionId,
|
|
319
|
-
resourceId: input.resourceId,
|
|
320
|
-
threadId: input.threadId,
|
|
321
|
-
sessionScope: input.sessionScope ?? "",
|
|
322
|
-
status: "open",
|
|
323
|
-
data: {
|
|
324
|
-
installationExternalId: input.installationExternalId,
|
|
325
|
-
projectRepositoryId: input.projectRepositoryId,
|
|
326
|
-
repositoryExternalId: input.repositoryExternalId,
|
|
327
|
-
repositorySlug: input.repositorySlug,
|
|
328
|
-
changeRequestId: input.changeRequestId,
|
|
329
|
-
ownerId: input.ownerId,
|
|
330
|
-
source: input.source,
|
|
331
|
-
subscribedByUserId: input.subscribedByUserId ?? null
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
async function unsubscribeFromPullRequest(input, storage) {
|
|
336
|
-
const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));
|
|
337
|
-
await Promise.all(rows.filter((row) => sameSession(row, input)).map((row) => storage.subscriptions.delete(row.id)));
|
|
338
|
-
}
|
|
339
|
-
async function listPullRequestSubscriptionsForThread(input, storage) {
|
|
340
|
-
const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);
|
|
341
|
-
return rows.filter(
|
|
342
|
-
(row) => row.orgId === input.orgId && row.resourceId === input.resourceId && row.threadId === input.threadId && (row.sessionScope ?? "") === (input.sessionScope ?? "")
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
async function listPullRequestSubscriptionsForWebhook(input, options, storage) {
|
|
346
|
-
const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));
|
|
347
|
-
return options?.includeTerminal ? rows : rows.filter((row) => row.status === "open");
|
|
348
|
-
}
|
|
349
|
-
function retirePullRequestSubscription(id, status, storage) {
|
|
350
|
-
return storage.subscriptions.updateStatus(id, status);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// src/integrations/github/webhook.ts
|
|
354
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
355
|
-
var SUPPORTED_GITHUB_WEBHOOK_EVENTS = /* @__PURE__ */ new Set([
|
|
356
|
-
"issues",
|
|
357
|
-
"issue_comment",
|
|
358
|
-
"pull_request",
|
|
359
|
-
"pull_request_review",
|
|
360
|
-
"pull_request_review_comment"
|
|
361
|
-
]);
|
|
362
|
-
function normalizeHeader(value) {
|
|
363
|
-
if (!value) return null;
|
|
364
|
-
const trimmed = value.trim();
|
|
365
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
366
|
-
}
|
|
367
|
-
function verifySignature(rawBody, signature, secret) {
|
|
368
|
-
if (!signature.startsWith("sha256=")) return false;
|
|
369
|
-
const signatureHex = signature.slice("sha256=".length);
|
|
370
|
-
if (!/^[a-fA-F0-9]{64}$/.test(signatureHex)) return false;
|
|
371
|
-
const expectedHex = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
372
|
-
const received = Buffer.from(signatureHex, "hex");
|
|
373
|
-
const expected = Buffer.from(expectedHex, "hex");
|
|
374
|
-
return received.length === expected.length && timingSafeEqual(received, expected);
|
|
375
|
-
}
|
|
376
|
-
async function parseGithubWebhook(c, secret) {
|
|
377
|
-
if (!secret) {
|
|
378
|
-
return { status: 401, body: { error: "unauthorized", message: "GitHub webhook secret is not configured" } };
|
|
379
|
-
}
|
|
380
|
-
const event = normalizeHeader(c.req.header("x-github-event"));
|
|
381
|
-
const deliveryId = normalizeHeader(c.req.header("x-github-delivery"));
|
|
382
|
-
const signature = normalizeHeader(c.req.header("x-hub-signature-256"));
|
|
383
|
-
if (!event) return { status: 400, body: { error: "bad_request", message: "Missing x-github-event header" } };
|
|
384
|
-
if (!deliveryId) return { status: 400, body: { error: "bad_request", message: "Missing x-github-delivery header" } };
|
|
385
|
-
if (!signature)
|
|
386
|
-
return { status: 401, body: { error: "unauthorized", message: "Missing x-hub-signature-256 header" } };
|
|
387
|
-
const rawBody = await c.req.text();
|
|
388
|
-
if (!verifySignature(rawBody, signature, secret)) {
|
|
389
|
-
return { status: 401, body: { error: "unauthorized", message: "Invalid GitHub webhook signature" } };
|
|
390
|
-
}
|
|
391
|
-
let payload;
|
|
392
|
-
try {
|
|
393
|
-
payload = JSON.parse(rawBody);
|
|
394
|
-
} catch {
|
|
395
|
-
return { status: 400, body: { error: "bad_request", message: "Malformed JSON payload" } };
|
|
396
|
-
}
|
|
397
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
398
|
-
return { status: 400, body: { error: "bad_request", message: "Payload must be a JSON object" } };
|
|
399
|
-
}
|
|
400
|
-
return { event, deliveryId, payload };
|
|
401
|
-
}
|
|
402
|
-
function getObject(value) {
|
|
403
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
404
|
-
}
|
|
405
|
-
function getString(value) {
|
|
406
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
407
|
-
}
|
|
408
|
-
function getNumber(value) {
|
|
409
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
410
|
-
}
|
|
411
|
-
function getBoolean(value) {
|
|
412
|
-
return typeof value === "boolean" ? value : void 0;
|
|
413
|
-
}
|
|
414
|
-
function getLabels(value) {
|
|
415
|
-
if (!Array.isArray(value)) return [];
|
|
416
|
-
return value.map((label) => typeof label === "string" ? label : getString(getObject(label)?.name)).filter((label) => Boolean(label));
|
|
417
|
-
}
|
|
418
|
-
function getIssueTriageRunInput(parsed) {
|
|
419
|
-
if (parsed.event !== "issues" || getString(parsed.payload.action) !== "opened") return null;
|
|
420
|
-
const repository = getString(getObject(parsed.payload.repository)?.full_name);
|
|
421
|
-
const issue = getObject(parsed.payload.issue);
|
|
422
|
-
const sender = getString(getObject(parsed.payload.sender)?.login);
|
|
423
|
-
const installationId = getNumber(getObject(parsed.payload.installation)?.id);
|
|
424
|
-
const issueNumber = getNumber(issue?.number);
|
|
425
|
-
const issueTitle = getString(issue?.title);
|
|
426
|
-
const issueUrl = getString(issue?.html_url);
|
|
427
|
-
if (!repository || !installationId || !issueNumber || !issueTitle || !issueUrl) return null;
|
|
428
|
-
return {
|
|
429
|
-
repository,
|
|
430
|
-
issueNumber,
|
|
431
|
-
issueTitle,
|
|
432
|
-
issueUrl,
|
|
433
|
-
labels: getLabels(issue?.labels),
|
|
434
|
-
sender,
|
|
435
|
-
installationId
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
function normalizeGithubWebhookMetadata(parsed) {
|
|
439
|
-
const { event, deliveryId, payload } = parsed;
|
|
440
|
-
const repository = getObject(payload.repository);
|
|
441
|
-
const issue = getObject(payload.issue);
|
|
442
|
-
const pullRequest = getObject(payload.pull_request);
|
|
443
|
-
const sender = getObject(payload.sender);
|
|
444
|
-
const installation = getObject(payload.installation);
|
|
445
|
-
const issuePullRequest = getObject(issue?.pull_request);
|
|
446
|
-
return {
|
|
447
|
-
event,
|
|
448
|
-
action: getString(payload.action),
|
|
449
|
-
deliveryId,
|
|
450
|
-
repository: getString(repository?.full_name),
|
|
451
|
-
repositoryId: getNumber(repository?.id),
|
|
452
|
-
issueNumber: getNumber(issue?.number),
|
|
453
|
-
pullRequestNumber: getNumber(pullRequest?.number) ?? (event === "issue_comment" && issuePullRequest ? getNumber(issue?.number) : void 0),
|
|
454
|
-
sender: getString(sender?.login),
|
|
455
|
-
senderType: getString(sender?.type),
|
|
456
|
-
installationId: getNumber(installation?.id)
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
function notificationSummary(metadata, label) {
|
|
460
|
-
const actor = metadata.sender ? `${metadata.sender} ` : "";
|
|
461
|
-
return `${actor}${label} on ${metadata.repository}#${metadata.pullRequestNumber}`;
|
|
462
|
-
}
|
|
463
|
-
function notificationTargetUrl(event, payload) {
|
|
464
|
-
if (event === "issue_comment" || event === "pull_request_review_comment") {
|
|
465
|
-
return getString(getObject(payload.comment)?.html_url);
|
|
466
|
-
}
|
|
467
|
-
if (event === "pull_request_review") {
|
|
468
|
-
return getString(getObject(payload.review)?.html_url);
|
|
469
|
-
}
|
|
470
|
-
return getString(getObject(payload.pull_request)?.html_url);
|
|
471
|
-
}
|
|
472
|
-
function classifyGithubWebhook(parsed) {
|
|
473
|
-
const metadata = normalizeGithubWebhookMetadata(parsed);
|
|
474
|
-
const { event, payload } = parsed;
|
|
475
|
-
const action = metadata.action;
|
|
476
|
-
if (!action || !metadata.repositoryId || !metadata.installationId || !metadata.pullRequestNumber || !metadata.repository) {
|
|
477
|
-
return void 0;
|
|
478
|
-
}
|
|
479
|
-
let priority;
|
|
480
|
-
let kind;
|
|
481
|
-
let label;
|
|
482
|
-
let terminal = false;
|
|
483
|
-
if (event === "pull_request_review" && action === "submitted") {
|
|
484
|
-
const state = getString(getObject(payload.review)?.state)?.toLowerCase().replaceAll("_", "-");
|
|
485
|
-
priority = state === "approved" || state === "changes-requested" ? "urgent" : "high";
|
|
486
|
-
kind = state === "approved" ? "review-approved" : state === "changes-requested" ? "review-changes-requested" : "review-submitted";
|
|
487
|
-
label = state === "approved" ? "approved the pull request" : state === "changes-requested" ? "requested changes" : "submitted a review";
|
|
488
|
-
} else if (event === "pull_request" && action === "closed") {
|
|
489
|
-
const merged = getBoolean(getObject(payload.pull_request)?.merged) === true;
|
|
490
|
-
priority = "urgent";
|
|
491
|
-
kind = merged ? "pull-request-merged" : "pull-request-closed";
|
|
492
|
-
label = merged ? "merged the pull request" : "closed the pull request";
|
|
493
|
-
terminal = true;
|
|
494
|
-
} else if (event === "issue_comment" && action === "created") {
|
|
495
|
-
priority = "high";
|
|
496
|
-
kind = "issue-comment-created";
|
|
497
|
-
label = "commented";
|
|
498
|
-
} else if (event === "pull_request_review_comment" && action === "created") {
|
|
499
|
-
priority = "high";
|
|
500
|
-
kind = "review-comment-created";
|
|
501
|
-
label = "left a review comment";
|
|
502
|
-
} else if (event === "pull_request" && action === "reopened") {
|
|
503
|
-
priority = "high";
|
|
504
|
-
kind = "pull-request-reopened";
|
|
505
|
-
label = "reopened the pull request";
|
|
506
|
-
} else if (event === "pull_request_review" && action === "dismissed") {
|
|
507
|
-
priority = "high";
|
|
508
|
-
kind = "review-dismissed";
|
|
509
|
-
label = "dismissed a review";
|
|
510
|
-
} else if (event === "pull_request" && [
|
|
511
|
-
"synchronize",
|
|
512
|
-
"ready_for_review",
|
|
513
|
-
"converted_to_draft",
|
|
514
|
-
"assigned",
|
|
515
|
-
"unassigned",
|
|
516
|
-
"review_requested",
|
|
517
|
-
"review_request_removed"
|
|
518
|
-
].includes(action)) {
|
|
519
|
-
priority = "medium";
|
|
520
|
-
kind = `pull-request-${action.replaceAll("_", "-")}`;
|
|
521
|
-
label = action.replaceAll("_", " ");
|
|
522
|
-
} else if (event === "pull_request" && ["edited", "labeled", "unlabeled", "milestoned", "demilestoned"].includes(action)) {
|
|
523
|
-
priority = "low";
|
|
524
|
-
kind = `pull-request-${action.replaceAll("_", "-")}`;
|
|
525
|
-
label = action.replaceAll("_", " ");
|
|
526
|
-
} else {
|
|
527
|
-
return void 0;
|
|
528
|
-
}
|
|
529
|
-
return {
|
|
530
|
-
action,
|
|
531
|
-
kind,
|
|
532
|
-
priority,
|
|
533
|
-
summary: notificationSummary(metadata, label),
|
|
534
|
-
terminal,
|
|
535
|
-
metadata: {
|
|
536
|
-
...metadata,
|
|
537
|
-
pullRequestNumber: metadata.pullRequestNumber,
|
|
538
|
-
repositoryId: metadata.repositoryId,
|
|
539
|
-
installationId: metadata.installationId
|
|
540
|
-
},
|
|
541
|
-
payload
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
async function resolveSubscriptionSession(controller, subscription) {
|
|
545
|
-
const { sessionId, resourceId, threadId } = subscription;
|
|
546
|
-
if (!sessionId || !resourceId || !threadId) {
|
|
547
|
-
throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);
|
|
548
|
-
}
|
|
549
|
-
const scope = subscription.sessionScope || void 0;
|
|
550
|
-
let session = await controller.getSessionByResource(resourceId, scope);
|
|
551
|
-
if (!session) {
|
|
552
|
-
const tags = {
|
|
553
|
-
factoryProjectId: resourceId,
|
|
554
|
-
projectRepositoryId: subscription.data.projectRepositoryId,
|
|
555
|
-
...scope ? { worktreePath: scope } : {}
|
|
556
|
-
};
|
|
557
|
-
session = await controller.createSession({
|
|
558
|
-
id: sessionId,
|
|
559
|
-
ownerId: subscription.data.ownerId,
|
|
560
|
-
resourceId,
|
|
561
|
-
scope,
|
|
562
|
-
tags
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
if (session.thread.getId() !== threadId) {
|
|
566
|
-
await session.thread.switch({ threadId, emitEvent: false });
|
|
567
|
-
}
|
|
568
|
-
if (session.thread.getId() !== threadId) {
|
|
569
|
-
throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
|
|
570
|
-
}
|
|
571
|
-
return session;
|
|
572
|
-
}
|
|
573
|
-
var AUTHORIZED_BOTS = /* @__PURE__ */ new Set(["coderabbitai[bot]", "devin-ai-integration[bot]"]);
|
|
574
|
-
var AUTHORIZED_PERMISSIONS = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
|
|
575
|
-
var PERMISSION_CHECK_TIMEOUT_MS = 5e3;
|
|
576
|
-
var AUTHOR_GATED_KINDS = /* @__PURE__ */ new Set([
|
|
577
|
-
"issue-comment-created",
|
|
578
|
-
"review-comment-created",
|
|
579
|
-
"review-submitted",
|
|
580
|
-
"review-approved",
|
|
581
|
-
"review-changes-requested",
|
|
582
|
-
"review-dismissed"
|
|
583
|
-
]);
|
|
584
|
-
async function isAuthorizedGithubSender(notification, github) {
|
|
585
|
-
if (!AUTHOR_GATED_KINDS.has(notification.kind)) return true;
|
|
586
|
-
const sender = notification.metadata.sender;
|
|
587
|
-
const repository = notification.metadata.repository;
|
|
588
|
-
if (!sender || !repository) return false;
|
|
589
|
-
const normalizedSender = sender.toLowerCase();
|
|
590
|
-
if (notification.metadata.senderType?.toLowerCase() === "bot" || normalizedSender.endsWith("[bot]")) {
|
|
591
|
-
return AUTHORIZED_BOTS.has(normalizedSender);
|
|
592
|
-
}
|
|
593
|
-
if (!github) return false;
|
|
594
|
-
const abortController = new AbortController();
|
|
595
|
-
let timeout;
|
|
596
|
-
try {
|
|
597
|
-
const permission = await Promise.race([
|
|
598
|
-
github.getRepositoryCollaboratorPermission(
|
|
599
|
-
notification.metadata.installationId,
|
|
600
|
-
repository,
|
|
601
|
-
sender,
|
|
602
|
-
abortController.signal
|
|
603
|
-
),
|
|
604
|
-
new Promise((resolve) => {
|
|
605
|
-
timeout = setTimeout(() => {
|
|
606
|
-
abortController.abort();
|
|
607
|
-
resolve(void 0);
|
|
608
|
-
}, PERMISSION_CHECK_TIMEOUT_MS);
|
|
609
|
-
})
|
|
610
|
-
]);
|
|
611
|
-
return permission !== void 0 && AUTHORIZED_PERMISSIONS.has(permission);
|
|
612
|
-
} catch {
|
|
613
|
-
return false;
|
|
614
|
-
} finally {
|
|
615
|
-
if (timeout) clearTimeout(timeout);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
async function dispatchGithubWebhook(parsed, dependencies) {
|
|
619
|
-
const notification = classifyGithubWebhook(parsed);
|
|
620
|
-
if (!notification) return { delivered: 0, failed: 0, ignored: true };
|
|
621
|
-
const isAuthorizedSender = dependencies.isAuthorizedSender ?? ((n) => isAuthorizedGithubSender(n, dependencies.github));
|
|
622
|
-
if (!await isAuthorizedSender(notification)) {
|
|
623
|
-
return { delivered: 0, failed: 0, ignored: true };
|
|
624
|
-
}
|
|
625
|
-
const target = {
|
|
626
|
-
installationExternalId: notification.metadata.installationId.toString(),
|
|
627
|
-
repositoryExternalId: notification.metadata.repositoryId.toString(),
|
|
628
|
-
changeRequestId: notification.metadata.pullRequestNumber.toString()
|
|
629
|
-
};
|
|
630
|
-
const listSubscriptions = dependencies.listSubscriptions ?? ((subscriptionTarget, options) => {
|
|
631
|
-
if (!dependencies.github) throw new Error("GitHub integration is required to load webhook subscriptions.");
|
|
632
|
-
return listPullRequestSubscriptionsForWebhook(
|
|
633
|
-
subscriptionTarget,
|
|
634
|
-
options,
|
|
635
|
-
dependencies.github.integrationStorage
|
|
636
|
-
);
|
|
637
|
-
});
|
|
638
|
-
const retireSubscription = dependencies.retireSubscription ?? ((id, status) => {
|
|
639
|
-
if (!dependencies.github) throw new Error("GitHub integration is required to retire webhook subscriptions.");
|
|
640
|
-
return retirePullRequestSubscription(id, status, dependencies.github.integrationStorage);
|
|
641
|
-
});
|
|
642
|
-
const subscriptions = await listSubscriptions(target, { includeTerminal: notification.action === "reopened" });
|
|
643
|
-
let delivered = 0;
|
|
644
|
-
let failed = 0;
|
|
645
|
-
for (const subscription of subscriptions) {
|
|
646
|
-
try {
|
|
647
|
-
const session = await resolveSubscriptionSession(dependencies.controller, subscription);
|
|
648
|
-
const result = await session.sendNotificationSignal({
|
|
649
|
-
source: "github",
|
|
650
|
-
kind: notification.kind,
|
|
651
|
-
summary: notification.summary,
|
|
652
|
-
priority: notification.priority,
|
|
653
|
-
payload: notification.payload,
|
|
654
|
-
sourceId: parsed.deliveryId,
|
|
655
|
-
dedupeKey: `${parsed.deliveryId}:${subscription.sessionId}:${subscription.threadId}`,
|
|
656
|
-
coalesceKey: `github:${subscription.data.repositoryExternalId}:pull-request:${subscription.data.changeRequestId}`,
|
|
657
|
-
metadata: {
|
|
658
|
-
event: notification.metadata.event,
|
|
659
|
-
action: notification.action,
|
|
660
|
-
repository: notification.metadata.repository,
|
|
661
|
-
issueNumber: notification.metadata.issueNumber,
|
|
662
|
-
pullRequestNumber: notification.metadata.pullRequestNumber,
|
|
663
|
-
targetUrl: notificationTargetUrl(parsed.event, parsed.payload),
|
|
664
|
-
deliveryId: parsed.deliveryId
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
await Promise.all([result.persisted, result.accepted].filter(Boolean));
|
|
668
|
-
if (notification.terminal) {
|
|
669
|
-
await retireSubscription(subscription.id, notification.kind === "pull-request-merged" ? "merged" : "closed");
|
|
670
|
-
} else if (notification.action === "reopened") {
|
|
671
|
-
await retireSubscription(subscription.id, "open");
|
|
672
|
-
}
|
|
673
|
-
delivered += 1;
|
|
674
|
-
} catch (error) {
|
|
675
|
-
failed += 1;
|
|
676
|
-
dependencies.onTargetError?.(subscription, error);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return { delivered, failed, ignored: false };
|
|
680
|
-
}
|
|
681
|
-
async function handleGithubWebhook(c, options) {
|
|
682
|
-
const parsed = await parseGithubWebhook(c, options.github.webhookSecret);
|
|
683
|
-
if ("status" in parsed) return parsed;
|
|
684
|
-
if (!SUPPORTED_GITHUB_WEBHOOK_EVENTS.has(parsed.event)) {
|
|
685
|
-
return { status: 202, body: { ok: true, ignored: true } };
|
|
686
|
-
}
|
|
687
|
-
const metadata = normalizeGithubWebhookMetadata(parsed);
|
|
688
|
-
console.info("[GitHub Webhook]", metadata);
|
|
689
|
-
if (options.ingestFactoryEvent) {
|
|
690
|
-
await options.ingestFactoryEvent(parsed);
|
|
691
|
-
} else {
|
|
692
|
-
const issueTriageRun = getIssueTriageRunInput(parsed);
|
|
693
|
-
if (issueTriageRun && options.runIssueTriage) {
|
|
694
|
-
void options.runIssueTriage(issueTriageRun).catch((error) => {
|
|
695
|
-
console.error("[GitHub Webhook] Failed to run issue triage", {
|
|
696
|
-
deliveryId: metadata.deliveryId,
|
|
697
|
-
repository: metadata.repository,
|
|
698
|
-
issueNumber: metadata.issueNumber,
|
|
699
|
-
error: error instanceof Error ? error.message : String(error)
|
|
700
|
-
});
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
if (!options.controller) {
|
|
705
|
-
return { status: 202, body: { ok: true } };
|
|
706
|
-
}
|
|
707
|
-
const result = await dispatchGithubWebhook(parsed, options);
|
|
708
|
-
if (result.failed > 0) {
|
|
709
|
-
console.warn(`[GitHub Webhook] ${result.failed} subscribed target(s) failed for delivery ${parsed.deliveryId}.`);
|
|
710
|
-
}
|
|
711
|
-
return { status: 202, body: { ok: true, ...result.ignored ? { ignored: true } : {} } };
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// src/integrations/github/routes.ts
|
|
715
|
-
var sessionOperationLocks = /* @__PURE__ */ new Map();
|
|
716
|
-
function withSessionOperationLock(sessionId, fn) {
|
|
717
|
-
const previous = sessionOperationLocks.get(sessionId) ?? Promise.resolve();
|
|
718
|
-
const next = previous.then(fn, fn);
|
|
719
|
-
const tail = next.then(
|
|
720
|
-
() => void 0,
|
|
721
|
-
() => void 0
|
|
722
|
-
);
|
|
723
|
-
sessionOperationLocks.set(sessionId, tail);
|
|
724
|
-
void tail.then(() => {
|
|
725
|
-
if (sessionOperationLocks.get(sessionId) === tail) sessionOperationLocks.delete(sessionId);
|
|
726
|
-
});
|
|
727
|
-
return next;
|
|
728
|
-
}
|
|
729
42
|
function loose(c) {
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
async function resolveFactoryDefaultModelId(projects, factoryProjectId) {
|
|
733
|
-
if (!projects || !factoryProjectId) return void 0;
|
|
734
|
-
try {
|
|
735
|
-
const project = await projects.getById({ id: factoryProjectId });
|
|
736
|
-
return project?.defaultModelId ?? void 0;
|
|
737
|
-
} catch {
|
|
738
|
-
return void 0;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
function pullRequestNumberFromUrl(value, expectedRepo) {
|
|
742
|
-
try {
|
|
743
|
-
const url = new URL(value);
|
|
744
|
-
const match = url.pathname.match(/^\/([^/]+\/[^/]+)\/pull\/(\d+)\/?$/);
|
|
745
|
-
if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) {
|
|
746
|
-
return void 0;
|
|
747
|
-
}
|
|
748
|
-
const number2 = Number(match[2]);
|
|
749
|
-
return Number.isInteger(number2) && number2 > 0 ? number2 : void 0;
|
|
750
|
-
} catch {
|
|
751
|
-
return void 0;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
function isCanonicalGithubIssueUrl(value, repoFullName, issueNumber) {
|
|
755
|
-
try {
|
|
756
|
-
const url = new URL(value);
|
|
757
|
-
const [owner, repo] = repoFullName.split("/");
|
|
758
|
-
return url.protocol === "https:" && url.hostname === "github.com" && url.pathname === `/${owner}/${repo}/issues/${issueNumber}` && url.search === "" && url.hash === "";
|
|
759
|
-
} catch {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
function isValidGitRef2(value) {
|
|
764
|
-
return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9_./-]+$/.test(value);
|
|
765
|
-
}
|
|
766
|
-
async function resolveOrgTenant(c, auth) {
|
|
767
|
-
await auth.ensureUser(c);
|
|
768
|
-
const tenant = auth.tenant(c);
|
|
769
|
-
if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
|
|
770
|
-
if (!tenant.orgId) {
|
|
771
|
-
return {
|
|
772
|
-
response: c.json(
|
|
773
|
-
{
|
|
774
|
-
error: "organization_required",
|
|
775
|
-
message: "GitHub projects require a WorkOS organization. Personal accounts cannot connect repositories."
|
|
776
|
-
},
|
|
777
|
-
403
|
|
778
|
-
)
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };
|
|
782
|
-
}
|
|
783
|
-
function parseListPage(raw) {
|
|
784
|
-
if (raw === void 0) return 1;
|
|
785
|
-
if (!/^\d{1,5}$/.test(raw)) return null;
|
|
786
|
-
const page = Number(raw);
|
|
787
|
-
return page >= 1 ? page : null;
|
|
788
|
-
}
|
|
789
|
-
var VALID_ISSUE_LABEL_FILTERS = /* @__PURE__ */ new Set(["auto-triaged", "needs-approval"]);
|
|
790
|
-
function parseIssueLabelFilter(raw) {
|
|
791
|
-
if (raw === void 0 || raw === "") return void 0;
|
|
792
|
-
if (VALID_ISSUE_LABEL_FILTERS.has(raw)) return raw;
|
|
793
|
-
return null;
|
|
794
|
-
}
|
|
795
|
-
function parseIssueNumberParam(raw) {
|
|
796
|
-
if (!raw || !/^\d{1,10}$/.test(raw)) return null;
|
|
797
|
-
const issueNumber = Number(raw);
|
|
798
|
-
return Number.isSafeInteger(issueNumber) && issueNumber > 0 ? issueNumber : null;
|
|
799
|
-
}
|
|
800
|
-
function parseStringList(value) {
|
|
801
|
-
if (!Array.isArray(value)) return [];
|
|
802
|
-
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
803
|
-
}
|
|
804
|
-
async function resolveProjectRepository(args) {
|
|
805
|
-
const projectRepository = await args.github.sourceControlStorage.projectRepositories.get({
|
|
806
|
-
orgId: args.orgId,
|
|
807
|
-
id: args.projectRepositoryId
|
|
808
|
-
});
|
|
809
|
-
if (!projectRepository) return null;
|
|
810
|
-
const connection = await args.github.sourceControlStorage.connections.get({
|
|
811
|
-
orgId: args.orgId,
|
|
812
|
-
id: projectRepository.connectionId
|
|
813
|
-
});
|
|
814
|
-
if (!connection) return null;
|
|
815
|
-
const repository = await args.github.sourceControlStorage.repositories.get({
|
|
816
|
-
orgId: args.orgId,
|
|
817
|
-
id: projectRepository.repositoryId
|
|
818
|
-
});
|
|
819
|
-
if (!repository) return null;
|
|
820
|
-
const installation = await args.github.sourceControlStorage.installations.get({
|
|
821
|
-
orgId: args.orgId,
|
|
822
|
-
id: connection.installationId
|
|
823
|
-
});
|
|
824
|
-
if (!installation) return null;
|
|
825
|
-
return {
|
|
826
|
-
...projectRepository,
|
|
827
|
-
connection,
|
|
828
|
-
installation,
|
|
829
|
-
repository,
|
|
830
|
-
factoryProjectId: connection.factoryProjectId,
|
|
831
|
-
defaultBranch: projectRepository.branch ?? repository.defaultBranch
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
function polledIssueEvent(project, issue) {
|
|
835
|
-
const repositoryId = Number(project.repository.externalId);
|
|
836
|
-
return {
|
|
837
|
-
event: "issues",
|
|
838
|
-
deliveryId: `poll:${repositoryId}:issue:${issue.number}:${issue.createdAt}`,
|
|
839
|
-
payload: {
|
|
840
|
-
action: "opened",
|
|
841
|
-
installation: { id: Number(project.installation.externalId) },
|
|
842
|
-
repository: { id: repositoryId, full_name: project.repository.slug },
|
|
843
|
-
sender: { login: issue.author ?? "__unknown__" },
|
|
844
|
-
issue: {
|
|
845
|
-
number: issue.number,
|
|
846
|
-
title: issue.title,
|
|
847
|
-
html_url: issue.url,
|
|
848
|
-
created_at: issue.createdAt,
|
|
849
|
-
labels: issue.labels.map((name) => ({ name }))
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
function polledPullRequestEvent(project, pullRequest) {
|
|
855
|
-
const repositoryId = Number(project.repository.externalId);
|
|
856
|
-
return {
|
|
857
|
-
event: "pull_request",
|
|
858
|
-
deliveryId: `poll:${repositoryId}:pull-request:${pullRequest.number}:${pullRequest.createdAt}`,
|
|
859
|
-
payload: {
|
|
860
|
-
action: "opened",
|
|
861
|
-
installation: { id: Number(project.installation.externalId) },
|
|
862
|
-
repository: { id: repositoryId, full_name: project.repository.slug },
|
|
863
|
-
sender: { login: pullRequest.author ?? "__unknown__" },
|
|
864
|
-
pull_request: {
|
|
865
|
-
number: pullRequest.number,
|
|
866
|
-
title: pullRequest.title,
|
|
867
|
-
html_url: pullRequest.url,
|
|
868
|
-
created_at: pullRequest.createdAt,
|
|
869
|
-
state: "open",
|
|
870
|
-
merged: false,
|
|
871
|
-
head: { ref: pullRequest.headBranch },
|
|
872
|
-
base: { ref: pullRequest.baseBranch }
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
async function ingestPolledEvents(events, ingestFactoryEvent) {
|
|
878
|
-
if (!ingestFactoryEvent) return;
|
|
879
|
-
const results = await Promise.allSettled(events.map((event) => ingestFactoryEvent(event)));
|
|
880
|
-
const rejected = results.find((result) => result.status === "rejected");
|
|
881
|
-
if (rejected) throw rejected.reason;
|
|
882
|
-
}
|
|
883
|
-
function buildGithubRoutes(options) {
|
|
884
|
-
const routes = [];
|
|
885
|
-
const { auth, fleet, storage, github, stateSigner, emitAudit } = options;
|
|
886
|
-
const diagnostics = () => getGithubFeatureDiagnostics({ github, auth, appDbConfigured: storage !== void 0, stateSigner, fleet });
|
|
887
|
-
routes.push(
|
|
888
|
-
registerApiRoute("/web/github/status", {
|
|
889
|
-
method: "GET",
|
|
890
|
-
requiresAuth: false,
|
|
891
|
-
handler: async (c) => {
|
|
892
|
-
if (!isGithubFeatureEnabled({ github, auth }) || !github || !stateSigner) {
|
|
893
|
-
return c.json({
|
|
894
|
-
enabled: false,
|
|
895
|
-
connected: false,
|
|
896
|
-
installations: [],
|
|
897
|
-
reason: "missing_config",
|
|
898
|
-
diagnostics: diagnostics()
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
await auth.ensureUser(loose(c));
|
|
902
|
-
const tenant = auth.tenant(loose(c));
|
|
903
|
-
if (!tenant) return c.json({ error: "unauthorized", reason: "auth_required" }, 401);
|
|
904
|
-
if (!tenant.orgId) {
|
|
905
|
-
return c.json({
|
|
906
|
-
enabled: true,
|
|
907
|
-
sandboxEnabled: fleet.enabled,
|
|
908
|
-
organizationRequired: true,
|
|
909
|
-
connected: false,
|
|
910
|
-
installations: [],
|
|
911
|
-
reason: "organization_required",
|
|
912
|
-
diagnostics: diagnostics()
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
const rows = options.github ? await options.github.sourceControlStorage.installations.list({ orgId: tenant.orgId }) : [];
|
|
916
|
-
const connected = rows.length > 0;
|
|
917
|
-
return c.json({
|
|
918
|
-
enabled: true,
|
|
919
|
-
sandboxEnabled: fleet.enabled,
|
|
920
|
-
connected,
|
|
921
|
-
installations: rows.map((r) => ({
|
|
922
|
-
installationId: Number(r.externalId),
|
|
923
|
-
accountLogin: r.accountName,
|
|
924
|
-
accountType: r.accountType
|
|
925
|
-
})),
|
|
926
|
-
reason: connected ? "ready" : "not_connected",
|
|
927
|
-
diagnostics: diagnostics()
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
})
|
|
931
|
-
);
|
|
932
|
-
if (!isGithubFeatureEnabled({ github, auth }) || !github || !stateSigner) {
|
|
933
|
-
return routes;
|
|
934
|
-
}
|
|
935
|
-
const signState = (orgId, userId) => stateSigner.sign(orgId, userId);
|
|
936
|
-
const verifyState = (state) => stateSigner.verify(state);
|
|
937
|
-
const { runIssueTriage } = options;
|
|
938
|
-
const runBoardIssueTriage = runIssueTriage ? async (input) => {
|
|
939
|
-
if (!input.resourceId || !input.projectPath) {
|
|
940
|
-
throw new Error("GitHub issue triage requires an explicit Factory project repository");
|
|
941
|
-
}
|
|
942
|
-
await github.addIssueLabels(input.installationId, input.repository, input.issueNumber, ["auto-triaged"]);
|
|
943
|
-
return runIssueTriage({
|
|
944
|
-
...input,
|
|
945
|
-
defaultModelId: input.defaultModelId ?? await resolveFactoryDefaultModelId(options.projects, input.resourceId),
|
|
946
|
-
labels: input.labels.includes("auto-triaged") ? input.labels : [...input.labels, "auto-triaged"]
|
|
947
|
-
});
|
|
948
|
-
} : void 0;
|
|
949
|
-
routes.push(
|
|
950
|
-
registerApiRoute("/web/github/subscriptions", {
|
|
951
|
-
method: "GET",
|
|
952
|
-
handler: async (c) => {
|
|
953
|
-
await auth.ensureUser(loose(c));
|
|
954
|
-
const tenant = auth.tenant(loose(c));
|
|
955
|
-
if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
|
|
956
|
-
const resourceId = c.req.query("resourceId");
|
|
957
|
-
const threadId = c.req.query("threadId");
|
|
958
|
-
const sessionScope = c.req.query("scope");
|
|
959
|
-
if (!resourceId || !threadId) return c.json({ error: "resourceId and threadId are required" }, 400);
|
|
960
|
-
const subscriptions = await listPullRequestSubscriptionsForThread(
|
|
961
|
-
{
|
|
962
|
-
orgId: tenant.orgId,
|
|
963
|
-
resourceId,
|
|
964
|
-
threadId,
|
|
965
|
-
sessionScope
|
|
966
|
-
},
|
|
967
|
-
github.integrationStorage
|
|
968
|
-
);
|
|
969
|
-
return c.json({
|
|
970
|
-
subscriptions: subscriptions.map((subscription) => ({
|
|
971
|
-
id: subscription.id,
|
|
972
|
-
repoFullName: subscription.data.repositorySlug,
|
|
973
|
-
pullRequestNumber: Number(subscription.data.changeRequestId),
|
|
974
|
-
status: subscription.status,
|
|
975
|
-
url: `https://github.com/${subscription.data.repositorySlug}/pull/${subscription.data.changeRequestId}`
|
|
976
|
-
}))
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
}),
|
|
980
|
-
registerApiRoute("/web/github/webhook", {
|
|
981
|
-
method: "POST",
|
|
982
|
-
requiresAuth: false,
|
|
983
|
-
handler: async (c) => {
|
|
984
|
-
const result = await handleGithubWebhook(loose(c), {
|
|
985
|
-
github,
|
|
986
|
-
runIssueTriage: runBoardIssueTriage,
|
|
987
|
-
ingestFactoryEvent: options.ingestFactoryEvent,
|
|
988
|
-
...options.controller ? {
|
|
989
|
-
controller: options.controller,
|
|
990
|
-
onTargetError: (subscription, error) => {
|
|
991
|
-
console.warn(
|
|
992
|
-
`[GitHub Webhook] Delivery failed for subscription ${subscription.id} (${subscription.resourceId}/${subscription.threadId}).`,
|
|
993
|
-
error
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
} : {}
|
|
997
|
-
});
|
|
998
|
-
return c.json(result.body, result.status);
|
|
999
|
-
}
|
|
1000
|
-
})
|
|
1001
|
-
);
|
|
1002
|
-
const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? "").replace(/\/$/, "")}/auth/github/callback`;
|
|
1003
|
-
routes.push(
|
|
1004
|
-
registerApiRoute("/auth/github/connect", {
|
|
1005
|
-
method: "GET",
|
|
1006
|
-
requiresAuth: false,
|
|
1007
|
-
handler: async (c) => {
|
|
1008
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1009
|
-
if ("response" in resolved) return resolved.response;
|
|
1010
|
-
const state = signState(resolved.tenant.orgId, resolved.tenant.userId);
|
|
1011
|
-
if (c.req.query("manage")) return c.redirect(github.buildInstallUrl(state));
|
|
1012
|
-
return c.redirect(github.buildOAuthIdentifyUrl(state, redirectUri));
|
|
1013
|
-
}
|
|
1014
|
-
})
|
|
1015
|
-
);
|
|
1016
|
-
routes.push(
|
|
1017
|
-
registerApiRoute("/auth/github/callback", {
|
|
1018
|
-
method: "GET",
|
|
1019
|
-
requiresAuth: false,
|
|
1020
|
-
handler: async (c) => {
|
|
1021
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1022
|
-
if ("response" in resolved) return resolved.response;
|
|
1023
|
-
const { orgId, userId } = resolved.tenant;
|
|
1024
|
-
const state = c.req.query("state");
|
|
1025
|
-
if (!state) {
|
|
1026
|
-
return c.redirect(github.buildOAuthIdentifyUrl(signState(orgId, userId), redirectUri));
|
|
1027
|
-
}
|
|
1028
|
-
const stateTenant = verifyState(state);
|
|
1029
|
-
if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {
|
|
1030
|
-
console.warn(
|
|
1031
|
-
"[GitHub] Install callback rejected: state/tenant mismatch.",
|
|
1032
|
-
JSON.stringify({
|
|
1033
|
-
stateValid: Boolean(stateTenant),
|
|
1034
|
-
stateOrgId: stateTenant?.orgId,
|
|
1035
|
-
stateUserId: stateTenant?.userId,
|
|
1036
|
-
sessionOrgId: orgId,
|
|
1037
|
-
sessionUserId: userId
|
|
1038
|
-
})
|
|
1039
|
-
);
|
|
1040
|
-
return c.redirect("/?github=error");
|
|
1041
|
-
}
|
|
1042
|
-
const code = c.req.query("code");
|
|
1043
|
-
if (!code) {
|
|
1044
|
-
return c.redirect(github.buildOAuthIdentifyUrl(signState(orgId, userId), redirectUri));
|
|
1045
|
-
}
|
|
1046
|
-
try {
|
|
1047
|
-
const userToken = await github.exchangeOAuthCode(code, redirectUri);
|
|
1048
|
-
const installations = await github.listUserInstallations(userToken);
|
|
1049
|
-
if (installations.length === 0) {
|
|
1050
|
-
return c.redirect(github.buildInstallUrl(signState(orgId, userId)));
|
|
1051
|
-
}
|
|
1052
|
-
for (const inst of installations) {
|
|
1053
|
-
await github.sourceControlStorage.installations.upsert({
|
|
1054
|
-
orgId,
|
|
1055
|
-
connectedByUserId: userId,
|
|
1056
|
-
externalId: inst.installationId.toString(),
|
|
1057
|
-
accountName: inst.accountLogin,
|
|
1058
|
-
accountType: inst.accountType
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
} catch (error) {
|
|
1062
|
-
console.warn(
|
|
1063
|
-
`[GitHub] Install callback failed to persist installations for org ${orgId} / user ${userId}.`,
|
|
1064
|
-
error
|
|
1065
|
-
);
|
|
1066
|
-
return c.redirect("/?github=error");
|
|
1067
|
-
}
|
|
1068
|
-
return c.redirect("/?github=connected");
|
|
1069
|
-
}
|
|
1070
|
-
})
|
|
1071
|
-
);
|
|
1072
|
-
routes.push(
|
|
1073
|
-
registerApiRoute("/web/github/repos", {
|
|
1074
|
-
method: "GET",
|
|
1075
|
-
requiresAuth: false,
|
|
1076
|
-
handler: async (c) => {
|
|
1077
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1078
|
-
if ("response" in resolved) return resolved.response;
|
|
1079
|
-
const installs = await github.sourceControlStorage.installations.list({ orgId: resolved.tenant.orgId });
|
|
1080
|
-
const query = (c.req.query("q") ?? "").toLowerCase();
|
|
1081
|
-
const repos = [];
|
|
1082
|
-
const seenRepositoryIds = /* @__PURE__ */ new Set();
|
|
1083
|
-
for (const inst of installs) {
|
|
1084
|
-
let list;
|
|
1085
|
-
try {
|
|
1086
|
-
list = await github.listInstallationRepos(Number(inst.externalId));
|
|
1087
|
-
} catch (err) {
|
|
1088
|
-
if (err.status !== 404) throw err;
|
|
1089
|
-
console.error(`[Mastra Factory] pruning stale GitHub installation ${inst.externalId} (404 from GitHub)`);
|
|
1090
|
-
await github.sourceControlStorage.installations.delete({ orgId: resolved.tenant.orgId, id: inst.id });
|
|
1091
|
-
continue;
|
|
1092
|
-
}
|
|
1093
|
-
for (const repo of list) {
|
|
1094
|
-
if (query && !repo.fullName.toLowerCase().includes(query)) continue;
|
|
1095
|
-
if (seenRepositoryIds.has(repo.id)) continue;
|
|
1096
|
-
seenRepositoryIds.add(repo.id);
|
|
1097
|
-
const repository = await github.sourceControlStorage.repositories.upsert({
|
|
1098
|
-
orgId: resolved.tenant.orgId,
|
|
1099
|
-
input: {
|
|
1100
|
-
installationId: inst.id,
|
|
1101
|
-
externalId: repo.id.toString(),
|
|
1102
|
-
slug: repo.fullName,
|
|
1103
|
-
defaultBranch: isValidGitRef2(repo.defaultBranch) ? repo.defaultBranch : "main",
|
|
1104
|
-
providerMetadata: { private: repo.private, owner: repo.owner }
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
repos.push({
|
|
1108
|
-
...repo,
|
|
1109
|
-
installationStorageId: inst.id,
|
|
1110
|
-
repositoryStorageId: repository.id,
|
|
1111
|
-
sandboxProvider: fleet.provider,
|
|
1112
|
-
sandboxWorkdir: fleet.computeWorkdir(repo.fullName)
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
return c.json({ repos });
|
|
1117
|
-
}
|
|
1118
|
-
})
|
|
1119
|
-
);
|
|
1120
|
-
routes.push(
|
|
1121
|
-
registerApiRoute("/web/github/projects/:id/ensure", {
|
|
1122
|
-
method: "POST",
|
|
1123
|
-
requiresAuth: false,
|
|
1124
|
-
handler: async (c) => {
|
|
1125
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1126
|
-
if ("response" in resolved) return resolved.response;
|
|
1127
|
-
const { orgId, userId } = resolved.tenant;
|
|
1128
|
-
if (!fleet.enabled) {
|
|
1129
|
-
return c.json({ error: "sandbox_not_configured", message: "No sandbox provider is configured." }, 503);
|
|
1130
|
-
}
|
|
1131
|
-
const projectRepositoryId = c.req.param("id");
|
|
1132
|
-
if (!projectRepositoryId) return c.json({ error: "Project repository not found" }, 404);
|
|
1133
|
-
const project = await resolveProjectRepository({ github, orgId, projectRepositoryId });
|
|
1134
|
-
if (!project) {
|
|
1135
|
-
return c.json({ error: "Project repository not found" }, 404);
|
|
1136
|
-
}
|
|
1137
|
-
const wantsStream = (c.req.header("accept") ?? "").includes("text/event-stream");
|
|
1138
|
-
if (wantsStream) {
|
|
1139
|
-
return streamSSE(loose(c), async (stream) => {
|
|
1140
|
-
try {
|
|
1141
|
-
const result = await prepareProject({
|
|
1142
|
-
github,
|
|
1143
|
-
fleet,
|
|
1144
|
-
project,
|
|
1145
|
-
userId,
|
|
1146
|
-
onProgress: (ev) => void stream.writeSSE({ event: "progress", data: JSON.stringify(ev) })
|
|
1147
|
-
});
|
|
1148
|
-
await stream.writeSSE({ event: "done", data: JSON.stringify(result) });
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify(ensureErrorPayload(err).body) });
|
|
1151
|
-
}
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1154
|
-
try {
|
|
1155
|
-
const result = await prepareProject({ github, fleet, project, userId });
|
|
1156
|
-
return c.json(result);
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
const { status, body } = ensureErrorPayload(err);
|
|
1159
|
-
return c.json(body, status);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
})
|
|
1163
|
-
);
|
|
1164
|
-
routes.push(
|
|
1165
|
-
registerApiRoute("/web/github/projects/:id/issues", {
|
|
1166
|
-
method: "GET",
|
|
1167
|
-
requiresAuth: false,
|
|
1168
|
-
handler: async (c) => {
|
|
1169
|
-
const loaded = await loadOrgProject({ github, auth, c: loose(c) });
|
|
1170
|
-
if ("response" in loaded) return loaded.response;
|
|
1171
|
-
const page = parseListPage(c.req.query("page"));
|
|
1172
|
-
if (page === null) return c.json({ error: "invalid_page" }, 400);
|
|
1173
|
-
const label = parseIssueLabelFilter(c.req.query("label"));
|
|
1174
|
-
if (label === null) return c.json({ error: "invalid_label" }, 400);
|
|
1175
|
-
try {
|
|
1176
|
-
const { issues, nextCursor } = await github.intake.listIssues({
|
|
1177
|
-
connection: {
|
|
1178
|
-
type: "app-installation",
|
|
1179
|
-
installationId: Number(loaded.project.installation.externalId)
|
|
1180
|
-
},
|
|
1181
|
-
sourceIds: [loaded.project.repository.slug],
|
|
1182
|
-
labels: label ? [label] : void 0,
|
|
1183
|
-
cursor: String(page)
|
|
1184
|
-
});
|
|
1185
|
-
const responseIssues = issues.map((issue) => ({
|
|
1186
|
-
number: Number(issue.id),
|
|
1187
|
-
title: issue.title,
|
|
1188
|
-
url: issue.url,
|
|
1189
|
-
author: issue.author,
|
|
1190
|
-
labels: issue.labels,
|
|
1191
|
-
comments: issue.commentCount ?? 0,
|
|
1192
|
-
createdAt: issue.createdAt,
|
|
1193
|
-
updatedAt: issue.updatedAt
|
|
1194
|
-
}));
|
|
1195
|
-
await ingestPolledEvents(
|
|
1196
|
-
responseIssues.map((issue) => polledIssueEvent(loaded.project, issue)),
|
|
1197
|
-
options.ingestFactoryEvent
|
|
1198
|
-
);
|
|
1199
|
-
return c.json({
|
|
1200
|
-
issues: responseIssues,
|
|
1201
|
-
nextPage: nextCursor === null ? null : Number(nextCursor)
|
|
1202
|
-
});
|
|
1203
|
-
} catch (err) {
|
|
1204
|
-
return c.json(
|
|
1205
|
-
{ error: "github_fetch_failed", message: err instanceof Error ? err.message : String(err) },
|
|
1206
|
-
502
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
})
|
|
1211
|
-
);
|
|
1212
|
-
routes.push(
|
|
1213
|
-
registerApiRoute("/web/github/projects/:id/issues/:number/triage", {
|
|
1214
|
-
method: "POST",
|
|
1215
|
-
requiresAuth: false,
|
|
1216
|
-
handler: async (c) => {
|
|
1217
|
-
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1218
|
-
if ("response" in owned) return owned.response;
|
|
1219
|
-
const { project, sandboxRow } = owned;
|
|
1220
|
-
const issueNumber = parseIssueNumberParam(c.req.param("number"));
|
|
1221
|
-
if (issueNumber === null) return c.json({ error: "invalid_issue_number" }, 400);
|
|
1222
|
-
let body;
|
|
1223
|
-
try {
|
|
1224
|
-
body = await c.req.json();
|
|
1225
|
-
} catch {
|
|
1226
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1227
|
-
}
|
|
1228
|
-
if (typeof body.title !== "string" || body.title.trim().length === 0 || body.title.length > 5e3) {
|
|
1229
|
-
return c.json({ error: "invalid_title" }, 400);
|
|
1230
|
-
}
|
|
1231
|
-
if (typeof body.url !== "string" || body.url.trim().length === 0 || body.url.length > 2048 || !isCanonicalGithubIssueUrl(body.url, project.repository.slug, issueNumber)) {
|
|
1232
|
-
return c.json({ error: "invalid_url" }, 400);
|
|
1233
|
-
}
|
|
1234
|
-
if (!runIssueTriage) return c.json({ error: "triage_unavailable" }, 503);
|
|
1235
|
-
const branch = `factory/issue-${issueNumber}`;
|
|
1236
|
-
const projectPath = computeWorktreePath(sandboxRow.sandboxWorkdir, branch);
|
|
1237
|
-
await github.addIssueLabels(Number(project.installation.externalId), project.repository.slug, issueNumber, [
|
|
1238
|
-
"auto-triaged"
|
|
1239
|
-
]);
|
|
1240
|
-
const result = await runIssueTriage({
|
|
1241
|
-
repository: project.repository.slug,
|
|
1242
|
-
issueNumber,
|
|
1243
|
-
issueTitle: body.title,
|
|
1244
|
-
issueUrl: body.url,
|
|
1245
|
-
labels: parseStringList(body.labels),
|
|
1246
|
-
installationId: Number(project.installation.externalId),
|
|
1247
|
-
resourceId: project.factoryProjectId,
|
|
1248
|
-
projectPath,
|
|
1249
|
-
branch,
|
|
1250
|
-
defaultModelId: await resolveFactoryDefaultModelId(options.projects, project.factoryProjectId)
|
|
1251
|
-
});
|
|
1252
|
-
await emitAudit?.({
|
|
1253
|
-
context: loose(c),
|
|
1254
|
-
input: {
|
|
1255
|
-
action: "factory.triage.started",
|
|
1256
|
-
factoryProjectId: project.factoryProjectId,
|
|
1257
|
-
projectRepositoryId: project.id,
|
|
1258
|
-
targets: [{ type: "issue", id: String(issueNumber), name: body.title }],
|
|
1259
|
-
metadata: { issueNumber, branch, threadId: result.threadId }
|
|
1260
|
-
}
|
|
1261
|
-
});
|
|
1262
|
-
return c.json(
|
|
1263
|
-
{
|
|
1264
|
-
ok: true,
|
|
1265
|
-
threadId: result.threadId,
|
|
1266
|
-
projectPath: result.projectPath ?? projectPath,
|
|
1267
|
-
branch: result.branch ?? branch
|
|
1268
|
-
},
|
|
1269
|
-
202
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
})
|
|
1273
|
-
);
|
|
1274
|
-
routes.push(
|
|
1275
|
-
registerApiRoute("/web/github/projects/:id/prs", {
|
|
1276
|
-
method: "GET",
|
|
1277
|
-
requiresAuth: false,
|
|
1278
|
-
handler: async (c) => {
|
|
1279
|
-
const loaded = await loadOrgProject({ github, auth, c: loose(c) });
|
|
1280
|
-
if ("response" in loaded) return loaded.response;
|
|
1281
|
-
const page = parseListPage(c.req.query("page"));
|
|
1282
|
-
if (page === null) return c.json({ error: "invalid_page" }, 400);
|
|
1283
|
-
try {
|
|
1284
|
-
const { pullRequests, nextCursor } = await github.versionControl.listPullRequests({
|
|
1285
|
-
connection: {
|
|
1286
|
-
type: "app-installation",
|
|
1287
|
-
installationId: Number(loaded.project.installation.externalId)
|
|
1288
|
-
},
|
|
1289
|
-
sourceId: loaded.project.repository.slug,
|
|
1290
|
-
includeDrafts: false,
|
|
1291
|
-
cursor: String(page)
|
|
1292
|
-
});
|
|
1293
|
-
const responsePullRequests = pullRequests.map((pr) => ({
|
|
1294
|
-
number: Number(pr.id),
|
|
1295
|
-
title: pr.title,
|
|
1296
|
-
url: pr.url,
|
|
1297
|
-
author: pr.author,
|
|
1298
|
-
baseBranch: pr.baseBranch,
|
|
1299
|
-
headBranch: pr.headBranch,
|
|
1300
|
-
createdAt: pr.createdAt,
|
|
1301
|
-
updatedAt: pr.updatedAt
|
|
1302
|
-
}));
|
|
1303
|
-
await ingestPolledEvents(
|
|
1304
|
-
responsePullRequests.map((pullRequest) => polledPullRequestEvent(loaded.project, pullRequest)),
|
|
1305
|
-
options.ingestFactoryEvent
|
|
1306
|
-
);
|
|
1307
|
-
return c.json({
|
|
1308
|
-
pullRequests: responsePullRequests,
|
|
1309
|
-
nextPage: nextCursor === null ? null : Number(nextCursor)
|
|
1310
|
-
});
|
|
1311
|
-
} catch (err) {
|
|
1312
|
-
return c.json(
|
|
1313
|
-
{ error: "github_fetch_failed", message: err instanceof Error ? err.message : String(err) },
|
|
1314
|
-
502
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
})
|
|
1319
|
-
);
|
|
1320
|
-
routes.push(
|
|
1321
|
-
registerApiRoute("/web/github/projects/:id/settings", {
|
|
1322
|
-
method: "GET",
|
|
1323
|
-
requiresAuth: false,
|
|
1324
|
-
handler: async (c) => {
|
|
1325
|
-
const loaded = await loadOrgProject({ github, auth, c: loose(c) });
|
|
1326
|
-
if ("response" in loaded) return loaded.response;
|
|
1327
|
-
return c.json({ setupCommand: loaded.project.setupCommand });
|
|
1328
|
-
}
|
|
1329
|
-
})
|
|
1330
|
-
);
|
|
1331
|
-
routes.push(
|
|
1332
|
-
registerApiRoute("/web/github/projects/:id/settings", {
|
|
1333
|
-
method: "POST",
|
|
1334
|
-
requiresAuth: false,
|
|
1335
|
-
handler: async (c) => {
|
|
1336
|
-
const loaded = await loadOrgProject({ github, auth, c: loose(c) });
|
|
1337
|
-
if ("response" in loaded) return loaded.response;
|
|
1338
|
-
let body;
|
|
1339
|
-
try {
|
|
1340
|
-
body = await c.req.json();
|
|
1341
|
-
} catch {
|
|
1342
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1343
|
-
}
|
|
1344
|
-
if (body.setupCommand !== null && typeof body.setupCommand !== "string") {
|
|
1345
|
-
return c.json({ error: "Invalid setupCommand" }, 400);
|
|
1346
|
-
}
|
|
1347
|
-
if (typeof body.setupCommand === "string" && body.setupCommand.length > 2e3) {
|
|
1348
|
-
return c.json({ error: "setupCommand too long (max 2000 characters)" }, 400);
|
|
1349
|
-
}
|
|
1350
|
-
if (typeof body.setupCommand === "string" && /[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(body.setupCommand)) {
|
|
1351
|
-
return c.json({ error: "setupCommand contains control characters" }, 400);
|
|
1352
|
-
}
|
|
1353
|
-
const setupCommand = typeof body.setupCommand === "string" && body.setupCommand.trim().length > 0 ? body.setupCommand.trim() : null;
|
|
1354
|
-
await github.sourceControlStorage.projectRepositories.update({
|
|
1355
|
-
orgId: loaded.project.installation.orgId,
|
|
1356
|
-
id: loaded.project.id,
|
|
1357
|
-
input: { setupCommand }
|
|
1358
|
-
});
|
|
1359
|
-
return c.json({ setupCommand });
|
|
1360
|
-
}
|
|
1361
|
-
})
|
|
1362
|
-
);
|
|
1363
|
-
const parsePatKind = (value) => {
|
|
1364
|
-
if (value === void 0 || value === null || value === "default") return "default";
|
|
1365
|
-
if (value === "reviewer") return "reviewer";
|
|
1366
|
-
return null;
|
|
1367
|
-
};
|
|
1368
|
-
routes.push(
|
|
1369
|
-
registerApiRoute("/web/github/pat", {
|
|
1370
|
-
method: "GET",
|
|
1371
|
-
requiresAuth: false,
|
|
1372
|
-
handler: async (c) => {
|
|
1373
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1374
|
-
if ("response" in resolved) return resolved.response;
|
|
1375
|
-
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1376
|
-
}
|
|
1377
|
-
}),
|
|
1378
|
-
registerApiRoute("/web/github/pat", {
|
|
1379
|
-
method: "POST",
|
|
1380
|
-
requiresAuth: false,
|
|
1381
|
-
handler: async (c) => {
|
|
1382
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1383
|
-
if ("response" in resolved) return resolved.response;
|
|
1384
|
-
let body;
|
|
1385
|
-
try {
|
|
1386
|
-
body = await c.req.json();
|
|
1387
|
-
} catch {
|
|
1388
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1389
|
-
}
|
|
1390
|
-
const kind = parsePatKind(body.kind);
|
|
1391
|
-
if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
|
|
1392
|
-
const token = typeof body.token === "string" ? body.token.trim() : "";
|
|
1393
|
-
if (!token) return c.json({ error: "A token is required" }, 400);
|
|
1394
|
-
if (token.length > 500) return c.json({ error: "Token too long (max 500 characters)" }, 400);
|
|
1395
|
-
if (/\s/.test(token)) return c.json({ error: "Token must not contain whitespace" }, 400);
|
|
1396
|
-
await setGithubPat(github.integrationStorage, resolved.tenant.orgId, token, kind);
|
|
1397
|
-
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1398
|
-
}
|
|
1399
|
-
}),
|
|
1400
|
-
registerApiRoute("/web/github/pat", {
|
|
1401
|
-
method: "DELETE",
|
|
1402
|
-
requiresAuth: false,
|
|
1403
|
-
handler: async (c) => {
|
|
1404
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1405
|
-
if ("response" in resolved) return resolved.response;
|
|
1406
|
-
const kind = parsePatKind(c.req.query("kind"));
|
|
1407
|
-
if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
|
|
1408
|
-
await clearGithubPat(github.integrationStorage, resolved.tenant.orgId, kind);
|
|
1409
|
-
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1410
|
-
}
|
|
1411
|
-
})
|
|
1412
|
-
);
|
|
1413
|
-
routes.push(...buildProjectGitRoutes({ github, auth, fleet, emitAudit }));
|
|
1414
|
-
return routes;
|
|
1415
|
-
}
|
|
1416
|
-
async function loadOrgProject(options) {
|
|
1417
|
-
const { github, auth, c } = options;
|
|
1418
|
-
const resolved = await resolveOrgTenant(c, auth);
|
|
1419
|
-
if ("response" in resolved) return { response: resolved.response };
|
|
1420
|
-
const { orgId, userId } = resolved.tenant;
|
|
1421
|
-
const projectRepositoryId = c.req.param("id");
|
|
1422
|
-
if (!projectRepositoryId) {
|
|
1423
|
-
return { response: c.json({ error: "Project repository not found" }, 404) };
|
|
1424
|
-
}
|
|
1425
|
-
const project = await resolveProjectRepository({ github, orgId, projectRepositoryId });
|
|
1426
|
-
if (!project) {
|
|
1427
|
-
return { response: c.json({ error: "Project repository not found" }, 404) };
|
|
1428
|
-
}
|
|
1429
|
-
return { project, userId };
|
|
1430
|
-
}
|
|
1431
|
-
function identityFromUser(user) {
|
|
1432
|
-
const u = user;
|
|
1433
|
-
return { name: u?.name ?? null, email: u?.email ?? null };
|
|
1434
|
-
}
|
|
1435
|
-
async function resolveProjectSandbox(options) {
|
|
1436
|
-
const { fleet, sandboxRow } = options;
|
|
1437
|
-
if (!sandboxRow.sandboxId) {
|
|
1438
|
-
throw new MaterializeError("Project sandbox is not provisioned. Open the project first.", "clone-failed");
|
|
1439
|
-
}
|
|
1440
|
-
return fleet.reattachSandbox(sandboxRow.sandboxId);
|
|
1441
|
-
}
|
|
1442
|
-
async function loadOrCreateSandboxRow(github, project, userId) {
|
|
1443
|
-
return github.sourceControlStorage.sandboxes.getOrCreate({ projectRepository: project, userId });
|
|
1444
|
-
}
|
|
1445
|
-
async function prepareProject(options) {
|
|
1446
|
-
const { github, fleet, project, userId, onProgress } = options;
|
|
1447
|
-
const sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
|
|
1448
|
-
const access = await github.versionControl.getRepositoryAccess({
|
|
1449
|
-
orgId: project.installation.orgId,
|
|
1450
|
-
repositoryId: project.repository.id
|
|
1451
|
-
});
|
|
1452
|
-
if (!access.authorization) {
|
|
1453
|
-
throw new MaterializeError("Repository access did not include a bearer token.", "clone-failed");
|
|
1454
|
-
}
|
|
1455
|
-
const ghCliToken = await getGithubPat(() => github.integrationStorage, project.installation.orgId) ?? access.authorization.token;
|
|
1456
|
-
const sandbox = await ensureProjectSandbox({
|
|
1457
|
-
fleet,
|
|
1458
|
-
row: sandboxRow,
|
|
1459
|
-
storage: github.sourceControlStorage.sandboxes,
|
|
1460
|
-
token: ghCliToken,
|
|
1461
|
-
onProgress
|
|
1462
|
-
});
|
|
1463
|
-
const fresh = await github.sourceControlStorage.sandboxes.getById({ id: sandboxRow.id });
|
|
1464
|
-
const finalRow = fresh ?? sandboxRow;
|
|
1465
|
-
await materializeRepo({
|
|
1466
|
-
row: finalRow,
|
|
1467
|
-
repoInfo: { repoFullName: project.repository.slug, defaultBranch: project.defaultBranch },
|
|
1468
|
-
sandbox,
|
|
1469
|
-
token: access.authorization.token,
|
|
1470
|
-
storage: github.sourceControlStorage.sandboxes,
|
|
1471
|
-
onProgress
|
|
1472
|
-
});
|
|
1473
|
-
const result = {
|
|
1474
|
-
resourceId: project.factoryProjectId,
|
|
1475
|
-
factoryProjectId: project.factoryProjectId,
|
|
1476
|
-
projectRepositoryId: project.id,
|
|
1477
|
-
sandboxId: finalRow.sandboxId,
|
|
1478
|
-
sandboxWorkdir: finalRow.sandboxWorkdir
|
|
1479
|
-
};
|
|
1480
|
-
const done = { phase: "done", message: "Workspace ready." };
|
|
1481
|
-
onProgress?.(done);
|
|
1482
|
-
return result;
|
|
1483
|
-
}
|
|
1484
|
-
function ensureErrorPayload(err) {
|
|
1485
|
-
if (err instanceof SandboxBudgetError) {
|
|
1486
|
-
return { status: 429, body: { error: err.code, message: err.message } };
|
|
1487
|
-
}
|
|
1488
|
-
if (err instanceof MaterializeError) {
|
|
1489
|
-
return { status: 502, body: { error: err.code, message: err.message } };
|
|
1490
|
-
}
|
|
1491
|
-
return {
|
|
1492
|
-
status: 500,
|
|
1493
|
-
body: { error: "materialize_failed", message: err instanceof Error ? err.message : String(err) }
|
|
1494
|
-
};
|
|
1495
|
-
}
|
|
1496
|
-
function gitErrorResponse(c, err) {
|
|
1497
|
-
if (err instanceof WorktreeError) {
|
|
1498
|
-
return c.json({ error: err.code, message: err.message }, err.code === "invalid-branch" ? 400 : 502);
|
|
1499
|
-
}
|
|
1500
|
-
if (err instanceof MaterializeError) {
|
|
1501
|
-
return c.json({ error: err.code, message: err.message }, 502);
|
|
1502
|
-
}
|
|
1503
|
-
return c.json({ error: "git_failed", message: err instanceof Error ? err.message : String(err) }, 500);
|
|
1504
|
-
}
|
|
1505
|
-
async function loadOwnedProject(options) {
|
|
1506
|
-
const { github, auth, fleet, c } = options;
|
|
1507
|
-
const resolved = await resolveOrgTenant(c, auth);
|
|
1508
|
-
if ("response" in resolved) return { response: resolved.response };
|
|
1509
|
-
const { orgId, userId } = resolved.tenant;
|
|
1510
|
-
if (!fleet.enabled) {
|
|
1511
|
-
return {
|
|
1512
|
-
response: c.json({ error: "sandbox_not_configured", message: "No sandbox provider is configured." }, 503)
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
const projectRepositoryId = c.req.param("id");
|
|
1516
|
-
if (!projectRepositoryId) {
|
|
1517
|
-
return { response: c.json({ error: "Project repository not found" }, 404) };
|
|
1518
|
-
}
|
|
1519
|
-
const project = await resolveProjectRepository({ github, orgId, projectRepositoryId });
|
|
1520
|
-
if (!project) {
|
|
1521
|
-
return { response: c.json({ error: "Project repository not found" }, 404) };
|
|
1522
|
-
}
|
|
1523
|
-
const sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
|
|
1524
|
-
return { orgId, userId, project, sandboxRow };
|
|
1525
|
-
}
|
|
1526
|
-
function buildProjectGitRoutes({
|
|
1527
|
-
github,
|
|
1528
|
-
auth,
|
|
1529
|
-
fleet,
|
|
1530
|
-
emitAudit
|
|
1531
|
-
}) {
|
|
1532
|
-
return [
|
|
1533
|
-
// ── Create / list Factory sessions ──────────────────────────────────────
|
|
1534
|
-
registerApiRoute("/web/github/projects/:id/sessions", {
|
|
1535
|
-
method: "GET",
|
|
1536
|
-
requiresAuth: false,
|
|
1537
|
-
handler: async (c) => {
|
|
1538
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1539
|
-
if ("response" in resolved) return resolved.response;
|
|
1540
|
-
const { orgId, userId } = resolved.tenant;
|
|
1541
|
-
const projectRepositoryId = c.req.param("id");
|
|
1542
|
-
const project = projectRepositoryId ? await resolveProjectRepository({ github, orgId, projectRepositoryId }) : null;
|
|
1543
|
-
if (!project) return c.json({ error: "Project repository not found" }, 404);
|
|
1544
|
-
const sessions = await github.sourceControlStorage.sessions.list({ projectRepositoryId: project.id, userId });
|
|
1545
|
-
return c.json({ sessions });
|
|
1546
|
-
}
|
|
1547
|
-
}),
|
|
1548
|
-
registerApiRoute("/web/github/projects/:id/sessions", {
|
|
1549
|
-
method: "POST",
|
|
1550
|
-
requiresAuth: false,
|
|
1551
|
-
handler: async (c) => {
|
|
1552
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1553
|
-
if ("response" in resolved) return resolved.response;
|
|
1554
|
-
const { orgId, userId } = resolved.tenant;
|
|
1555
|
-
const projectRepositoryId = c.req.param("id");
|
|
1556
|
-
const project = projectRepositoryId ? await resolveProjectRepository({ github, orgId, projectRepositoryId }) : null;
|
|
1557
|
-
if (!project) return c.json({ error: "Project repository not found" }, 404);
|
|
1558
|
-
let body;
|
|
1559
|
-
try {
|
|
1560
|
-
body = await c.req.json();
|
|
1561
|
-
} catch {
|
|
1562
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1563
|
-
}
|
|
1564
|
-
if (!isValidGitRef(body.branch)) return c.json({ error: "Invalid branch" }, 400);
|
|
1565
|
-
const baseBranch = body.baseBranch === void 0 ? project.defaultBranch : body.baseBranch;
|
|
1566
|
-
if (!isValidGitRef(baseBranch)) return c.json({ error: "Invalid baseBranch" }, 400);
|
|
1567
|
-
const session = await github.sourceControlStorage.sessions.create({
|
|
1568
|
-
sessionId: randomUUID(),
|
|
1569
|
-
projectRepositoryId: project.id,
|
|
1570
|
-
orgId,
|
|
1571
|
-
userId,
|
|
1572
|
-
branch: body.branch,
|
|
1573
|
-
baseBranch
|
|
1574
|
-
});
|
|
1575
|
-
return c.json({ session });
|
|
1576
|
-
}
|
|
1577
|
-
}),
|
|
1578
|
-
registerApiRoute("/web/user-sessions/:sessionId", {
|
|
1579
|
-
method: "GET",
|
|
1580
|
-
requiresAuth: false,
|
|
1581
|
-
handler: async (c) => {
|
|
1582
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1583
|
-
if ("response" in resolved) return resolved.response;
|
|
1584
|
-
const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
|
|
1585
|
-
if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) {
|
|
1586
|
-
return c.json({ error: "Session not found" }, 404);
|
|
1587
|
-
}
|
|
1588
|
-
return c.json({ session });
|
|
1589
|
-
}
|
|
1590
|
-
}),
|
|
1591
|
-
registerApiRoute("/web/user-sessions/:sessionId", {
|
|
1592
|
-
method: "DELETE",
|
|
1593
|
-
requiresAuth: false,
|
|
1594
|
-
handler: async (c) => {
|
|
1595
|
-
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1596
|
-
if ("response" in resolved) return resolved.response;
|
|
1597
|
-
const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
|
|
1598
|
-
if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) {
|
|
1599
|
-
return c.json({ error: "Session not found" }, 404);
|
|
1600
|
-
}
|
|
1601
|
-
let sandbox;
|
|
1602
|
-
if (session.sandboxId) {
|
|
1603
|
-
try {
|
|
1604
|
-
sandbox = await fleet.reattachSandbox(session.sandboxId);
|
|
1605
|
-
} catch {
|
|
1606
|
-
}
|
|
1607
|
-
await fleet.teardownSandbox(
|
|
1608
|
-
{
|
|
1609
|
-
sandboxId: session.sandboxId,
|
|
1610
|
-
setSandboxId: async () => {
|
|
1611
|
-
},
|
|
1612
|
-
clear: async () => {
|
|
1613
|
-
await github.sourceControlStorage.sessions.setSandbox({
|
|
1614
|
-
id: session.id,
|
|
1615
|
-
sandboxId: null,
|
|
1616
|
-
sandboxWorkdir: session.sandboxWorkdir ?? ""
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
},
|
|
1620
|
-
sandbox
|
|
1621
|
-
);
|
|
1622
|
-
}
|
|
1623
|
-
await github.sourceControlStorage.sessions.delete(session.id);
|
|
1624
|
-
return c.json({ removed: true });
|
|
1625
|
-
}
|
|
1626
|
-
}),
|
|
1627
|
-
// ── Stage all + commit inside a Factory session workspace ──────────────
|
|
1628
|
-
registerApiRoute("/web/github/projects/:id/commit", {
|
|
1629
|
-
method: "POST",
|
|
1630
|
-
requiresAuth: false,
|
|
1631
|
-
handler: async (c) => {
|
|
1632
|
-
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1633
|
-
if ("response" in owned) return owned.response;
|
|
1634
|
-
const { userId, project } = owned;
|
|
1635
|
-
let body;
|
|
1636
|
-
try {
|
|
1637
|
-
body = await c.req.json();
|
|
1638
|
-
} catch {
|
|
1639
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1640
|
-
}
|
|
1641
|
-
if (typeof body.message !== "string" || body.message.trim().length === 0 || body.message.length > 5e3) {
|
|
1642
|
-
return c.json({ error: "Invalid message" }, 400);
|
|
1643
|
-
}
|
|
1644
|
-
const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
|
|
1645
|
-
if (!sessionWorkspace) {
|
|
1646
|
-
return c.json({ error: "Invalid sessionId" }, 400);
|
|
1647
|
-
}
|
|
1648
|
-
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1649
|
-
try {
|
|
1650
|
-
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1651
|
-
const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
|
|
1652
|
-
const result = await commitAll(
|
|
1653
|
-
sandbox,
|
|
1654
|
-
workdir,
|
|
1655
|
-
body.message,
|
|
1656
|
-
identityFromUser(await auth.ensureUser(loose(c)))
|
|
1657
|
-
);
|
|
1658
|
-
if (result.committed) {
|
|
1659
|
-
await emitAudit?.({
|
|
1660
|
-
context: loose(c),
|
|
1661
|
-
input: {
|
|
1662
|
-
action: "factory.git.commit",
|
|
1663
|
-
factoryProjectId: project.factoryProjectId,
|
|
1664
|
-
projectRepositoryId: project.id,
|
|
1665
|
-
targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
|
|
1666
|
-
metadata: { sessionId: sessionWorkspace.session.sessionId }
|
|
1667
|
-
}
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
return c.json({ committed: result.committed });
|
|
1671
|
-
});
|
|
1672
|
-
} catch (err) {
|
|
1673
|
-
return gitErrorResponse(loose(c), err);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
}),
|
|
1677
|
-
// ── Push a branch back to GitHub ────────────────────────────────────────
|
|
1678
|
-
registerApiRoute("/web/github/projects/:id/push", {
|
|
1679
|
-
method: "POST",
|
|
1680
|
-
requiresAuth: false,
|
|
1681
|
-
handler: async (c) => {
|
|
1682
|
-
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1683
|
-
if ("response" in owned) return owned.response;
|
|
1684
|
-
const { orgId, userId, project } = owned;
|
|
1685
|
-
let body;
|
|
1686
|
-
try {
|
|
1687
|
-
body = await c.req.json();
|
|
1688
|
-
} catch {
|
|
1689
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1690
|
-
}
|
|
1691
|
-
if (!isValidGitRef(body.branch)) {
|
|
1692
|
-
return c.json({ error: "Invalid branch" }, 400);
|
|
1693
|
-
}
|
|
1694
|
-
const branch = body.branch;
|
|
1695
|
-
const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
|
|
1696
|
-
if (!sessionWorkspace) {
|
|
1697
|
-
return c.json({ error: "Invalid sessionId" }, 400);
|
|
1698
|
-
}
|
|
1699
|
-
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1700
|
-
try {
|
|
1701
|
-
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1702
|
-
const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
|
|
1703
|
-
const access = await github.versionControl.getRepositoryAccess({
|
|
1704
|
-
orgId,
|
|
1705
|
-
repositoryId: project.repository.id
|
|
1706
|
-
});
|
|
1707
|
-
if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
|
|
1708
|
-
await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
|
|
1709
|
-
await emitAudit?.({
|
|
1710
|
-
context: loose(c),
|
|
1711
|
-
input: {
|
|
1712
|
-
action: "factory.git.push",
|
|
1713
|
-
factoryProjectId: project.factoryProjectId,
|
|
1714
|
-
projectRepositoryId: project.id,
|
|
1715
|
-
targets: [{ type: "branch", id: branch }],
|
|
1716
|
-
metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
return c.json({ pushed: true, branch });
|
|
1720
|
-
});
|
|
1721
|
-
} catch (err) {
|
|
1722
|
-
return gitErrorResponse(loose(c), err);
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}),
|
|
1726
|
-
// ── Open a pull request through the version-control capability ─────────
|
|
1727
|
-
registerApiRoute("/web/github/projects/:id/pr", {
|
|
1728
|
-
method: "POST",
|
|
1729
|
-
requiresAuth: false,
|
|
1730
|
-
handler: async (c) => {
|
|
1731
|
-
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1732
|
-
if ("response" in owned) return owned.response;
|
|
1733
|
-
const { orgId, userId, project } = owned;
|
|
1734
|
-
let body;
|
|
1735
|
-
try {
|
|
1736
|
-
body = await c.req.json();
|
|
1737
|
-
} catch {
|
|
1738
|
-
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1739
|
-
}
|
|
1740
|
-
if (!isValidGitRef(body.branch)) {
|
|
1741
|
-
return c.json({ error: "Invalid branch" }, 400);
|
|
1742
|
-
}
|
|
1743
|
-
const base = body.base === void 0 ? project.defaultBranch : body.base;
|
|
1744
|
-
if (!isValidGitRef(base)) {
|
|
1745
|
-
return c.json({ error: "Invalid base" }, 400);
|
|
1746
|
-
}
|
|
1747
|
-
if (typeof body.title !== "string" || body.title.trim().length === 0 || body.title.length > 256) {
|
|
1748
|
-
return c.json({ error: "Invalid title" }, 400);
|
|
1749
|
-
}
|
|
1750
|
-
if (body.body !== void 0 && (typeof body.body !== "string" || body.body.length > 65536)) {
|
|
1751
|
-
return c.json({ error: "Invalid body" }, 400);
|
|
1752
|
-
}
|
|
1753
|
-
const head = body.branch;
|
|
1754
|
-
const title = body.title;
|
|
1755
|
-
const prBody = body.body;
|
|
1756
|
-
const sessionWorkspace = await resolveSessionWorkspace(github, project.id, userId, body.sessionId);
|
|
1757
|
-
if (!sessionWorkspace) {
|
|
1758
|
-
return c.json({ error: "Invalid sessionId" }, 400);
|
|
1759
|
-
}
|
|
1760
|
-
try {
|
|
1761
|
-
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1762
|
-
const result = await github.versionControl.createPullRequest({
|
|
1763
|
-
connection: {
|
|
1764
|
-
type: "app-installation",
|
|
1765
|
-
installationId: Number(project.installation.externalId)
|
|
1766
|
-
},
|
|
1767
|
-
sourceId: project.repository.slug,
|
|
1768
|
-
baseBranch: base,
|
|
1769
|
-
headBranch: head,
|
|
1770
|
-
title,
|
|
1771
|
-
body: prBody,
|
|
1772
|
-
actingUserId: userId
|
|
1773
|
-
});
|
|
1774
|
-
await emitAudit?.({
|
|
1775
|
-
context: loose(c),
|
|
1776
|
-
input: {
|
|
1777
|
-
action: "factory.git.pr_opened",
|
|
1778
|
-
factoryProjectId: project.factoryProjectId,
|
|
1779
|
-
projectRepositoryId: project.id,
|
|
1780
|
-
targets: [{ type: "pull_request", id: result.url, name: title }],
|
|
1781
|
-
metadata: { branch: head, base, url: result.url }
|
|
1782
|
-
}
|
|
1783
|
-
});
|
|
1784
|
-
const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
|
|
1785
|
-
if (pullRequestNumber) {
|
|
1786
|
-
const sessionId = sessionWorkspace.session.sessionId;
|
|
1787
|
-
await subscribeToPullRequest(
|
|
1788
|
-
{
|
|
1789
|
-
orgId,
|
|
1790
|
-
installationExternalId: project.installation.externalId,
|
|
1791
|
-
projectRepositoryId: project.id,
|
|
1792
|
-
repositoryExternalId: project.repository.externalId,
|
|
1793
|
-
repositorySlug: project.repository.slug,
|
|
1794
|
-
changeRequestId: pullRequestNumber.toString(),
|
|
1795
|
-
sessionId,
|
|
1796
|
-
ownerId: userId,
|
|
1797
|
-
resourceId: sessionId,
|
|
1798
|
-
threadId: sessionId,
|
|
1799
|
-
source: "factory-pr-create",
|
|
1800
|
-
subscribedByUserId: userId
|
|
1801
|
-
},
|
|
1802
|
-
github.integrationStorage
|
|
1803
|
-
).catch((error) => {
|
|
1804
|
-
console.warn(
|
|
1805
|
-
`[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
|
|
1806
|
-
error
|
|
1807
|
-
);
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
return c.json({ url: result.url });
|
|
1811
|
-
});
|
|
1812
|
-
} catch (err) {
|
|
1813
|
-
return c.json(
|
|
1814
|
-
{ error: "github_pr_create_failed", message: err instanceof Error ? err.message : String(err) },
|
|
1815
|
-
502
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
}),
|
|
1820
|
-
// ── Tear down the caller's sandbox for a project ────────────────────────
|
|
1821
|
-
// Per-user teardown only: drops the caller's `(project, user)` sandbox
|
|
1822
|
-
// binding and stops the VM, freeing a slot in the per-replica budget. Project
|
|
1823
|
-
// deletion at the org level is out of scope (org admin model is later).
|
|
1824
|
-
registerApiRoute("/web/github/projects/:id/sandbox", {
|
|
1825
|
-
method: "DELETE",
|
|
1826
|
-
requiresAuth: false,
|
|
1827
|
-
handler: async (c) => {
|
|
1828
|
-
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1829
|
-
if ("response" in owned) return owned.response;
|
|
1830
|
-
const { sandboxRow } = owned;
|
|
1831
|
-
if (!sandboxRow.sandboxId) {
|
|
1832
|
-
return c.json({ tornDown: false });
|
|
1833
|
-
}
|
|
1834
|
-
try {
|
|
1835
|
-
return await withSessionOperationLock(`sandbox:${sandboxRow.id}`, async () => {
|
|
1836
|
-
const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
|
|
1837
|
-
await teardownProjectSandbox({
|
|
1838
|
-
fleet,
|
|
1839
|
-
row: sandboxRow,
|
|
1840
|
-
storage: github.sourceControlStorage.sandboxes,
|
|
1841
|
-
sandbox
|
|
1842
|
-
});
|
|
1843
|
-
return c.json({ tornDown: true });
|
|
1844
|
-
});
|
|
1845
|
-
} catch (err) {
|
|
1846
|
-
return gitErrorResponse(loose(c), err);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
})
|
|
1850
|
-
];
|
|
1851
|
-
}
|
|
1852
|
-
async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
|
|
1853
|
-
if (typeof sessionId !== "string") {
|
|
1854
|
-
return void 0;
|
|
1855
|
-
}
|
|
1856
|
-
const session = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
|
|
1857
|
-
if (session?.projectRepositoryId !== projectId || session.userId !== userId || !session.sandboxId || !session.sandboxWorkdir) {
|
|
1858
|
-
return void 0;
|
|
1859
|
-
}
|
|
1860
|
-
return {
|
|
1861
|
-
session,
|
|
1862
|
-
workdir: session.sandboxWorkdir,
|
|
1863
|
-
sandboxBinding: {
|
|
1864
|
-
id: session.id,
|
|
1865
|
-
projectRepositoryId: session.projectRepositoryId,
|
|
1866
|
-
userId: session.userId,
|
|
1867
|
-
sandboxId: session.sandboxId,
|
|
1868
|
-
sandboxWorkdir: session.sandboxWorkdir,
|
|
1869
|
-
materializedAt: session.materializedAt,
|
|
1870
|
-
createdAt: session.createdAt
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
// src/rules/resolve.ts
|
|
1876
|
-
function resolveFactoryGithubRule(rules, event) {
|
|
1877
|
-
return rules.github[event]?.onEvent;
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
// src/rules/types.ts
|
|
1881
|
-
var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
|
|
1882
|
-
var FACTORY_RULE_BOARDS = ["work", "review"];
|
|
1883
|
-
|
|
1884
|
-
// src/rules/validation.ts
|
|
1885
|
-
var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
|
|
1886
|
-
var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
1887
|
-
var MAX_REASON_LENGTH = 512;
|
|
1888
|
-
var MAX_TITLE_LENGTH = 512;
|
|
1889
|
-
var MAX_MESSAGE_LENGTH = 8192;
|
|
1890
|
-
var MAX_ARGUMENTS_LENGTH = 4096;
|
|
1891
|
-
var MAX_ROLE_LENGTH = 32;
|
|
1892
|
-
var MAX_SKILL_NAME_LENGTH = 128;
|
|
1893
|
-
var MAX_SOURCE_KEY_LENGTH = 256;
|
|
1894
|
-
var MAX_URL_LENGTH = 2048;
|
|
1895
|
-
var MAX_METADATA_JSON_LENGTH = 16384;
|
|
1896
|
-
var MAX_JSON_DEPTH = 8;
|
|
1897
|
-
var MAX_JSON_COLLECTION_SIZE = 100;
|
|
1898
|
-
var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
1899
|
-
var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1900
|
-
var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
|
|
1901
|
-
var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
|
|
1902
|
-
var REJECTION_CODES = [
|
|
1903
|
-
"forbidden",
|
|
1904
|
-
"invalid_transition",
|
|
1905
|
-
"missing_binding",
|
|
1906
|
-
"stale",
|
|
1907
|
-
"timeout",
|
|
1908
|
-
"rule_error",
|
|
1909
|
-
"causal_depth_exceeded",
|
|
1910
|
-
"repeated_transition"
|
|
1911
|
-
];
|
|
1912
|
-
var FactoryRuleValidationError = class extends Error {
|
|
1913
|
-
code = "invalid_factory_rule";
|
|
1914
|
-
constructor(message) {
|
|
1915
|
-
super(message);
|
|
1916
|
-
this.name = "FactoryRuleValidationError";
|
|
1917
|
-
}
|
|
1918
|
-
};
|
|
1919
|
-
function isPlainObject(value) {
|
|
1920
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1921
|
-
const prototype = Object.getPrototypeOf(value);
|
|
1922
|
-
return prototype === Object.prototype || prototype === null;
|
|
1923
|
-
}
|
|
1924
|
-
function assertExactKeys(value, keys, label) {
|
|
1925
|
-
const allowed = new Set(keys);
|
|
1926
|
-
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
1927
|
-
throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
function boundedString(value, label, max, pattern) {
|
|
1931
|
-
if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
|
|
1932
|
-
const normalized = value.trim();
|
|
1933
|
-
if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
|
|
1934
|
-
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
1935
|
-
}
|
|
1936
|
-
return normalized;
|
|
1937
|
-
}
|
|
1938
|
-
function optionalBoundedString(value, label, max) {
|
|
1939
|
-
if (value === void 0) return void 0;
|
|
1940
|
-
return boundedString(value, label, max);
|
|
1941
|
-
}
|
|
1942
|
-
function enumValue(value, allowed, label) {
|
|
1943
|
-
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
1944
|
-
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
1945
|
-
}
|
|
1946
|
-
return value;
|
|
1947
|
-
}
|
|
1948
|
-
function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
1949
|
-
if (value === null || typeof value === "boolean" || typeof value === "string") return value;
|
|
1950
|
-
if (typeof value === "number") {
|
|
1951
|
-
if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
|
|
1952
|
-
return value;
|
|
1953
|
-
}
|
|
1954
|
-
if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
|
|
1955
|
-
throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
|
|
1956
|
-
}
|
|
1957
|
-
if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
|
|
1958
|
-
seen.add(value);
|
|
1959
|
-
try {
|
|
1960
|
-
if (Array.isArray(value)) {
|
|
1961
|
-
if (value.length > MAX_JSON_COLLECTION_SIZE) {
|
|
1962
|
-
throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
|
|
1963
|
-
}
|
|
1964
|
-
return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
|
|
1965
|
-
}
|
|
1966
|
-
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
|
|
1967
|
-
const entries = Object.entries(value);
|
|
1968
|
-
if (entries.length > MAX_JSON_COLLECTION_SIZE) {
|
|
1969
|
-
throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
|
|
1970
|
-
}
|
|
1971
|
-
const sanitized = {};
|
|
1972
|
-
for (const [key, entry] of entries) {
|
|
1973
|
-
const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
|
|
1974
|
-
sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
|
|
1975
|
-
}
|
|
1976
|
-
return sanitized;
|
|
1977
|
-
} finally {
|
|
1978
|
-
seen.delete(value);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
function sanitizeMetadata(value) {
|
|
1982
|
-
if (value === void 0) return void 0;
|
|
1983
|
-
const sanitized = normalizeFactoryRuleJsonValue(value);
|
|
1984
|
-
if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
|
|
1985
|
-
if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
|
|
1986
|
-
throw new FactoryRuleValidationError("Rule metadata is too large.");
|
|
1987
|
-
}
|
|
1988
|
-
return sanitized;
|
|
1989
|
-
}
|
|
1990
|
-
function commonCommitFields(value) {
|
|
1991
|
-
return {
|
|
1992
|
-
idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
|
|
1993
|
-
};
|
|
1994
|
-
}
|
|
1995
|
-
function validateFactoryRuleDecision(value, causalDepth = 0) {
|
|
1996
|
-
if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
|
|
1997
|
-
throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
|
|
1998
|
-
}
|
|
1999
|
-
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
|
|
2000
|
-
const type = value.type;
|
|
2001
|
-
if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
|
|
2002
|
-
switch (type) {
|
|
2003
|
-
case "reject": {
|
|
2004
|
-
assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
|
|
2005
|
-
return {
|
|
2006
|
-
type,
|
|
2007
|
-
code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
|
|
2008
|
-
reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
case "transition": {
|
|
2012
|
-
assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
|
|
2013
|
-
return {
|
|
2014
|
-
type,
|
|
2015
|
-
...commonCommitFields(value),
|
|
2016
|
-
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
|
|
2017
|
-
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
|
|
2018
|
-
};
|
|
2019
|
-
}
|
|
2020
|
-
case "upsertLinkedWorkItem": {
|
|
2021
|
-
assertExactKeys(
|
|
2022
|
-
value,
|
|
2023
|
-
["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
|
|
2024
|
-
"Factory linked work item decision"
|
|
2025
|
-
);
|
|
2026
|
-
const url = value.url;
|
|
2027
|
-
if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
|
|
2028
|
-
throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
|
|
2029
|
-
}
|
|
2030
|
-
const metadata = sanitizeMetadata(value.metadata);
|
|
2031
|
-
return {
|
|
2032
|
-
type,
|
|
2033
|
-
...commonCommitFields(value),
|
|
2034
|
-
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
|
|
2035
|
-
source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
|
|
2036
|
-
sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
|
|
2037
|
-
title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
|
|
2038
|
-
url,
|
|
2039
|
-
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
|
|
2040
|
-
...metadata ? { metadata } : {}
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
case "invokeSkill": {
|
|
2044
|
-
assertExactKeys(
|
|
2045
|
-
value,
|
|
2046
|
-
["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
|
|
2047
|
-
"Factory invoke skill decision"
|
|
2048
|
-
);
|
|
2049
|
-
const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
|
|
2050
|
-
const precedingMessage = optionalBoundedString(
|
|
2051
|
-
value.precedingMessage,
|
|
2052
|
-
"Factory skill preceding message",
|
|
2053
|
-
MAX_MESSAGE_LENGTH
|
|
2054
|
-
);
|
|
2055
|
-
return {
|
|
2056
|
-
type,
|
|
2057
|
-
...commonCommitFields(value),
|
|
2058
|
-
role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
2059
|
-
skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
|
|
2060
|
-
...args ? { arguments: args } : {},
|
|
2061
|
-
...precedingMessage ? { precedingMessage } : {}
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
case "sendMessage": {
|
|
2065
|
-
assertExactKeys(
|
|
2066
|
-
value,
|
|
2067
|
-
["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
|
|
2068
|
-
"Factory send message decision"
|
|
2069
|
-
);
|
|
2070
|
-
const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
|
|
2071
|
-
const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
|
|
2072
|
-
if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
|
|
2073
|
-
throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
|
|
2074
|
-
}
|
|
2075
|
-
return {
|
|
2076
|
-
type,
|
|
2077
|
-
...commonCommitFields(value),
|
|
2078
|
-
role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
2079
|
-
message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
|
|
2080
|
-
...priority ? { priority } : {},
|
|
2081
|
-
...idleBehavior ? { idleBehavior } : {},
|
|
2082
|
-
...value.prepareBinding === true ? { prepareBinding: true } : {}
|
|
2083
|
-
};
|
|
2084
|
-
}
|
|
2085
|
-
case "notify": {
|
|
2086
|
-
assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
|
|
2087
|
-
const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
|
|
2088
|
-
const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
|
|
2089
|
-
return {
|
|
2090
|
-
type,
|
|
2091
|
-
...commonCommitFields(value),
|
|
2092
|
-
title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
|
|
2093
|
-
...body ? { body } : {},
|
|
2094
|
-
...level ? { level } : {}
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
default:
|
|
2098
|
-
throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
function validateFactoryRuleDecisions(values, causalDepth = 0) {
|
|
2102
|
-
if (values.length > MAX_JSON_COLLECTION_SIZE) {
|
|
2103
|
-
throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
|
|
2104
|
-
}
|
|
2105
|
-
const decisions = [];
|
|
2106
|
-
for (const value of values) {
|
|
2107
|
-
const decision = validateFactoryRuleDecision(value, causalDepth);
|
|
2108
|
-
if (decision.type === "reject") {
|
|
2109
|
-
throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
|
|
2110
|
-
}
|
|
2111
|
-
decisions.push(decision);
|
|
2112
|
-
}
|
|
2113
|
-
const keys = decisions.map((decision) => decision.idempotencyKey);
|
|
2114
|
-
if (new Set(keys).size !== keys.length) {
|
|
2115
|
-
throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
|
|
2116
|
-
}
|
|
2117
|
-
return decisions;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
// src/integrations/github/rules.ts
|
|
2121
|
-
var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
|
|
2122
|
-
var RULE_TIMEOUT_MS = 5e3;
|
|
2123
|
-
async function withRuleTimeout(promise) {
|
|
2124
|
-
let timeout;
|
|
2125
|
-
try {
|
|
2126
|
-
return await Promise.race([
|
|
2127
|
-
promise,
|
|
2128
|
-
new Promise((_, reject) => {
|
|
2129
|
-
timeout = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
|
|
2130
|
-
})
|
|
2131
|
-
]);
|
|
2132
|
-
} finally {
|
|
2133
|
-
if (timeout) clearTimeout(timeout);
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
function object(value) {
|
|
2137
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2138
|
-
}
|
|
2139
|
-
function string(value) {
|
|
2140
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2141
|
-
}
|
|
2142
|
-
function number(value) {
|
|
2143
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2144
|
-
}
|
|
2145
|
-
function boolean(value) {
|
|
2146
|
-
return typeof value === "boolean" ? value : void 0;
|
|
2147
|
-
}
|
|
2148
|
-
function eventName(parsed) {
|
|
2149
|
-
const action = string(parsed.payload.action);
|
|
2150
|
-
if (parsed.event === "issues" && action === "opened") return "issueOpened";
|
|
2151
|
-
if (parsed.event === "pull_request" && action === "opened") return "pullRequestOpened";
|
|
2152
|
-
if (parsed.event === "pull_request" && action === "synchronize") return "pullRequestUpdated";
|
|
2153
|
-
if (parsed.event === "pull_request" && action === "closed" && boolean(object(parsed.payload.pull_request)?.merged)) {
|
|
2154
|
-
return "pullRequestMerged";
|
|
2155
|
-
}
|
|
2156
|
-
if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
|
|
2157
|
-
return void 0;
|
|
2158
|
-
}
|
|
2159
|
-
function canonicalSourceKey(kind, itemNumber) {
|
|
2160
|
-
return kind === "issue" ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;
|
|
2161
|
-
}
|
|
2162
|
-
function legacySourceKey(repositoryId, kind, itemNumber) {
|
|
2163
|
-
return `github:${repositoryId}:${kind}:${itemNumber}`;
|
|
2164
|
-
}
|
|
2165
|
-
function provenanceTarget(repositoryId, pullRequestNumber) {
|
|
2166
|
-
return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;
|
|
2167
|
-
}
|
|
2168
|
-
function workItemSource(item) {
|
|
2169
|
-
if (!item.externalSource) return "manual";
|
|
2170
|
-
return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
|
|
2171
|
-
}
|
|
2172
|
-
function workItemSourceKey(item) {
|
|
2173
|
-
return item.externalSource?.externalId ?? null;
|
|
2174
|
-
}
|
|
2175
|
-
async function githubActor(github, input) {
|
|
2176
|
-
let trusted = false;
|
|
2177
|
-
try {
|
|
2178
|
-
const permission = await github.getRepositoryCollaboratorPermission(
|
|
2179
|
-
input.installationId,
|
|
2180
|
-
input.repository,
|
|
2181
|
-
input.login
|
|
2182
|
-
);
|
|
2183
|
-
trusted = permission !== void 0 && TRUSTED_PERMISSIONS.has(permission);
|
|
2184
|
-
} catch {
|
|
2185
|
-
trusted = false;
|
|
2186
|
-
}
|
|
2187
|
-
return { type: "github", login: input.login, trusted, factoryAuthored: input.factoryAuthored };
|
|
2188
|
-
}
|
|
2189
|
-
function pullRequestProvenance(data) {
|
|
2190
|
-
if (!data || data.kind !== "factory-pr-provenance" || typeof data.workItemId !== "string") return null;
|
|
2191
|
-
return { kind: "factory-pr-provenance", workItemId: data.workItemId };
|
|
2192
|
-
}
|
|
2193
|
-
var GithubRules = class {
|
|
2194
|
-
constructor(options) {
|
|
2195
|
-
this.options = options;
|
|
2196
|
-
}
|
|
2197
|
-
options;
|
|
2198
|
-
async ingest(parsed) {
|
|
2199
|
-
const event = eventName(parsed);
|
|
2200
|
-
const repository = object(parsed.payload.repository);
|
|
2201
|
-
const installationId = number(object(parsed.payload.installation)?.id);
|
|
2202
|
-
const repositoryId = number(repository?.id);
|
|
2203
|
-
const repositoryName = string(repository?.full_name);
|
|
2204
|
-
const login = string(object(parsed.payload.sender)?.login);
|
|
2205
|
-
if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
|
|
2206
|
-
const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
|
|
2207
|
-
installationExternalId: String(installationId),
|
|
2208
|
-
repositoryExternalId: String(repositoryId)
|
|
2209
|
-
});
|
|
2210
|
-
if (projects.length === 0) return { status: "ignored" };
|
|
2211
|
-
const results = [];
|
|
2212
|
-
for (const project of projects) {
|
|
2213
|
-
results.push(
|
|
2214
|
-
await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project)
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
if (results.some((result) => result.status === "committed")) return { status: "committed" };
|
|
2218
|
-
if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
|
|
2219
|
-
return results[0] ?? { status: "ignored" };
|
|
2220
|
-
}
|
|
2221
|
-
async #ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) {
|
|
2222
|
-
const factoryProject = await this.options.projects.get({
|
|
2223
|
-
orgId: project.orgId,
|
|
2224
|
-
id: project.factoryProjectId
|
|
2225
|
-
});
|
|
2226
|
-
if (!factoryProject) return { status: "missing" };
|
|
2227
|
-
const issue = object(parsed.payload.issue);
|
|
2228
|
-
const pullRequest = object(parsed.payload.pull_request);
|
|
2229
|
-
const issueNumber = number(issue?.number);
|
|
2230
|
-
const pullRequestNumber = number(pullRequest?.number);
|
|
2231
|
-
const provenance = pullRequestNumber ? pullRequestProvenance(
|
|
2232
|
-
(await this.options.integrationStorage.subscriptions.listByTarget(
|
|
2233
|
-
provenanceTarget(repositoryId, pullRequestNumber),
|
|
2234
|
-
{ status: "active" }
|
|
2235
|
-
)).find((subscription) => subscription.orgId === project.orgId)?.data
|
|
2236
|
-
) : null;
|
|
2237
|
-
const relatedItem = await this.#relatedItem(
|
|
2238
|
-
project.orgId,
|
|
2239
|
-
project.factoryProjectId,
|
|
2240
|
-
repositoryId,
|
|
2241
|
-
issueNumber,
|
|
2242
|
-
pullRequestNumber,
|
|
2243
|
-
string(object(pullRequest?.head)?.ref),
|
|
2244
|
-
provenance
|
|
2245
|
-
);
|
|
2246
|
-
const actor = await githubActor(this.options.github, {
|
|
2247
|
-
installationId,
|
|
2248
|
-
repository: repositoryName,
|
|
2249
|
-
login,
|
|
2250
|
-
factoryAuthored: provenance !== null
|
|
2251
|
-
});
|
|
2252
|
-
const context = {
|
|
2253
|
-
tenant: { orgId: project.orgId, projectId: project.factoryProjectId },
|
|
2254
|
-
actor,
|
|
2255
|
-
ingress: { type: "github", id: `${installationId}:${parsed.deliveryId}` },
|
|
2256
|
-
cause: `github.${event}`,
|
|
2257
|
-
causalChain: [],
|
|
2258
|
-
ruleSetVersion: this.options.rules.version,
|
|
2259
|
-
...relatedItem ? {
|
|
2260
|
-
item: {
|
|
2261
|
-
id: relatedItem.id,
|
|
2262
|
-
source: workItemSource(relatedItem),
|
|
2263
|
-
sourceKey: workItemSourceKey(relatedItem),
|
|
2264
|
-
parentWorkItemId: relatedItem.parentWorkItemId,
|
|
2265
|
-
title: relatedItem.title,
|
|
2266
|
-
url: relatedItem.externalSource?.url ?? null,
|
|
2267
|
-
stages: relatedItem.stages
|
|
2268
|
-
},
|
|
2269
|
-
board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
|
|
2270
|
-
itemRevision: relatedItem.revision
|
|
2271
|
-
} : {},
|
|
2272
|
-
event,
|
|
2273
|
-
deliveryId: parsed.deliveryId,
|
|
2274
|
-
factory: { createdAt: factoryProject.createdAt.toISOString() },
|
|
2275
|
-
repository: { id: repositoryId, fullName: repositoryName },
|
|
2276
|
-
...issueNumber && string(issue?.title) && string(issue?.html_url) ? {
|
|
2277
|
-
issue: {
|
|
2278
|
-
number: issueNumber,
|
|
2279
|
-
title: string(issue?.title),
|
|
2280
|
-
url: string(issue?.html_url),
|
|
2281
|
-
...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}
|
|
2282
|
-
}
|
|
2283
|
-
} : {},
|
|
2284
|
-
...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? {
|
|
2285
|
-
pullRequest: {
|
|
2286
|
-
number: pullRequestNumber,
|
|
2287
|
-
title: string(pullRequest?.title),
|
|
2288
|
-
url: string(pullRequest?.html_url),
|
|
2289
|
-
...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
|
|
2290
|
-
state: string(pullRequest?.state) === "closed" ? "closed" : "open",
|
|
2291
|
-
merged: boolean(pullRequest?.merged) ?? false,
|
|
2292
|
-
headBranch: string(object(pullRequest?.head)?.ref) ?? "",
|
|
2293
|
-
baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
|
|
2294
|
-
}
|
|
2295
|
-
} : {},
|
|
2296
|
-
...object(parsed.payload.review) ? {
|
|
2297
|
-
review: {
|
|
2298
|
-
id: number(object(parsed.payload.review)?.id) ?? 0,
|
|
2299
|
-
state: string(object(parsed.payload.review)?.state) ?? "unknown",
|
|
2300
|
-
url: string(object(parsed.payload.review)?.html_url) ?? ""
|
|
2301
|
-
}
|
|
2302
|
-
} : {}
|
|
2303
|
-
};
|
|
2304
|
-
const rule = resolveFactoryGithubRule(this.options.rules, event);
|
|
2305
|
-
let decision;
|
|
2306
|
-
let decisions = [];
|
|
2307
|
-
let outcome = { status: "accepted" };
|
|
2308
|
-
try {
|
|
2309
|
-
decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
|
|
2310
|
-
if (decision?.type === "reject") {
|
|
2311
|
-
outcome = { status: "rejected", code: decision.code, reason: decision.reason };
|
|
2312
|
-
} else if (decision) {
|
|
2313
|
-
decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
|
|
2314
|
-
}
|
|
2315
|
-
} catch (error) {
|
|
2316
|
-
const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
|
|
2317
|
-
outcome = {
|
|
2318
|
-
status: "rejected",
|
|
2319
|
-
code: timedOut ? "timeout" : "rule_error",
|
|
2320
|
-
reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
|
|
2321
|
-
};
|
|
2322
|
-
}
|
|
2323
|
-
const committed = await this.options.storage.commitRuleEvaluation({
|
|
2324
|
-
orgId: project.orgId,
|
|
2325
|
-
factoryProjectId: project.factoryProjectId,
|
|
2326
|
-
workItemId: relatedItem?.id ?? null,
|
|
2327
|
-
ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },
|
|
2328
|
-
ruleSetVersion: this.options.rules.version,
|
|
2329
|
-
expectedRevision: relatedItem?.revision ?? null,
|
|
2330
|
-
actor: { ...actor },
|
|
2331
|
-
outcome,
|
|
2332
|
-
decisions,
|
|
2333
|
-
causalChain: [],
|
|
2334
|
-
now: /* @__PURE__ */ new Date()
|
|
2335
|
-
});
|
|
2336
|
-
return { status: committed.status };
|
|
2337
|
-
}
|
|
2338
|
-
async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
|
|
2339
|
-
const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
|
|
2340
|
-
if (provenance) return items.find((item) => item.id === provenance.workItemId);
|
|
2341
|
-
if (issueNumber) {
|
|
2342
|
-
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
|
|
2343
|
-
}
|
|
2344
|
-
if (pullRequestNumber) {
|
|
2345
|
-
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
|
|
2346
|
-
(item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
|
|
2347
|
-
) ?? // Provenance fallback: a PR pushed from a work item's session branch
|
|
2348
|
-
// belongs to that item even when no gh-pr-create provenance was
|
|
2349
|
-
// recorded (session predating state seeding, or the PR was opened
|
|
2350
|
-
// outside the tracked tool call). Session branches are per-item
|
|
2351
|
-
// (`factory/issue-N`), so a head-branch match is unambiguous.
|
|
2352
|
-
(pullRequestHeadBranch ? items.find(
|
|
2353
|
-
(item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
|
|
2354
|
-
) : void 0);
|
|
2355
|
-
}
|
|
2356
|
-
return void 0;
|
|
2357
|
-
}
|
|
2358
|
-
};
|
|
2359
|
-
function attachGithubRules(github, context) {
|
|
2360
|
-
if (!context.rules) return void 0;
|
|
2361
|
-
const rules = new GithubRules({
|
|
2362
|
-
github,
|
|
2363
|
-
sourceControl: context.storage.sourceControl,
|
|
2364
|
-
integrationStorage: context.storage.generic,
|
|
2365
|
-
projects: context.storage.projects,
|
|
2366
|
-
storage: context.rules.workItems,
|
|
2367
|
-
rules: context.rules.config
|
|
2368
|
-
});
|
|
2369
|
-
return (event) => rules.ingest(event);
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
// src/integrations/github/session-subscriptions.ts
|
|
2373
|
-
import { createTool } from "@mastra/core/tools";
|
|
2374
|
-
import { z } from "zod";
|
|
2375
|
-
|
|
2376
|
-
// src/integrations/github/token-refresh.ts
|
|
2377
|
-
var GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = "factoryGithubTokenInjector";
|
|
2378
|
-
var GITHUB_PAT_KIND_CONTEXT_KEY = "factoryGithubPatKind";
|
|
2379
|
-
function getRegisteredGithubPatKind(requestContext) {
|
|
2380
|
-
const kind = requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY);
|
|
2381
|
-
return kind === "reviewer" ? "reviewer" : "default";
|
|
2382
|
-
}
|
|
2383
|
-
function injectGithubToken(requestContext, token) {
|
|
2384
|
-
const injector = requestContext.get(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY);
|
|
2385
|
-
if (!injector) {
|
|
2386
|
-
throw new Error("GitHub token refresh requires an active Factory sandbox workspace.");
|
|
2387
|
-
}
|
|
2388
|
-
injector(token);
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
// src/integrations/github/session-subscriptions.ts
|
|
2392
|
-
function sessionUserId(user) {
|
|
2393
|
-
return user?.workosId ?? user?.id;
|
|
2394
|
-
}
|
|
2395
|
-
function sessionOrgId(user) {
|
|
2396
|
-
return user?.organizationId;
|
|
2397
|
-
}
|
|
2398
|
-
var pullRequestInputSchema = z.object({
|
|
2399
|
-
pullRequest: z.union([z.number().int().positive(), z.string().min(1)])
|
|
2400
|
-
});
|
|
2401
|
-
function parsePullRequest(value, expectedRepo) {
|
|
2402
|
-
if (typeof value === "number") return value;
|
|
2403
|
-
if (/^\d+$/.test(value)) return Number(value);
|
|
2404
|
-
const match = value.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)\/?$/i);
|
|
2405
|
-
if (!match || match[1].toLowerCase() !== expectedRepo.toLowerCase()) {
|
|
2406
|
-
throw new Error(`Pull request must belong to ${expectedRepo}.`);
|
|
2407
|
-
}
|
|
2408
|
-
return Number(match[2]);
|
|
2409
|
-
}
|
|
2410
|
-
function isGithubProjectSession(requestContext) {
|
|
2411
|
-
const context = requestContext.get("controller");
|
|
2412
|
-
const user = requestContext.get("user");
|
|
2413
|
-
return Boolean(
|
|
2414
|
-
context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user)
|
|
2415
|
-
);
|
|
2416
|
-
}
|
|
2417
|
-
async function resolveSessionTarget(requestContext, github) {
|
|
2418
|
-
const context = requestContext.get("controller");
|
|
2419
|
-
const user = requestContext.get("user");
|
|
2420
|
-
const orgId = sessionOrgId(user);
|
|
2421
|
-
const userId = sessionUserId(user);
|
|
2422
|
-
const projectRepositoryId = context?.getState().projectRepositoryId;
|
|
2423
|
-
if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {
|
|
2424
|
-
throw new Error("GitHub subscriptions require an authenticated repository session with an active thread.");
|
|
2425
|
-
}
|
|
2426
|
-
const projectRepository = await github.sourceControlStorage.projectRepositories.get({
|
|
2427
|
-
orgId,
|
|
2428
|
-
id: projectRepositoryId
|
|
2429
|
-
});
|
|
2430
|
-
if (!projectRepository) throw new Error("Project repository not found for this organization.");
|
|
2431
|
-
const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });
|
|
2432
|
-
if (!connection) throw new Error("Source-control connection not found for this organization.");
|
|
2433
|
-
const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });
|
|
2434
|
-
if (!repository) throw new Error("Repository not found for this organization.");
|
|
2435
|
-
const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });
|
|
2436
|
-
if (!installation) throw new Error("Source-control installation not found for this organization.");
|
|
2437
|
-
return { context, projectRepository, connection, installation, repository, orgId, userId };
|
|
2438
|
-
}
|
|
2439
|
-
async function verifyPullRequest(target, pullRequest, github) {
|
|
2440
|
-
const [owner, repo] = target.repository.slug.split("/");
|
|
2441
|
-
if (!owner || !repo) throw new Error("GitHub repository is invalid.");
|
|
2442
|
-
const octokit = github.getInstallationOctokit(Number(target.installation.externalId));
|
|
2443
|
-
const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });
|
|
2444
|
-
if (String(data.base.repo.id) !== target.repository.externalId)
|
|
2445
|
-
throw new Error("Pull request repository does not match the active project repository.");
|
|
2446
|
-
}
|
|
2447
|
-
async function subscriptionInput(target, pullRequestNumber) {
|
|
2448
|
-
return {
|
|
2449
|
-
orgId: target.orgId,
|
|
2450
|
-
installationExternalId: target.installation.externalId,
|
|
2451
|
-
projectRepositoryId: target.projectRepository.id,
|
|
2452
|
-
repositoryExternalId: target.repository.externalId,
|
|
2453
|
-
repositorySlug: target.repository.slug,
|
|
2454
|
-
changeRequestId: String(pullRequestNumber),
|
|
2455
|
-
sessionId: target.context.session.id,
|
|
2456
|
-
ownerId: target.context.session.ownerId,
|
|
2457
|
-
resourceId: target.connection.factoryProjectId,
|
|
2458
|
-
threadId: target.context.threadId,
|
|
2459
|
-
sessionScope: target.context.scope,
|
|
2460
|
-
source: "explicit-tool",
|
|
2461
|
-
subscribedByUserId: target.userId
|
|
2462
|
-
};
|
|
2463
|
-
}
|
|
2464
|
-
async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
|
|
2465
|
-
if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
|
|
2466
|
-
const target = await resolveSessionTarget(requestContext, github);
|
|
2467
|
-
const number2 = parsePullRequest(pullRequest, target.repository.slug);
|
|
2468
|
-
await verifyPullRequest(target, number2, github);
|
|
2469
|
-
await subscribeToPullRequest({ ...await subscriptionInput(target, number2), source }, github.integrationStorage);
|
|
2470
|
-
return number2;
|
|
2471
|
-
}
|
|
2472
|
-
async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
|
|
2473
|
-
const target = await resolveSessionTarget(requestContext, github);
|
|
2474
|
-
const number2 = parsePullRequest(pullRequest, target.repository.slug);
|
|
2475
|
-
await unsubscribeFromPullRequest(await subscriptionInput(target, number2), github.integrationStorage);
|
|
2476
|
-
return number2;
|
|
2477
|
-
}
|
|
2478
|
-
async function refreshGithubToken(requestContext, github) {
|
|
2479
|
-
const target = await resolveSessionTarget(requestContext, github);
|
|
2480
|
-
const pat = await getGithubPat(
|
|
2481
|
-
() => github.integrationStorage,
|
|
2482
|
-
target.orgId,
|
|
2483
|
-
getRegisteredGithubPatKind(requestContext)
|
|
2484
|
-
);
|
|
2485
|
-
if (pat) {
|
|
2486
|
-
injectGithubToken(requestContext, pat);
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
const access = await github.versionControl.getRepositoryAccess({
|
|
2490
|
-
orgId: target.orgId,
|
|
2491
|
-
repositoryId: target.repository.id
|
|
2492
|
-
});
|
|
2493
|
-
const token = access.authorization?.token;
|
|
2494
|
-
if (!token) throw new Error("Repository access did not include a bearer token for the Factory session.");
|
|
2495
|
-
injectGithubToken(requestContext, token);
|
|
2496
|
-
}
|
|
2497
|
-
function createGithubSubscriptionTools(requestContext, github) {
|
|
2498
|
-
const context = requestContext.get("controller");
|
|
2499
|
-
const user = requestContext.get("user");
|
|
2500
|
-
if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};
|
|
2501
|
-
return {
|
|
2502
|
-
github_refresh_token: createTool({
|
|
2503
|
-
id: "github_refresh_token",
|
|
2504
|
-
description: "Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.",
|
|
2505
|
-
inputSchema: z.object({}),
|
|
2506
|
-
execute: async () => {
|
|
2507
|
-
await refreshGithubToken(requestContext, github);
|
|
2508
|
-
return { refreshed: true };
|
|
2509
|
-
}
|
|
2510
|
-
}),
|
|
2511
|
-
github_subscribe_pr: createTool({
|
|
2512
|
-
id: "github_subscribe_pr",
|
|
2513
|
-
description: "Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.",
|
|
2514
|
-
inputSchema: pullRequestInputSchema,
|
|
2515
|
-
execute: async ({ pullRequest }) => {
|
|
2516
|
-
const number2 = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
|
|
2517
|
-
return { subscribed: true, pullRequestNumber: number2 };
|
|
2518
|
-
}
|
|
2519
|
-
}),
|
|
2520
|
-
github_unsubscribe_pr: createTool({
|
|
2521
|
-
id: "github_unsubscribe_pr",
|
|
2522
|
-
description: "Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.",
|
|
2523
|
-
inputSchema: pullRequestInputSchema,
|
|
2524
|
-
execute: async ({ pullRequest }) => {
|
|
2525
|
-
const number2 = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
|
|
2526
|
-
return { subscribed: false, pullRequestNumber: number2 };
|
|
2527
|
-
}
|
|
2528
|
-
})
|
|
2529
|
-
};
|
|
2530
|
-
}
|
|
2531
|
-
function stripHeredocBodies(command) {
|
|
2532
|
-
const lines = command.split("\n");
|
|
2533
|
-
const executableLines = [];
|
|
2534
|
-
let delimiter;
|
|
2535
|
-
for (const line of lines) {
|
|
2536
|
-
if (delimiter) {
|
|
2537
|
-
if (line.trim() === delimiter) delimiter = void 0;
|
|
2538
|
-
continue;
|
|
2539
|
-
}
|
|
2540
|
-
executableLines.push(line);
|
|
2541
|
-
const heredoc = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);
|
|
2542
|
-
delimiter = heredoc?.[2];
|
|
2543
|
-
}
|
|
2544
|
-
return executableLines.join("\n");
|
|
2545
|
-
}
|
|
2546
|
-
function parseCreatedPullRequest(context) {
|
|
2547
|
-
if (context.toolName !== "execute_command" || context.error) return void 0;
|
|
2548
|
-
const command = context.input?.command;
|
|
2549
|
-
if (typeof command !== "string" || !/(?:^|\n|;|&&|\|\|)\s*gh\s+pr\s+create(?:\s|$)/.test(stripHeredocBodies(command))) {
|
|
2550
|
-
return void 0;
|
|
2551
|
-
}
|
|
2552
|
-
const output = context.output;
|
|
2553
|
-
const stdout = typeof context.output === "string" ? context.output : output?.stdout ?? output?.result;
|
|
2554
|
-
if (typeof stdout !== "string") return void 0;
|
|
2555
|
-
const urls = stdout.match(/https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g) ?? [];
|
|
2556
|
-
return urls.length === 1 ? urls[0] : void 0;
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
// src/integrations/platform/api-client.ts
|
|
2560
|
-
function platformApiClientConfigFromEnv() {
|
|
2561
|
-
const sharedApiUrl = process.env.MASTRA_SHARED_API_URL?.trim() || "https://platform.mastra.ai/v1";
|
|
2562
|
-
const accessToken = process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();
|
|
2563
|
-
if (!accessToken) {
|
|
2564
|
-
throw new Error("Platform integration: missing required environment variable MASTRA_PLATFORM_SECRET_KEY.");
|
|
2565
|
-
}
|
|
2566
|
-
return { baseUrl: normalizeSharedApiUrl(sharedApiUrl), accessToken };
|
|
2567
|
-
}
|
|
2568
|
-
function normalizeSharedApiUrl(sharedApiUrl) {
|
|
2569
|
-
return sharedApiUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
2570
|
-
}
|
|
2571
|
-
var PlatformApiError = class extends Error {
|
|
2572
|
-
status;
|
|
2573
|
-
retryAfterSeconds;
|
|
2574
|
-
constructor(message, status, retryAfterSeconds = null) {
|
|
2575
|
-
super(message);
|
|
2576
|
-
this.name = "PlatformApiError";
|
|
2577
|
-
this.status = status;
|
|
2578
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
2579
|
-
}
|
|
2580
|
-
};
|
|
2581
|
-
var PlatformApiClient = class {
|
|
2582
|
-
#baseUrl;
|
|
2583
|
-
#accessToken;
|
|
2584
|
-
#fetch;
|
|
2585
|
-
constructor(config) {
|
|
2586
|
-
const missing = ["baseUrl", "accessToken"].filter((field) => !config[field]);
|
|
2587
|
-
if (missing.length > 0) {
|
|
2588
|
-
throw new Error(`Platform integration: missing required config field(s): ${missing.join(", ")}.`);
|
|
2589
|
-
}
|
|
2590
|
-
this.#baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2591
|
-
this.#accessToken = config.accessToken;
|
|
2592
|
-
this.#fetch = config.fetchImpl ?? globalThis.fetch;
|
|
2593
|
-
}
|
|
2594
|
-
async request(method, path2, body, options) {
|
|
2595
|
-
const response = await this.#send(method, path2, body, options);
|
|
2596
|
-
if (!response.ok) {
|
|
2597
|
-
const message = redact(await extractError(response), this.#accessToken);
|
|
2598
|
-
const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
|
|
2599
|
-
logPlatformError("Platform API request failed", {
|
|
2600
|
-
method,
|
|
2601
|
-
path: path2,
|
|
2602
|
-
status: response.status,
|
|
2603
|
-
retryAfterSeconds,
|
|
2604
|
-
message
|
|
2605
|
-
});
|
|
2606
|
-
throw new PlatformApiError(message, response.status, retryAfterSeconds);
|
|
2607
|
-
}
|
|
2608
|
-
if (response.status === 204) return void 0;
|
|
2609
|
-
return await response.json();
|
|
2610
|
-
}
|
|
2611
|
-
async requestRedirect(method, path2, options) {
|
|
2612
|
-
const response = await this.#send(method, path2, void 0, options, "manual");
|
|
2613
|
-
if (response.status >= 300 && response.status < 400) {
|
|
2614
|
-
const location = response.headers.get("location");
|
|
2615
|
-
if (location) return location;
|
|
2616
|
-
}
|
|
2617
|
-
if (!response.ok) {
|
|
2618
|
-
const message = redact(await extractError(response), this.#accessToken);
|
|
2619
|
-
const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
|
|
2620
|
-
logPlatformError("Platform API redirect request failed", {
|
|
2621
|
-
method,
|
|
2622
|
-
path: path2,
|
|
2623
|
-
status: response.status,
|
|
2624
|
-
retryAfterSeconds,
|
|
2625
|
-
message
|
|
2626
|
-
});
|
|
2627
|
-
throw new PlatformApiError(message, response.status, retryAfterSeconds);
|
|
2628
|
-
}
|
|
2629
|
-
logPlatformError("Platform API request did not return a redirect", { method, path: path2, status: response.status });
|
|
2630
|
-
throw new PlatformApiError("Platform API request did not return a redirect.", response.status);
|
|
2631
|
-
}
|
|
2632
|
-
async #send(method, path2, body, options, redirect) {
|
|
2633
|
-
const headers = {
|
|
2634
|
-
accept: "application/json",
|
|
2635
|
-
authorization: `Bearer ${this.#accessToken}`
|
|
2636
|
-
};
|
|
2637
|
-
if (options?.actingUserId) {
|
|
2638
|
-
headers["x-acting-user-id"] = options.actingUserId;
|
|
2639
|
-
}
|
|
2640
|
-
const timeoutSignal = AbortSignal.timeout(15e3);
|
|
2641
|
-
const init = {
|
|
2642
|
-
method,
|
|
2643
|
-
headers,
|
|
2644
|
-
redirect,
|
|
2645
|
-
signal: options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal
|
|
2646
|
-
};
|
|
2647
|
-
if (body !== void 0) {
|
|
2648
|
-
headers["content-type"] = "application/json";
|
|
2649
|
-
init.body = JSON.stringify(body);
|
|
2650
|
-
}
|
|
2651
|
-
try {
|
|
2652
|
-
return await this.#fetch(`${this.#baseUrl}${path2}`, init);
|
|
2653
|
-
} catch (error) {
|
|
2654
|
-
if (error instanceof Error && error.message.includes(this.#accessToken)) {
|
|
2655
|
-
const redacted = new Error(redact(error.message, this.#accessToken));
|
|
2656
|
-
redacted.name = error.name;
|
|
2657
|
-
logPlatformError("Platform API transport error", {
|
|
2658
|
-
method,
|
|
2659
|
-
path: path2,
|
|
2660
|
-
name: redacted.name,
|
|
2661
|
-
message: redacted.message
|
|
2662
|
-
});
|
|
2663
|
-
throw redacted;
|
|
2664
|
-
}
|
|
2665
|
-
logPlatformError("Platform API transport error", {
|
|
2666
|
-
method,
|
|
2667
|
-
path: path2,
|
|
2668
|
-
name: error instanceof Error ? error.name : void 0,
|
|
2669
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2670
|
-
});
|
|
2671
|
-
throw error;
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
};
|
|
2675
|
-
async function extractError(response) {
|
|
2676
|
-
try {
|
|
2677
|
-
const data = await response.clone().json();
|
|
2678
|
-
for (const field of ["detail", "error", "title"]) {
|
|
2679
|
-
if (typeof data[field] === "string" && data[field]) return data[field];
|
|
2680
|
-
}
|
|
2681
|
-
} catch {
|
|
2682
|
-
}
|
|
2683
|
-
return `Platform API request failed (${response.status})`;
|
|
2684
|
-
}
|
|
2685
|
-
function redact(message, accessToken) {
|
|
2686
|
-
return message.split(accessToken).join("[REDACTED]");
|
|
2687
|
-
}
|
|
2688
|
-
function logPlatformInfo(message, fields) {
|
|
2689
|
-
writePlatformLog("info", message, fields);
|
|
2690
|
-
}
|
|
2691
|
-
function logPlatformWarn(message, fields) {
|
|
2692
|
-
writePlatformLog("warn", message, fields);
|
|
2693
|
-
}
|
|
2694
|
-
function logPlatformError(message, fields) {
|
|
2695
|
-
writePlatformLog("error", message, fields);
|
|
2696
|
-
}
|
|
2697
|
-
function writePlatformLog(level, message, fields) {
|
|
2698
|
-
const metadata = fields ? ` ${JSON.stringify(stripUndefined(fields))}` : "";
|
|
2699
|
-
process.stderr.write(`[Mastra Factory] ${level.toUpperCase()} ${message}${metadata}
|
|
2700
|
-
`);
|
|
2701
|
-
}
|
|
2702
|
-
function stripUndefined(fields) {
|
|
2703
|
-
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
|
|
2704
|
-
}
|
|
2705
|
-
function parseRetryAfter(value) {
|
|
2706
|
-
if (!value) return null;
|
|
2707
|
-
const seconds = Number.parseInt(value, 10);
|
|
2708
|
-
return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
// src/integrations/platform/github/event-worker.ts
|
|
2712
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2713
|
-
import { isLeaseProvider, NoopLeaseProvider } from "@mastra/core/events";
|
|
2714
|
-
import { MastraWorker } from "@mastra/core/worker";
|
|
2715
|
-
var API_PREFIX = "/v1/server/github-app";
|
|
2716
|
-
var DEFAULT_POLL_INTERVAL_MS = 2e4;
|
|
2717
|
-
var EVENT_PAGE_SIZE = 500;
|
|
2718
|
-
var MIN_LEASE_TTL_MS = 3e4;
|
|
2719
|
-
var CURSOR_ORG_ID = "__platform_github_event_worker__";
|
|
2720
|
-
var CURSOR_USER_ID = "worker";
|
|
2721
|
-
var SUPPORTED_EVENTS = /* @__PURE__ */ new Set([
|
|
2722
|
-
"issues",
|
|
2723
|
-
"issue_comment",
|
|
2724
|
-
"pull_request",
|
|
2725
|
-
"pull_request_review",
|
|
2726
|
-
"pull_request_review_comment"
|
|
2727
|
-
]);
|
|
2728
|
-
var AUTHOR_GATED_KINDS2 = /* @__PURE__ */ new Set([
|
|
2729
|
-
"issue-comment",
|
|
2730
|
-
"pull-request-comment",
|
|
2731
|
-
"pull-request-review",
|
|
2732
|
-
"pull-request-review-comment"
|
|
2733
|
-
]);
|
|
2734
|
-
var AUTHORIZED_BOTS2 = /* @__PURE__ */ new Set(["coderabbitai[bot]", "devin-ai-integration[bot]"]);
|
|
2735
|
-
var AUTHORIZED_PERMISSIONS2 = /* @__PURE__ */ new Set(["admin", "maintain", "write"]);
|
|
2736
|
-
var PERMISSION_CHECK_TIMEOUT_MS2 = 5e3;
|
|
2737
|
-
var PlatformGithubEventWorker = class extends MastraWorker {
|
|
2738
|
-
name = "platform-github-events";
|
|
2739
|
-
#client;
|
|
2740
|
-
#controller;
|
|
2741
|
-
#github;
|
|
2742
|
-
#storage;
|
|
2743
|
-
#ingestFactoryEvent;
|
|
2744
|
-
#intervalMs;
|
|
2745
|
-
#now;
|
|
2746
|
-
#dispatch;
|
|
2747
|
-
#leaseOwner = randomUUID2();
|
|
2748
|
-
#running = false;
|
|
2749
|
-
#timer;
|
|
2750
|
-
#leaseRenewalTimer;
|
|
2751
|
-
#inFlight;
|
|
2752
|
-
#leaseProvider = NoopLeaseProvider;
|
|
2753
|
-
#leaseTtlMs;
|
|
2754
|
-
#hasLease = false;
|
|
2755
|
-
#startedAt = 0;
|
|
2756
|
-
#settings = { version: 1, repositories: {} };
|
|
2757
|
-
constructor(config) {
|
|
2758
|
-
super();
|
|
2759
|
-
this.#client = config.client;
|
|
2760
|
-
this.#controller = config.controller;
|
|
2761
|
-
this.#github = config.github;
|
|
2762
|
-
this.#storage = config.storage;
|
|
2763
|
-
this.#ingestFactoryEvent = config.ingestFactoryEvent;
|
|
2764
|
-
this.#intervalMs = config.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
2765
|
-
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {
|
|
2766
|
-
throw new Error("Platform GitHub event polling interval must be a positive number.");
|
|
2767
|
-
}
|
|
2768
|
-
this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);
|
|
2769
|
-
this.#now = config.now ?? Date.now;
|
|
2770
|
-
this.#dispatch = config.dispatch ?? dispatchGithubWebhook;
|
|
2771
|
-
}
|
|
2772
|
-
async init(deps) {
|
|
2773
|
-
await super.init(deps);
|
|
2774
|
-
this.#leaseProvider = getLeaseProvider(deps.pubsub);
|
|
2775
|
-
}
|
|
2776
|
-
async start() {
|
|
2777
|
-
if (this.#running) return;
|
|
2778
|
-
if (!this.deps) throw new Error("PlatformGithubEventWorker: call init() before start()");
|
|
2779
|
-
this.#startedAt = this.#now() - 1;
|
|
2780
|
-
this.#settings = normalizeSettings(await this.#storage.settings.get(CURSOR_ORG_ID, CURSOR_USER_ID));
|
|
2781
|
-
this.#running = true;
|
|
2782
|
-
this.deps.logger.info("Platform GitHub event polling started", {
|
|
2783
|
-
intervalMs: this.#intervalMs,
|
|
2784
|
-
leaseTtlMs: this.#leaseTtlMs
|
|
2785
|
-
});
|
|
2786
|
-
this.#schedule(0);
|
|
2787
|
-
}
|
|
2788
|
-
async stop() {
|
|
2789
|
-
if (!this.#running) return;
|
|
2790
|
-
this.#running = false;
|
|
2791
|
-
if (this.#timer) clearTimeout(this.#timer);
|
|
2792
|
-
this.#timer = void 0;
|
|
2793
|
-
this.#stopLeaseRenewal();
|
|
2794
|
-
await this.#inFlight;
|
|
2795
|
-
if (this.#hasLease) {
|
|
2796
|
-
await this.#leaseProvider.releaseLease(this.#leaseKey(), this.#leaseOwner).catch(() => void 0);
|
|
2797
|
-
this.#hasLease = false;
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
get isRunning() {
|
|
2801
|
-
return this.#running;
|
|
2802
|
-
}
|
|
2803
|
-
#schedule(delayMs) {
|
|
2804
|
-
if (!this.#running) return;
|
|
2805
|
-
this.#timer = setTimeout(() => {
|
|
2806
|
-
this.#timer = void 0;
|
|
2807
|
-
const run = this.#tick();
|
|
2808
|
-
this.#inFlight = run;
|
|
2809
|
-
void run.finally(() => {
|
|
2810
|
-
if (this.#inFlight === run) this.#inFlight = void 0;
|
|
2811
|
-
});
|
|
2812
|
-
}, delayMs);
|
|
2813
|
-
this.#timer.unref?.();
|
|
2814
|
-
}
|
|
2815
|
-
async #tick() {
|
|
2816
|
-
let nextDelay = this.#intervalMs;
|
|
2817
|
-
try {
|
|
2818
|
-
if (!await this.#ensureLease()) return;
|
|
2819
|
-
nextDelay = await this.#poll();
|
|
2820
|
-
} catch (error) {
|
|
2821
|
-
nextDelay = retryDelay(error, this.#intervalMs);
|
|
2822
|
-
this.deps?.logger.error("Platform GitHub event polling cycle failed", {
|
|
2823
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2824
|
-
retryInMs: nextDelay
|
|
2825
|
-
});
|
|
2826
|
-
} finally {
|
|
2827
|
-
this.#schedule(nextDelay);
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
async #ensureLease() {
|
|
2831
|
-
if (this.#hasLease) return true;
|
|
2832
|
-
const result = await this.#leaseProvider.acquireLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs);
|
|
2833
|
-
this.#hasLease = result.acquired;
|
|
2834
|
-
if (this.#hasLease) this.#startLeaseRenewal();
|
|
2835
|
-
return this.#hasLease;
|
|
2836
|
-
}
|
|
2837
|
-
#startLeaseRenewal() {
|
|
2838
|
-
if (this.#leaseRenewalTimer) return;
|
|
2839
|
-
this.#leaseRenewalTimer = setInterval(
|
|
2840
|
-
() => {
|
|
2841
|
-
void this.#leaseProvider.renewLease(this.#leaseKey(), this.#leaseOwner, this.#leaseTtlMs).then((renewed) => {
|
|
2842
|
-
if (!renewed) {
|
|
2843
|
-
this.#hasLease = false;
|
|
2844
|
-
this.#stopLeaseRenewal();
|
|
2845
|
-
}
|
|
2846
|
-
}).catch((error) => {
|
|
2847
|
-
this.#hasLease = false;
|
|
2848
|
-
this.#stopLeaseRenewal();
|
|
2849
|
-
this.deps?.logger.warn("Platform GitHub event polling lease renewal failed", {
|
|
2850
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2851
|
-
});
|
|
2852
|
-
});
|
|
2853
|
-
},
|
|
2854
|
-
Math.floor(this.#leaseTtlMs / 3)
|
|
2855
|
-
);
|
|
2856
|
-
this.#leaseRenewalTimer.unref?.();
|
|
2857
|
-
}
|
|
2858
|
-
#stopLeaseRenewal() {
|
|
2859
|
-
if (this.#leaseRenewalTimer) clearInterval(this.#leaseRenewalTimer);
|
|
2860
|
-
this.#leaseRenewalTimer = void 0;
|
|
2861
|
-
}
|
|
2862
|
-
async #poll() {
|
|
2863
|
-
const repositories = await this.#discoverRepositories();
|
|
2864
|
-
let retryInMs = this.#intervalMs;
|
|
2865
|
-
for (const repository of repositories) {
|
|
2866
|
-
if (!this.#running || !this.#hasLease) break;
|
|
2867
|
-
try {
|
|
2868
|
-
await this.#pollRepository(repository.id);
|
|
2869
|
-
} catch (error) {
|
|
2870
|
-
const delay = retryDelay(error, this.#intervalMs);
|
|
2871
|
-
retryInMs = Math.max(retryInMs, delay);
|
|
2872
|
-
this.deps?.logger.error("Platform GitHub repository event polling failed", {
|
|
2873
|
-
repositoryId: repository.id,
|
|
2874
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2875
|
-
retryInMs: delay
|
|
2876
|
-
});
|
|
2877
|
-
if (error instanceof PlatformApiError && error.status === 429) break;
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
return retryInMs;
|
|
2881
|
-
}
|
|
2882
|
-
async #discoverRepositories() {
|
|
2883
|
-
const result = await this.#client.request("GET", `${API_PREFIX}/installations`);
|
|
2884
|
-
const repositories = /* @__PURE__ */ new Map();
|
|
2885
|
-
for (const installation of result.installations) {
|
|
2886
|
-
if (!installation.usable || installation.suspendedAt) continue;
|
|
2887
|
-
const page = await this.#client.request(
|
|
2888
|
-
"GET",
|
|
2889
|
-
`${API_PREFIX}/installations/${installation.installationId}/repositories`
|
|
2890
|
-
);
|
|
2891
|
-
for (const repository of page.repositories) repositories.set(repository.id, repository);
|
|
2892
|
-
}
|
|
2893
|
-
return [...repositories.values()];
|
|
2894
|
-
}
|
|
2895
|
-
async #pollRepository(repositoryId) {
|
|
2896
|
-
const key = String(repositoryId);
|
|
2897
|
-
if (!this.#settings.repositories[key]) {
|
|
2898
|
-
this.#settings.repositories[key] = { afterTimestamp: this.#startedAt };
|
|
2899
|
-
await this.#saveSettings();
|
|
2900
|
-
}
|
|
2901
|
-
while (this.#running && this.#hasLease) {
|
|
2902
|
-
const cursor = this.#settings.repositories[key];
|
|
2903
|
-
const query = new URLSearchParams({ limit: String(EVENT_PAGE_SIZE) });
|
|
2904
|
-
if ("afterEventId" in cursor) query.set("afterEventId", cursor.afterEventId);
|
|
2905
|
-
else query.set("afterTimestamp", String(cursor.afterTimestamp));
|
|
2906
|
-
const pollStartedAt = performance.now();
|
|
2907
|
-
const page = await this.#client.request(
|
|
2908
|
-
"GET",
|
|
2909
|
-
`${API_PREFIX}/repositories/${repositoryId}/events?${query}`
|
|
2910
|
-
);
|
|
2911
|
-
this.deps?.logger.info("Platform GitHub repository event poll completed", {
|
|
2912
|
-
repositoryId,
|
|
2913
|
-
eventCount: page.events.length,
|
|
2914
|
-
latencyMs: Math.round(performance.now() - pollStartedAt)
|
|
2915
|
-
});
|
|
2916
|
-
if (page.events.length === 0 || !page.nextCursor) return;
|
|
2917
|
-
for (const event of page.events) {
|
|
2918
|
-
if (!this.#running || !this.#hasLease) return;
|
|
2919
|
-
const parsed = parseEvent(event);
|
|
2920
|
-
if (!parsed) {
|
|
2921
|
-
this.deps?.logger.warn("Platform GitHub event log returned a malformed event", {
|
|
2922
|
-
repositoryId,
|
|
2923
|
-
eventId: event.id
|
|
2924
|
-
});
|
|
2925
|
-
continue;
|
|
2926
|
-
}
|
|
2927
|
-
if (isFactoryClosureEvent(parsed)) {
|
|
2928
|
-
await this.#ingestFactoryEvent?.(parsed);
|
|
2929
|
-
}
|
|
2930
|
-
const result = await this.#dispatch(parsed, {
|
|
2931
|
-
controller: this.#controller,
|
|
2932
|
-
listSubscriptions: (target, options) => listPullRequestSubscriptionsForWebhook(target, options, this.#github.integrationStorage),
|
|
2933
|
-
retireSubscription: (id, status) => retirePullRequestSubscription(id, status, this.#github.integrationStorage),
|
|
2934
|
-
isAuthorizedSender: (notification) => this.#isAuthorizedSender(notification),
|
|
2935
|
-
onTargetError: (subscription, error) => {
|
|
2936
|
-
this.deps?.logger.error("Platform GitHub event delivery failed for a subscription", {
|
|
2937
|
-
subscriptionId: subscription.id,
|
|
2938
|
-
resourceId: subscription.resourceId,
|
|
2939
|
-
threadId: subscription.threadId,
|
|
2940
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
|
-
});
|
|
2944
|
-
if (result.failed > 0) {
|
|
2945
|
-
throw new Error(
|
|
2946
|
-
`Platform GitHub event ${event.deliveryId} failed for ${result.failed} subscribed target(s).`
|
|
2947
|
-
);
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
if (page.nextCursor === ("afterEventId" in cursor ? cursor.afterEventId : void 0)) return;
|
|
2951
|
-
this.#settings.repositories[key] = { afterEventId: page.nextCursor };
|
|
2952
|
-
await this.#saveSettings();
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
async #isAuthorizedSender(notification) {
|
|
2956
|
-
if (!AUTHOR_GATED_KINDS2.has(notification.kind)) return true;
|
|
2957
|
-
const sender = notification.metadata.sender;
|
|
2958
|
-
const repository = notification.metadata.repository;
|
|
2959
|
-
if (!sender || !repository) return false;
|
|
2960
|
-
if (AUTHORIZED_BOTS2.has(sender)) return true;
|
|
2961
|
-
const abortController = new AbortController();
|
|
2962
|
-
const timeout = setTimeout(() => abortController.abort(), PERMISSION_CHECK_TIMEOUT_MS2);
|
|
2963
|
-
try {
|
|
2964
|
-
const permission = await this.#github.getRepositoryCollaboratorPermission(
|
|
2965
|
-
notification.metadata.installationId,
|
|
2966
|
-
repository,
|
|
2967
|
-
sender,
|
|
2968
|
-
abortController.signal
|
|
2969
|
-
);
|
|
2970
|
-
return permission !== void 0 && AUTHORIZED_PERMISSIONS2.has(permission);
|
|
2971
|
-
} catch {
|
|
2972
|
-
return false;
|
|
2973
|
-
} finally {
|
|
2974
|
-
clearTimeout(timeout);
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
async #saveSettings() {
|
|
2978
|
-
await this.#storage.settings.save(CURSOR_ORG_ID, CURSOR_USER_ID, this.#settings);
|
|
2979
|
-
}
|
|
2980
|
-
#leaseKey() {
|
|
2981
|
-
return `${this.name}:${this.#storage.integrationId}`;
|
|
2982
|
-
}
|
|
2983
|
-
};
|
|
2984
|
-
function getLeaseProvider(pubsub) {
|
|
2985
|
-
const getProvider = pubsub.getLeaseProvider;
|
|
2986
|
-
if (typeof getProvider === "function") return getProvider.call(pubsub) ?? NoopLeaseProvider;
|
|
2987
|
-
return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;
|
|
2988
|
-
}
|
|
2989
|
-
function normalizeSettings(value) {
|
|
2990
|
-
if (!value || value.version !== 1 || !value.repositories || typeof value.repositories !== "object") {
|
|
2991
|
-
return { version: 1, repositories: {} };
|
|
2992
|
-
}
|
|
2993
|
-
return { version: 1, repositories: { ...value.repositories } };
|
|
2994
|
-
}
|
|
2995
|
-
function isFactoryClosureEvent(event) {
|
|
2996
|
-
return (event.event === "issues" || event.event === "pull_request") && event.payload.action === "closed";
|
|
2997
|
-
}
|
|
2998
|
-
function parseEvent(event) {
|
|
2999
|
-
if (!event.id || !event.deliveryId || !SUPPORTED_EVENTS.has(event.event) || !event.payload || typeof event.payload !== "object" || Array.isArray(event.payload)) {
|
|
3000
|
-
return null;
|
|
3001
|
-
}
|
|
3002
|
-
return {
|
|
3003
|
-
event: event.event,
|
|
3004
|
-
deliveryId: event.deliveryId,
|
|
3005
|
-
payload: event.payload
|
|
3006
|
-
};
|
|
3007
|
-
}
|
|
3008
|
-
function retryDelay(error, fallbackMs) {
|
|
3009
|
-
if (error instanceof PlatformApiError && error.status === 429 && error.retryAfterSeconds !== null) {
|
|
3010
|
-
return Math.max(fallbackMs, error.retryAfterSeconds * 1e3);
|
|
3011
|
-
}
|
|
3012
|
-
return fallbackMs;
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
// src/integrations/platform/github/integration.ts
|
|
3016
|
-
var PAGE_SIZE = 30;
|
|
3017
|
-
var API_PREFIX2 = "/v1/server";
|
|
3018
|
-
var REPOSITORY_TOKEN_PERMISSIONS = {
|
|
3019
|
-
contents: "write",
|
|
3020
|
-
issues: "write",
|
|
3021
|
-
pull_requests: "write"
|
|
3022
|
-
};
|
|
3023
|
-
function loose2(c) {
|
|
3024
|
-
return c;
|
|
43
|
+
return c;
|
|
3025
44
|
}
|
|
3026
45
|
function routeBaseUrl(ctx, requestUrl) {
|
|
3027
|
-
|
|
46
|
+
return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
|
|
3028
47
|
}
|
|
3029
48
|
var PlatformGithubIntegration = class {
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
);
|
|
3717
|
-
} catch (error) {
|
|
3718
|
-
if (isNotFound(error)) return null;
|
|
3719
|
-
throw error;
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
async #createReview(input) {
|
|
3723
|
-
const review = await this.#client.request(
|
|
3724
|
-
"POST",
|
|
3725
|
-
`${pullRequestPath(input, input.pullRequestId)}/reviews`,
|
|
3726
|
-
{ body: input.body, commitId: input.commitId, event: input.event ? reviewEvent(input.event) : void 0 },
|
|
3727
|
-
{ actingUserId: input.actingUserId }
|
|
3728
|
-
);
|
|
3729
|
-
return parseReview(review);
|
|
3730
|
-
}
|
|
3731
|
-
async #updateReview(input) {
|
|
3732
|
-
const review = await this.#client.request(
|
|
3733
|
-
"PUT",
|
|
3734
|
-
`${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`,
|
|
3735
|
-
{ body: input.body },
|
|
3736
|
-
{ actingUserId: input.actingUserId }
|
|
3737
|
-
);
|
|
3738
|
-
return parseReview(review);
|
|
3739
|
-
}
|
|
3740
|
-
async #submitReview(input) {
|
|
3741
|
-
const review = await this.#client.request(
|
|
3742
|
-
"POST",
|
|
3743
|
-
`${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/events`,
|
|
3744
|
-
{ body: input.body, event: reviewEvent(input.event) },
|
|
3745
|
-
{ actingUserId: input.actingUserId }
|
|
3746
|
-
);
|
|
3747
|
-
return parseReview(review);
|
|
3748
|
-
}
|
|
3749
|
-
async #dismissReview(input) {
|
|
3750
|
-
const review = await this.#client.request(
|
|
3751
|
-
"PUT",
|
|
3752
|
-
`${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/dismissals`,
|
|
3753
|
-
{ message: input.message },
|
|
3754
|
-
{ actingUserId: input.actingUserId }
|
|
3755
|
-
);
|
|
3756
|
-
return parseReview(review);
|
|
3757
|
-
}
|
|
3758
|
-
async #deletePendingReview(input) {
|
|
3759
|
-
await this.#client.request(
|
|
3760
|
-
"DELETE",
|
|
3761
|
-
`${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`,
|
|
3762
|
-
void 0,
|
|
3763
|
-
{ actingUserId: input.actingUserId }
|
|
3764
|
-
);
|
|
3765
|
-
}
|
|
3766
|
-
async #listReviewComments(input) {
|
|
3767
|
-
const page = parsePositiveCursor(input.cursor);
|
|
3768
|
-
const result = await this.#client.request(
|
|
3769
|
-
"GET",
|
|
3770
|
-
`${pullRequestPath(input, input.pullRequestId)}/comments?page=${page}&per_page=${PAGE_SIZE}`
|
|
3771
|
-
);
|
|
3772
|
-
return {
|
|
3773
|
-
comments: result.comments.map(parseReviewComment),
|
|
3774
|
-
nextCursor: result.comments.length === PAGE_SIZE ? String(page + 1) : null
|
|
3775
|
-
};
|
|
3776
|
-
}
|
|
3777
|
-
async #createReviewComment(input) {
|
|
3778
|
-
let body;
|
|
3779
|
-
if (input.replyToId !== void 0) {
|
|
3780
|
-
body = { body: input.body, replyToId: requirePositiveId(input.replyToId, "review comment") };
|
|
3781
|
-
} else {
|
|
3782
|
-
if (!input.commitId || !input.path || input.line === void 0 || !input.side) {
|
|
3783
|
-
throw new Error("A review comment requires commitId, path, line, and side unless it is a reply.");
|
|
3784
|
-
}
|
|
3785
|
-
body = {
|
|
3786
|
-
body: input.body,
|
|
3787
|
-
commitId: input.commitId,
|
|
3788
|
-
path: input.path,
|
|
3789
|
-
line: input.line,
|
|
3790
|
-
side: input.side.toUpperCase(),
|
|
3791
|
-
startLine: input.startLine,
|
|
3792
|
-
startSide: input.startSide?.toUpperCase()
|
|
3793
|
-
};
|
|
3794
|
-
}
|
|
3795
|
-
return parseReviewComment(
|
|
3796
|
-
await this.#client.request(
|
|
3797
|
-
"POST",
|
|
3798
|
-
`${pullRequestPath(input, input.pullRequestId)}/comments`,
|
|
3799
|
-
body,
|
|
3800
|
-
{ actingUserId: input.actingUserId }
|
|
3801
|
-
)
|
|
3802
|
-
);
|
|
3803
|
-
}
|
|
3804
|
-
async #updateReviewComment(input) {
|
|
3805
|
-
requireGithubConnection(input.connection);
|
|
3806
|
-
return parseReviewComment(
|
|
3807
|
-
await this.#client.request(
|
|
3808
|
-
"PATCH",
|
|
3809
|
-
repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`),
|
|
3810
|
-
{ body: input.body },
|
|
3811
|
-
{ actingUserId: input.actingUserId }
|
|
3812
|
-
)
|
|
3813
|
-
);
|
|
3814
|
-
}
|
|
3815
|
-
async #deleteReviewComment(input) {
|
|
3816
|
-
requireGithubConnection(input.connection);
|
|
3817
|
-
await this.#client.request(
|
|
3818
|
-
"DELETE",
|
|
3819
|
-
repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`),
|
|
3820
|
-
void 0,
|
|
3821
|
-
{ actingUserId: input.actingUserId }
|
|
3822
|
-
);
|
|
3823
|
-
}
|
|
3824
|
-
#requestedReviewers(method, input) {
|
|
3825
|
-
requireGithubConnection(input.connection);
|
|
3826
|
-
return this.#client.request(
|
|
3827
|
-
method,
|
|
3828
|
-
`${pullRequestPath(input, input.pullRequestId)}/requested-reviewers`,
|
|
3829
|
-
method === "GET" ? void 0 : { users: input.users, teams: input.teams },
|
|
3830
|
-
method === "GET" ? void 0 : { actingUserId: input.actingUserId }
|
|
3831
|
-
);
|
|
3832
|
-
}
|
|
49
|
+
id = "github";
|
|
50
|
+
#client;
|
|
51
|
+
#endpointHost;
|
|
52
|
+
#pollingEnabled;
|
|
53
|
+
#pollingIntervalMs;
|
|
54
|
+
#storage;
|
|
55
|
+
#integrationStorage;
|
|
56
|
+
/** installationId → cached repository listing (TTL-bounded). */
|
|
57
|
+
#installationReposCache = /* @__PURE__ */ new Map();
|
|
58
|
+
/** `orgId:repositoryId` → cached repository access (TTL-bounded). */
|
|
59
|
+
#repositoryAccessCache = /* @__PURE__ */ new Map();
|
|
60
|
+
intake = {
|
|
61
|
+
resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
|
|
62
|
+
listSources: async ({ orgId, userId }) => {
|
|
63
|
+
const usable = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations`)).installations.filter((installation) => installation.usable && !installation.suspendedAt);
|
|
64
|
+
return (await Promise.all(usable.map(async (installation) => {
|
|
65
|
+
const storedInstallation = await this.versionControl.registerInstallation({
|
|
66
|
+
orgId,
|
|
67
|
+
userId,
|
|
68
|
+
installation: {
|
|
69
|
+
externalId: String(installation.installationId),
|
|
70
|
+
accountName: installation.accountLogin,
|
|
71
|
+
accountType: installation.accountType
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const result = await this.#client.request("GET", `${API_PREFIX}/github-app/installations/${installation.installationId}/repositories`);
|
|
75
|
+
await this.versionControl.registerRepositories({
|
|
76
|
+
orgId,
|
|
77
|
+
installationId: storedInstallation.id,
|
|
78
|
+
repositories: result.repositories.map((repository) => ({
|
|
79
|
+
externalId: String(repository.id),
|
|
80
|
+
slug: repository.fullName,
|
|
81
|
+
defaultBranch: repository.defaultBranch,
|
|
82
|
+
metadata: {
|
|
83
|
+
private: repository.private,
|
|
84
|
+
url: repository.htmlUrl
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
});
|
|
88
|
+
return result.repositories.map((repository) => ({
|
|
89
|
+
repository,
|
|
90
|
+
installation
|
|
91
|
+
}));
|
|
92
|
+
}))).flat().map(({ repository, installation }) => ({
|
|
93
|
+
id: repository.fullName,
|
|
94
|
+
name: repository.fullName,
|
|
95
|
+
type: "repository",
|
|
96
|
+
metadata: {
|
|
97
|
+
installationId: installation.installationId,
|
|
98
|
+
accountLogin: installation.accountLogin,
|
|
99
|
+
accountType: installation.accountType,
|
|
100
|
+
repositoryId: repository.id,
|
|
101
|
+
defaultBranch: repository.defaultBranch,
|
|
102
|
+
private: repository.private,
|
|
103
|
+
url: repository.htmlUrl
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
},
|
|
107
|
+
listItems: async ({ sourceIds, cursor }) => {
|
|
108
|
+
const page = parsePositiveCursor(cursor);
|
|
109
|
+
const pages = await Promise.all(sourceIds.map(async (sourceId) => {
|
|
110
|
+
const connection = {
|
|
111
|
+
type: "app-installation",
|
|
112
|
+
installationId: 1
|
|
113
|
+
};
|
|
114
|
+
const [issues, pullRequests] = await Promise.all([this.#listIssues(connection, sourceId, page), this.#listPullRequests({
|
|
115
|
+
connection,
|
|
116
|
+
sourceId,
|
|
117
|
+
includeDrafts: false,
|
|
118
|
+
cursor: String(page)
|
|
119
|
+
})]);
|
|
120
|
+
return {
|
|
121
|
+
items: [...issues.issues.map((issue) => ({
|
|
122
|
+
source: {
|
|
123
|
+
type: "issue",
|
|
124
|
+
externalId: `${sourceId}:${issue.id}`,
|
|
125
|
+
url: issue.url
|
|
126
|
+
},
|
|
127
|
+
sourceId,
|
|
128
|
+
title: issue.title,
|
|
129
|
+
status: issue.state ?? void 0,
|
|
130
|
+
labels: issue.labels,
|
|
131
|
+
assignee: issue.assignee,
|
|
132
|
+
createdAt: issue.createdAt,
|
|
133
|
+
updatedAt: issue.updatedAt,
|
|
134
|
+
metadata: {
|
|
135
|
+
repository: sourceId,
|
|
136
|
+
number: Number(issue.id),
|
|
137
|
+
author: issue.author
|
|
138
|
+
}
|
|
139
|
+
})), ...pullRequests.pullRequests.map((pullRequest) => ({
|
|
140
|
+
source: {
|
|
141
|
+
type: "pull-request",
|
|
142
|
+
externalId: `${sourceId}:${pullRequest.id}`,
|
|
143
|
+
url: pullRequest.url
|
|
144
|
+
},
|
|
145
|
+
sourceId,
|
|
146
|
+
title: pullRequest.title,
|
|
147
|
+
status: pullRequest.state,
|
|
148
|
+
createdAt: pullRequest.createdAt,
|
|
149
|
+
updatedAt: pullRequest.updatedAt,
|
|
150
|
+
metadata: {
|
|
151
|
+
repository: sourceId,
|
|
152
|
+
number: Number(pullRequest.id),
|
|
153
|
+
author: pullRequest.author,
|
|
154
|
+
baseBranch: pullRequest.baseBranch,
|
|
155
|
+
headBranch: pullRequest.headBranch
|
|
156
|
+
}
|
|
157
|
+
}))],
|
|
158
|
+
hasNextPage: issues.nextCursor !== null || pullRequests.nextCursor !== null
|
|
159
|
+
};
|
|
160
|
+
}));
|
|
161
|
+
return {
|
|
162
|
+
items: pages.flatMap((result) => result.items),
|
|
163
|
+
nextCursor: pages.some((result) => result.hasNextPage) ? String(page + 1) : null
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
listIssues: async (input) => {
|
|
167
|
+
requireGithubConnection(input.connection);
|
|
168
|
+
const sourceId = requireSingleSource(input.sourceIds, "GitHub Intake requires exactly one repository source.");
|
|
169
|
+
return this.#listIssues(input.connection, sourceId, parsePositiveCursor(input.cursor), input.labels);
|
|
170
|
+
},
|
|
171
|
+
getIssue: (input) => this.#getIssue(input.connection, input.sourceId, input.issueId),
|
|
172
|
+
createComment: (input) => this.#createIssueComment(input),
|
|
173
|
+
updateIssue: (input) => this.#updateIntakeIssue(input)
|
|
174
|
+
};
|
|
175
|
+
versionControl = {
|
|
176
|
+
initialize: ({ storage }) => {
|
|
177
|
+
this.#storage = storage;
|
|
178
|
+
},
|
|
179
|
+
registerInstallation: ({ orgId, userId, installation }) => this.storage.installations.upsert({
|
|
180
|
+
orgId,
|
|
181
|
+
connectedByUserId: userId,
|
|
182
|
+
externalId: installation.externalId,
|
|
183
|
+
accountName: installation.accountName,
|
|
184
|
+
accountType: installation.accountType,
|
|
185
|
+
providerMetadata: installation.metadata
|
|
186
|
+
}),
|
|
187
|
+
registerRepositories: ({ orgId, installationId, repositories }) => Promise.all(repositories.map((repository) => this.storage.repositories.upsert({
|
|
188
|
+
orgId,
|
|
189
|
+
input: {
|
|
190
|
+
installationId,
|
|
191
|
+
externalId: repository.externalId,
|
|
192
|
+
slug: repository.slug,
|
|
193
|
+
defaultBranch: repository.defaultBranch,
|
|
194
|
+
providerMetadata: repository.metadata
|
|
195
|
+
}
|
|
196
|
+
}))),
|
|
197
|
+
getRepositoryAccess: async ({ orgId, repositoryId }) => {
|
|
198
|
+
const cacheKey = `${orgId}:${repositoryId}`;
|
|
199
|
+
const cached = this.#repositoryAccessCache.get(cacheKey);
|
|
200
|
+
if (cached && cached.expiresAt > Date.now()) return cached.access;
|
|
201
|
+
this.#repositoryAccessCache.delete(cacheKey);
|
|
202
|
+
const repository = await this.storage.repositories.get({
|
|
203
|
+
orgId,
|
|
204
|
+
id: repositoryId
|
|
205
|
+
});
|
|
206
|
+
if (!repository) throw new Error("Version-control repository not found.");
|
|
207
|
+
const cloneUrl = `https://github.com/${repository.slug}.git`;
|
|
208
|
+
const installation = await this.storage.installations.get({
|
|
209
|
+
orgId,
|
|
210
|
+
id: repository.installationId
|
|
211
|
+
});
|
|
212
|
+
if (!installation) throw new Error("Version-control installation not found.");
|
|
213
|
+
const installationId = parsePositiveInteger(installation.externalId);
|
|
214
|
+
if (installationId === null) throw new Error("GitHub installation id is invalid.");
|
|
215
|
+
const repositoryName = splitRepository(repository.slug).repo;
|
|
216
|
+
const access = {
|
|
217
|
+
cloneUrl,
|
|
218
|
+
authorization: {
|
|
219
|
+
scheme: "bearer",
|
|
220
|
+
token: (await this.#client.request("POST", `${API_PREFIX}/github-app/installations/${installationId}/token`, {
|
|
221
|
+
repositories: [repositoryName],
|
|
222
|
+
permissions: REPOSITORY_TOKEN_PERMISSIONS
|
|
223
|
+
})).token
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
setBounded(this.#repositoryAccessCache, cacheKey, {
|
|
227
|
+
access,
|
|
228
|
+
expiresAt: Date.now() + REPOSITORY_ACCESS_CACHE_TTL_MS
|
|
229
|
+
});
|
|
230
|
+
return access;
|
|
231
|
+
},
|
|
232
|
+
listPullRequests: (input) => this.#listPullRequests(input),
|
|
233
|
+
getPullRequest: (input) => this.#getPullRequest(input),
|
|
234
|
+
createPullRequest: (input) => this.#createPullRequest(input),
|
|
235
|
+
updatePullRequest: (input) => this.#updatePullRequest(input),
|
|
236
|
+
closePullRequest: (input) => this.#updatePullRequest({
|
|
237
|
+
...input,
|
|
238
|
+
state: "closed"
|
|
239
|
+
}),
|
|
240
|
+
mergePullRequest: (input) => this.#mergePullRequest(input),
|
|
241
|
+
listComments: (input) => this.#listComments(input),
|
|
242
|
+
createComment: (input) => this.#createComment(input),
|
|
243
|
+
updateComment: (input) => this.#updateComment(input),
|
|
244
|
+
deleteComment: (input) => this.#deleteComment(input),
|
|
245
|
+
listReviews: (input) => this.#listReviews(input),
|
|
246
|
+
getReview: (input) => this.#getReview(input),
|
|
247
|
+
createReview: (input) => this.#createReview(input),
|
|
248
|
+
updateReview: (input) => this.#updateReview(input),
|
|
249
|
+
submitReview: (input) => this.#submitReview(input),
|
|
250
|
+
dismissReview: (input) => this.#dismissReview(input),
|
|
251
|
+
deletePendingReview: (input) => this.#deletePendingReview(input),
|
|
252
|
+
listReviewComments: (input) => this.#listReviewComments(input),
|
|
253
|
+
createReviewComment: (input) => this.#createReviewComment(input),
|
|
254
|
+
updateReviewComment: (input) => this.#updateReviewComment(input),
|
|
255
|
+
deleteReviewComment: (input) => this.#deleteReviewComment(input),
|
|
256
|
+
listRequestedReviewers: (input) => this.#requestedReviewers("GET", input),
|
|
257
|
+
requestReviewers: (input) => this.#requestedReviewers("POST", input),
|
|
258
|
+
removeRequestedReviewers: (input) => this.#requestedReviewers("DELETE", input)
|
|
259
|
+
};
|
|
260
|
+
constructor() {
|
|
261
|
+
const config = platformApiClientConfigFromEnv();
|
|
262
|
+
this.#client = new PlatformApiClient(config);
|
|
263
|
+
this.#endpointHost = new URL(config.baseUrl).host;
|
|
264
|
+
this.#pollingEnabled = process.env.MASTRA_PLATFORM_GITHUB_POLLING_ENABLED?.trim().toLowerCase() !== "false";
|
|
265
|
+
this.#pollingIntervalMs = optionalPositiveIntegerEnv("MASTRA_PLATFORM_GITHUB_POLLING_INTERVAL_MS");
|
|
266
|
+
}
|
|
267
|
+
get storage() {
|
|
268
|
+
if (!this.#storage) throw new Error("PlatformGithubIntegration source-control storage has not been initialized.");
|
|
269
|
+
return this.#storage;
|
|
270
|
+
}
|
|
271
|
+
get sourceControlStorage() {
|
|
272
|
+
return this.storage;
|
|
273
|
+
}
|
|
274
|
+
get integrationStorage() {
|
|
275
|
+
if (!this.#integrationStorage) throw new Error("PlatformGithubIntegration generic storage has not been initialized.");
|
|
276
|
+
return this.#integrationStorage;
|
|
277
|
+
}
|
|
278
|
+
/** Resolve a stored GitHub locator without scanning installations or repositories. */
|
|
279
|
+
async #resolveIntakeDispatch({ orgId, externalSource }) {
|
|
280
|
+
const target = parseGithubExternalTarget(externalSource.externalId);
|
|
281
|
+
if (!target) return null;
|
|
282
|
+
const repository = target.repository.includes("/") ? target.repository : (await this.storage.repositories.findByExternalId({
|
|
283
|
+
orgId,
|
|
284
|
+
externalId: target.repository
|
|
285
|
+
}))?.slug;
|
|
286
|
+
if (!repository) return null;
|
|
287
|
+
return {
|
|
288
|
+
connection: {
|
|
289
|
+
type: "app-installation",
|
|
290
|
+
installationId: 1
|
|
291
|
+
},
|
|
292
|
+
sourceId: repository,
|
|
293
|
+
issueId: target.issueId
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
initialize({ storage }) {
|
|
297
|
+
this.#integrationStorage = storage;
|
|
298
|
+
logPlatformInfo("Platform GitHub integration initialized", {
|
|
299
|
+
endpointHost: this.#endpointHost,
|
|
300
|
+
pollingEnabled: this.#pollingEnabled,
|
|
301
|
+
pollingIntervalMs: this.#pollingIntervalMs
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
routes(ctx) {
|
|
305
|
+
const ingestFactoryEvent = attachGithubRules(this, ctx);
|
|
306
|
+
return [
|
|
307
|
+
this.#statusRoute(ctx),
|
|
308
|
+
this.#connectRoute(ctx),
|
|
309
|
+
this.#connectUserRoute(ctx),
|
|
310
|
+
...buildGithubRoutes({
|
|
311
|
+
auth: ctx.auth,
|
|
312
|
+
fleet: ctx.fleet,
|
|
313
|
+
storage: ctx.factoryStorage,
|
|
314
|
+
github: this,
|
|
315
|
+
stateSigner: ctx.stateSigner,
|
|
316
|
+
baseUrl: ctx.baseUrl,
|
|
317
|
+
controller: ctx.controller,
|
|
318
|
+
projects: ctx.storage.projects,
|
|
319
|
+
emitAudit: ctx.hooks?.emitAudit,
|
|
320
|
+
ingestFactoryEvent
|
|
321
|
+
}).filter((route) => route.path !== "/web/github/status" && route.path !== "/web/github/webhook" && !route.path.startsWith("/auth/github/"))
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
#statusRoute(ctx) {
|
|
325
|
+
return registerApiRoute("/web/github/status", {
|
|
326
|
+
method: "GET",
|
|
327
|
+
requiresAuth: false,
|
|
328
|
+
handler: async (c) => {
|
|
329
|
+
await ctx.auth.ensureUser(loose(c));
|
|
330
|
+
const tenant = ctx.auth.tenant(loose(c));
|
|
331
|
+
if (!tenant) return c.json({
|
|
332
|
+
error: "unauthorized",
|
|
333
|
+
reason: "auth_required"
|
|
334
|
+
}, 401);
|
|
335
|
+
if (!tenant.orgId) return c.json({
|
|
336
|
+
enabled: true,
|
|
337
|
+
sandboxEnabled: ctx.fleet.enabled,
|
|
338
|
+
organizationRequired: true,
|
|
339
|
+
connected: false,
|
|
340
|
+
installations: [],
|
|
341
|
+
userConnected: false,
|
|
342
|
+
userGithubUsername: null,
|
|
343
|
+
reason: "organization_required",
|
|
344
|
+
diagnostics: this.diagnostics()
|
|
345
|
+
});
|
|
346
|
+
const [installations, userConnection] = await Promise.all([this.#syncInstallations(tenant.orgId, tenant.userId), this.#fetchUserConnection(tenant.userId)]);
|
|
347
|
+
return c.json({
|
|
348
|
+
enabled: true,
|
|
349
|
+
sandboxEnabled: ctx.fleet.enabled,
|
|
350
|
+
connected: installations.length > 0,
|
|
351
|
+
installations: installations.map((installation) => ({
|
|
352
|
+
installationId: Number(installation.externalId),
|
|
353
|
+
accountLogin: installation.accountName,
|
|
354
|
+
accountType: installation.accountType
|
|
355
|
+
})),
|
|
356
|
+
userConnected: userConnection.connected,
|
|
357
|
+
userGithubUsername: userConnection.githubUsername,
|
|
358
|
+
reason: installations.length > 0 ? "ready" : "not_connected",
|
|
359
|
+
diagnostics: this.diagnostics()
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
#connectRoute(ctx) {
|
|
365
|
+
return registerApiRoute("/auth/github/connect", {
|
|
366
|
+
method: "GET",
|
|
367
|
+
requiresAuth: false,
|
|
368
|
+
handler: async (c) => {
|
|
369
|
+
await ctx.auth.ensureUser(loose(c));
|
|
370
|
+
const tenant = ctx.auth.tenant(loose(c));
|
|
371
|
+
if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
|
|
372
|
+
const redirectTo = c.req.query("redirectTo") || c.req.query("return_to") || "/";
|
|
373
|
+
const originator = routeBaseUrl(ctx, c.req.url);
|
|
374
|
+
logPlatformInfo("Starting Platform GitHub connect flow", {
|
|
375
|
+
orgId: tenant.orgId,
|
|
376
|
+
redirectTo,
|
|
377
|
+
originator
|
|
378
|
+
});
|
|
379
|
+
const query = new URLSearchParams({
|
|
380
|
+
action: "install",
|
|
381
|
+
redirectTo,
|
|
382
|
+
originator
|
|
383
|
+
});
|
|
384
|
+
const { url } = await this.#client.request("GET", `${API_PREFIX}/github-app/install-url?${query}`);
|
|
385
|
+
return c.redirect(url);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
#connectUserRoute(ctx) {
|
|
390
|
+
return registerApiRoute("/auth/github/connect-user", {
|
|
391
|
+
method: "GET",
|
|
392
|
+
requiresAuth: false,
|
|
393
|
+
handler: async (c) => {
|
|
394
|
+
await ctx.auth.ensureUser(loose(c));
|
|
395
|
+
const tenant = ctx.auth.tenant(loose(c));
|
|
396
|
+
if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
|
|
397
|
+
const redirectTo = c.req.query("redirectTo") || c.req.query("return_to") || "/";
|
|
398
|
+
const originator = routeBaseUrl(ctx, c.req.url);
|
|
399
|
+
logPlatformInfo("Starting Platform GitHub user authorization flow", {
|
|
400
|
+
orgId: tenant.orgId,
|
|
401
|
+
redirectTo,
|
|
402
|
+
originator
|
|
403
|
+
});
|
|
404
|
+
const query = new URLSearchParams({
|
|
405
|
+
userId: tenant.userId,
|
|
406
|
+
redirectTo,
|
|
407
|
+
originator
|
|
408
|
+
});
|
|
409
|
+
const { url } = await this.#client.request("GET", `${API_PREFIX}/github-app/authenticate?${query}`);
|
|
410
|
+
return c.redirect(url);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Personal GitHub connection status for the acting user. Returns
|
|
416
|
+
* not-connected when the platform predates the user-connection endpoint.
|
|
417
|
+
*/
|
|
418
|
+
async #fetchUserConnection(userId) {
|
|
419
|
+
try {
|
|
420
|
+
const connection = await this.#client.request("GET", `${API_PREFIX}/github-app/user-connection?${new URLSearchParams({ userId })}`);
|
|
421
|
+
if (!connection.connected && connection.reason) logPlatformWarn("Platform GitHub user connection verification failed", {
|
|
422
|
+
userId,
|
|
423
|
+
reason: connection.reason
|
|
424
|
+
});
|
|
425
|
+
return connection;
|
|
426
|
+
} catch {
|
|
427
|
+
return {
|
|
428
|
+
connected: false,
|
|
429
|
+
githubUsername: null
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async #syncInstallations(orgId, userId) {
|
|
434
|
+
const usableInstallations = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations`)).installations.filter((installation) => installation.usable && !installation.suspendedAt);
|
|
435
|
+
return Promise.all(usableInstallations.map((installation) => this.versionControl.registerInstallation({
|
|
436
|
+
orgId,
|
|
437
|
+
userId,
|
|
438
|
+
installation: {
|
|
439
|
+
externalId: String(installation.installationId),
|
|
440
|
+
accountName: installation.accountLogin,
|
|
441
|
+
accountType: installation.accountType
|
|
442
|
+
}
|
|
443
|
+
})));
|
|
444
|
+
}
|
|
445
|
+
workers(ctx) {
|
|
446
|
+
if (!this.#pollingEnabled) return [];
|
|
447
|
+
if (!ctx.controller) throw new Error("Platform GitHub event polling requires the mounted Mastra Code controller.");
|
|
448
|
+
return [new PlatformGithubEventWorker({
|
|
449
|
+
client: this.#client,
|
|
450
|
+
controller: ctx.controller,
|
|
451
|
+
github: this,
|
|
452
|
+
storage: ctx.storage.generic,
|
|
453
|
+
ingestFactoryEvent: attachGithubRules(this, ctx),
|
|
454
|
+
intervalMs: this.#pollingIntervalMs
|
|
455
|
+
})];
|
|
456
|
+
}
|
|
457
|
+
sessionTools({ requestContext }) {
|
|
458
|
+
return createGithubSubscriptionTools(requestContext, this);
|
|
459
|
+
}
|
|
460
|
+
async postToolObserver({ toolContext, requestContext }) {
|
|
461
|
+
const pullRequestUrl = parseCreatedPullRequest(toolContext);
|
|
462
|
+
if (!pullRequestUrl || !requestContext) return;
|
|
463
|
+
await subscribeCurrentSessionToPullRequest(requestContext, pullRequestUrl, "auto-gh-pr-create", this);
|
|
464
|
+
}
|
|
465
|
+
diagnostics() {
|
|
466
|
+
return {
|
|
467
|
+
mode: "platform",
|
|
468
|
+
endpointHost: this.#endpointHost,
|
|
469
|
+
polling: {
|
|
470
|
+
enabled: this.#pollingEnabled,
|
|
471
|
+
...this.#pollingIntervalMs === void 0 ? {} : { intervalMs: this.#pollingIntervalMs }
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async getRepositoryCollaboratorPermission(_installationId, repoFullName, username, signal) {
|
|
476
|
+
let repository;
|
|
477
|
+
try {
|
|
478
|
+
repository = splitRepository(repoFullName);
|
|
479
|
+
} catch {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
return (await this.#client.request("GET", `${API_PREFIX}/github/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/collaborators/${encodeURIComponent(username)}/permission`, void 0, { signal })).permission;
|
|
484
|
+
} catch {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async listInstallationRepos(installationId) {
|
|
489
|
+
const cached = this.#installationReposCache.get(installationId);
|
|
490
|
+
if (cached && cached.expiresAt > Date.now()) return cached.repos;
|
|
491
|
+
this.#installationReposCache.delete(installationId);
|
|
492
|
+
const repos = (await this.#client.request("GET", `${API_PREFIX}/github-app/installations/${installationId}/repositories`)).repositories.map((repository) => ({
|
|
493
|
+
...repository,
|
|
494
|
+
installationId
|
|
495
|
+
}));
|
|
496
|
+
setBounded(this.#installationReposCache, installationId, {
|
|
497
|
+
repos,
|
|
498
|
+
expiresAt: Date.now() + INSTALLATION_REPOS_CACHE_TTL_MS
|
|
499
|
+
});
|
|
500
|
+
return repos;
|
|
501
|
+
}
|
|
502
|
+
async mintInstallationToken(installationId) {
|
|
503
|
+
const repositories = await this.listInstallationRepos(installationId);
|
|
504
|
+
if (repositories.length === 0 || repositories.length > 10) throw new Error("Platform GitHub token minting requires between one and ten installation repositories.");
|
|
505
|
+
return (await this.#client.request("POST", `${API_PREFIX}/github-app/installations/${installationId}/token`, {
|
|
506
|
+
repositories: repositories.map((repository) => repository.name),
|
|
507
|
+
permissions: REPOSITORY_TOKEN_PERMISSIONS
|
|
508
|
+
})).token;
|
|
509
|
+
}
|
|
510
|
+
async addIssueLabels(_installationId, sourceId, issueNumber, labels) {
|
|
511
|
+
return (await this.#client.request("POST", repositoryPath(sourceId, `issues/${issueNumber}/labels`), { labels })).labels;
|
|
512
|
+
}
|
|
513
|
+
getInstallationOctokit(_installationId) {
|
|
514
|
+
return { pulls: { get: async ({ owner, repo, pull_number }) => {
|
|
515
|
+
return { data: { base: { repo: { id: (await this.#client.request("GET", repositoryPath(`${owner}/${repo}`, `pulls/${pull_number}`))).base.repo.id } } } };
|
|
516
|
+
} } };
|
|
517
|
+
}
|
|
518
|
+
async #listIssues(connection, sourceId, page, labels) {
|
|
519
|
+
requireGithubConnection(connection);
|
|
520
|
+
const path = repositoryPath(sourceId, "issues");
|
|
521
|
+
const query = new URLSearchParams({
|
|
522
|
+
state: "open",
|
|
523
|
+
page: String(page),
|
|
524
|
+
per_page: String(PAGE_SIZE)
|
|
525
|
+
});
|
|
526
|
+
const normalizedLabels = normalizeLabels(labels);
|
|
527
|
+
if (normalizedLabels.length > 0) query.set("label", normalizedLabels.join(","));
|
|
528
|
+
const result = await this.#client.request("GET", `${path}?${query}`);
|
|
529
|
+
return {
|
|
530
|
+
issues: result.issues.map((issue) => parseIntakeIssue(sourceId, issue)),
|
|
531
|
+
nextCursor: result.issues.length === PAGE_SIZE ? String(page + 1) : null
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async #getIssue(connection, sourceId, issueId) {
|
|
535
|
+
requireGithubConnection(connection);
|
|
536
|
+
const repository = requireSource(sourceId, "GitHub Intake requires a repository source.");
|
|
537
|
+
const issueNumber = requirePositiveId(issueId, "issue");
|
|
538
|
+
try {
|
|
539
|
+
const [issue, comments] = await Promise.all([this.#client.request("GET", repositoryPath(repository, `issues/${issueNumber}`)), this.#client.request("GET", `${repositoryPath(repository, `issues/${issueNumber}/comments`)}?per_page=100`)]);
|
|
540
|
+
return parseIntakeIssueDetail(repository, issue, comments.comments);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (isNotFound(error)) return null;
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async #updateIntakeIssue(input) {
|
|
547
|
+
requireGithubConnection(input.connection);
|
|
548
|
+
const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
|
|
549
|
+
const issueNumber = requirePositiveId(input.issueId, "issue");
|
|
550
|
+
if (input.state.kind === "byName") {
|
|
551
|
+
logPlatformWarn(`Platform GitHub: updateIssue byName is not supported (name=${input.state.name}); ignoring.`);
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const targetState = input.state.stateType === "unstarted" || input.state.stateType === "started" ? "open" : "closed";
|
|
555
|
+
const stateReason = targetState === "closed" ? input.state.stateType === "canceled" ? "not_planned" : "completed" : null;
|
|
556
|
+
try {
|
|
557
|
+
await this.#client.request("GET", repositoryPath(repository, `pulls/${issueNumber}`));
|
|
558
|
+
logPlatformWarn(`Platform GitHub: updateIssue rejected — target ${repository}#${issueNumber} is a pull request.`);
|
|
559
|
+
return null;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (!isNotFound(error)) throw error;
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
return parseIntakeIssue(repository, await this.#client.request("PATCH", repositoryPath(repository, `issues/${issueNumber}`), {
|
|
565
|
+
state: targetState,
|
|
566
|
+
...stateReason ? { state_reason: stateReason } : {}
|
|
567
|
+
}, { actingUserId: input.actingUserId }));
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (isNotFound(error)) return null;
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async #createIssueComment(input) {
|
|
574
|
+
requireGithubConnection(input.connection);
|
|
575
|
+
const repository = requireSource(input.sourceId, "GitHub Intake requires a repository source.");
|
|
576
|
+
const issueNumber = requirePositiveId(input.issueId, "issue");
|
|
577
|
+
try {
|
|
578
|
+
const comment = await this.#client.request("POST", repositoryPath(repository, `issues/${issueNumber}/comments`), { body: input.body }, { actingUserId: input.actingUserId });
|
|
579
|
+
return {
|
|
580
|
+
id: String(comment.id),
|
|
581
|
+
url: comment.htmlUrl
|
|
582
|
+
};
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (isNotFound(error)) return null;
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async #listPullRequests(input) {
|
|
589
|
+
requireGithubConnection(input.connection);
|
|
590
|
+
const page = parsePositiveCursor(input.cursor);
|
|
591
|
+
const query = new URLSearchParams({
|
|
592
|
+
state: input.state ?? "open",
|
|
593
|
+
page: String(page),
|
|
594
|
+
per_page: String(PAGE_SIZE)
|
|
595
|
+
});
|
|
596
|
+
const result = await this.#client.request("GET", `${repositoryPath(input.sourceId, "pulls")}?${query}`);
|
|
597
|
+
return {
|
|
598
|
+
pullRequests: result.pullRequests.filter((pullRequest) => input.includeDrafts !== false || !pullRequest.draft).map(parsePullRequest),
|
|
599
|
+
nextCursor: result.pullRequests.length === PAGE_SIZE ? String(page + 1) : null
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async #getPullRequest(input) {
|
|
603
|
+
try {
|
|
604
|
+
return parsePullRequest(await this.#client.request("GET", pullRequestPath(input, input.pullRequestId)));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (isNotFound(error)) return null;
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async #createPullRequest(input) {
|
|
611
|
+
requireGithubConnection(input.connection);
|
|
612
|
+
return parsePullRequest(await this.#client.request("POST", repositoryPath(input.sourceId, "pulls"), {
|
|
613
|
+
head: input.headBranch,
|
|
614
|
+
base: input.baseBranch,
|
|
615
|
+
title: input.title,
|
|
616
|
+
body: input.body,
|
|
617
|
+
draft: input.draft
|
|
618
|
+
}, { actingUserId: input.actingUserId }));
|
|
619
|
+
}
|
|
620
|
+
async #updatePullRequest(input) {
|
|
621
|
+
return parsePullRequest(await this.#client.request("PATCH", pullRequestPath(input, input.pullRequestId), {
|
|
622
|
+
title: input.title,
|
|
623
|
+
body: input.body === null ? "" : input.body,
|
|
624
|
+
base: input.baseBranch,
|
|
625
|
+
state: input.state
|
|
626
|
+
}, { actingUserId: input.actingUserId }));
|
|
627
|
+
}
|
|
628
|
+
#mergePullRequest(input) {
|
|
629
|
+
return this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/merge`, {
|
|
630
|
+
commitTitle: input.commitTitle,
|
|
631
|
+
commitMessage: input.commitMessage,
|
|
632
|
+
method: input.method
|
|
633
|
+
}, { actingUserId: input.actingUserId });
|
|
634
|
+
}
|
|
635
|
+
async #listComments(input) {
|
|
636
|
+
const page = parsePositiveCursor(input.cursor);
|
|
637
|
+
const result = await this.#client.request("GET", `${repositoryPath(input.sourceId, `issues/${requirePositiveId(input.pullRequestId, "pull request")}/comments`)}?page=${page}&per_page=${PAGE_SIZE}`);
|
|
638
|
+
return {
|
|
639
|
+
comments: result.comments.map(parseComment),
|
|
640
|
+
nextCursor: result.comments.length === PAGE_SIZE ? String(page + 1) : null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async #createComment(input) {
|
|
644
|
+
return parseComment(await this.#client.request("POST", repositoryPath(input.sourceId, `issues/${requirePositiveId(input.pullRequestId, "pull request")}/comments`), { body: input.body }, { actingUserId: input.actingUserId }));
|
|
645
|
+
}
|
|
646
|
+
async #updateComment(input) {
|
|
647
|
+
requireGithubConnection(input.connection);
|
|
648
|
+
return parseComment(await this.#client.request("PATCH", repositoryPath(input.sourceId, `issues/comments/${requirePositiveId(input.commentId, "comment")}`), { body: input.body }, { actingUserId: input.actingUserId }));
|
|
649
|
+
}
|
|
650
|
+
async #deleteComment(input) {
|
|
651
|
+
requireGithubConnection(input.connection);
|
|
652
|
+
await this.#client.request("DELETE", repositoryPath(input.sourceId, `issues/comments/${requirePositiveId(input.commentId, "comment")}`), void 0, { actingUserId: input.actingUserId });
|
|
653
|
+
}
|
|
654
|
+
async #listReviews(input) {
|
|
655
|
+
const page = parsePositiveCursor(input.cursor);
|
|
656
|
+
const result = await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/reviews?page=${page}&per_page=${PAGE_SIZE}`);
|
|
657
|
+
return {
|
|
658
|
+
reviews: result.reviews.map(parseReview),
|
|
659
|
+
nextCursor: result.reviews.length === PAGE_SIZE ? String(page + 1) : null
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async #getReview(input) {
|
|
663
|
+
try {
|
|
664
|
+
return parseReview(await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`));
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (isNotFound(error)) return null;
|
|
667
|
+
throw error;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async #createReview(input) {
|
|
671
|
+
return parseReview(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/reviews`, {
|
|
672
|
+
body: input.body,
|
|
673
|
+
commitId: input.commitId,
|
|
674
|
+
event: input.event ? reviewEvent(input.event) : void 0
|
|
675
|
+
}, { actingUserId: input.actingUserId }));
|
|
676
|
+
}
|
|
677
|
+
async #updateReview(input) {
|
|
678
|
+
return parseReview(await this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`, { body: input.body }, { actingUserId: input.actingUserId }));
|
|
679
|
+
}
|
|
680
|
+
async #submitReview(input) {
|
|
681
|
+
return parseReview(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/events`, {
|
|
682
|
+
body: input.body,
|
|
683
|
+
event: reviewEvent(input.event)
|
|
684
|
+
}, { actingUserId: input.actingUserId }));
|
|
685
|
+
}
|
|
686
|
+
async #dismissReview(input) {
|
|
687
|
+
return parseReview(await this.#client.request("PUT", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}/dismissals`, { message: input.message }, { actingUserId: input.actingUserId }));
|
|
688
|
+
}
|
|
689
|
+
async #deletePendingReview(input) {
|
|
690
|
+
await this.#client.request("DELETE", `${pullRequestPath(input, input.pullRequestId)}/reviews/${requirePositiveId(input.reviewId, "review")}`, void 0, { actingUserId: input.actingUserId });
|
|
691
|
+
}
|
|
692
|
+
async #listReviewComments(input) {
|
|
693
|
+
const page = parsePositiveCursor(input.cursor);
|
|
694
|
+
const result = await this.#client.request("GET", `${pullRequestPath(input, input.pullRequestId)}/comments?page=${page}&per_page=${PAGE_SIZE}`);
|
|
695
|
+
return {
|
|
696
|
+
comments: result.comments.map(parseReviewComment),
|
|
697
|
+
nextCursor: result.comments.length === PAGE_SIZE ? String(page + 1) : null
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
async #createReviewComment(input) {
|
|
701
|
+
let body;
|
|
702
|
+
if (input.replyToId !== void 0) body = {
|
|
703
|
+
body: input.body,
|
|
704
|
+
replyToId: requirePositiveId(input.replyToId, "review comment")
|
|
705
|
+
};
|
|
706
|
+
else {
|
|
707
|
+
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.");
|
|
708
|
+
body = {
|
|
709
|
+
body: input.body,
|
|
710
|
+
commitId: input.commitId,
|
|
711
|
+
path: input.path,
|
|
712
|
+
line: input.line,
|
|
713
|
+
side: input.side.toUpperCase(),
|
|
714
|
+
startLine: input.startLine,
|
|
715
|
+
startSide: input.startSide?.toUpperCase()
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
return parseReviewComment(await this.#client.request("POST", `${pullRequestPath(input, input.pullRequestId)}/comments`, body, { actingUserId: input.actingUserId }));
|
|
719
|
+
}
|
|
720
|
+
async #updateReviewComment(input) {
|
|
721
|
+
requireGithubConnection(input.connection);
|
|
722
|
+
return parseReviewComment(await this.#client.request("PATCH", repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`), { body: input.body }, { actingUserId: input.actingUserId }));
|
|
723
|
+
}
|
|
724
|
+
async #deleteReviewComment(input) {
|
|
725
|
+
requireGithubConnection(input.connection);
|
|
726
|
+
await this.#client.request("DELETE", repositoryPath(input.sourceId, `pulls/comments/${requirePositiveId(input.commentId, "review comment")}`), void 0, { actingUserId: input.actingUserId });
|
|
727
|
+
}
|
|
728
|
+
#requestedReviewers(method, input) {
|
|
729
|
+
requireGithubConnection(input.connection);
|
|
730
|
+
return this.#client.request(method, `${pullRequestPath(input, input.pullRequestId)}/requested-reviewers`, method === "GET" ? void 0 : {
|
|
731
|
+
users: input.users,
|
|
732
|
+
teams: input.teams
|
|
733
|
+
}, method === "GET" ? void 0 : { actingUserId: input.actingUserId });
|
|
734
|
+
}
|
|
3833
735
|
};
|
|
3834
736
|
function repositoryPath(sourceId, suffix) {
|
|
3835
|
-
|
|
3836
|
-
|
|
737
|
+
const { owner, repo } = splitRepository(sourceId);
|
|
738
|
+
return `${API_PREFIX}/github/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${suffix}`;
|
|
3837
739
|
}
|
|
3838
740
|
function pullRequestPath(input, pullRequestId) {
|
|
3839
|
-
|
|
3840
|
-
|
|
741
|
+
requireGithubConnection(input.connection);
|
|
742
|
+
return repositoryPath(input.sourceId, `pulls/${requirePositiveId(pullRequestId, "pull request")}`);
|
|
3841
743
|
}
|
|
3842
744
|
function splitRepository(sourceId) {
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
745
|
+
const slash = sourceId.indexOf("/");
|
|
746
|
+
if (slash <= 0 || slash === sourceId.length - 1) throw new Error("GitHub capabilities require an owner/repository source.");
|
|
747
|
+
return {
|
|
748
|
+
owner: sourceId.slice(0, slash),
|
|
749
|
+
repo: sourceId.slice(slash + 1)
|
|
750
|
+
};
|
|
3848
751
|
}
|
|
3849
752
|
function parseIntakeIssue(sourceId, issue) {
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
753
|
+
return {
|
|
754
|
+
id: String(issue.number),
|
|
755
|
+
identifier: `#${issue.number}`,
|
|
756
|
+
title: issue.title,
|
|
757
|
+
url: issue.htmlUrl,
|
|
758
|
+
author: issue.user?.login ?? null,
|
|
759
|
+
state: issue.state,
|
|
760
|
+
stateType: issue.state,
|
|
761
|
+
priority: null,
|
|
762
|
+
assignee: issue.assignees[0] ?? null,
|
|
763
|
+
source: sourceId,
|
|
764
|
+
labels: issue.labels,
|
|
765
|
+
commentCount: issue.commentCount,
|
|
766
|
+
createdAt: issue.createdAt,
|
|
767
|
+
updatedAt: issue.updatedAt
|
|
768
|
+
};
|
|
3866
769
|
}
|
|
3867
770
|
function parseIntakeIssueDetail(sourceId, issue, comments) {
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
}
|
|
3878
|
-
function
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
771
|
+
return {
|
|
772
|
+
...parseIntakeIssue(sourceId, issue),
|
|
773
|
+
description: issue.body?.trim() ? issue.body : null,
|
|
774
|
+
comments: comments.map((comment) => ({
|
|
775
|
+
author: comment.user?.login ?? null,
|
|
776
|
+
body: comment.body,
|
|
777
|
+
createdAt: comment.createdAt
|
|
778
|
+
}))
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function parsePullRequest(pullRequest) {
|
|
782
|
+
return {
|
|
783
|
+
id: String(pullRequest.number),
|
|
784
|
+
title: pullRequest.title,
|
|
785
|
+
url: pullRequest.htmlUrl,
|
|
786
|
+
author: pullRequest.user?.login ?? null,
|
|
787
|
+
body: pullRequest.body?.trim() ? pullRequest.body : null,
|
|
788
|
+
state: pullRequest.state,
|
|
789
|
+
draft: pullRequest.draft,
|
|
790
|
+
merged: pullRequest.merged,
|
|
791
|
+
mergeable: pullRequest.mergeable,
|
|
792
|
+
baseBranch: pullRequest.base.ref,
|
|
793
|
+
headBranch: pullRequest.head.ref,
|
|
794
|
+
headSha: pullRequest.head.sha,
|
|
795
|
+
createdAt: pullRequest.createdAt,
|
|
796
|
+
updatedAt: pullRequest.updatedAt
|
|
797
|
+
};
|
|
3895
798
|
}
|
|
3896
799
|
function parseComment(comment) {
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
800
|
+
return {
|
|
801
|
+
id: String(comment.id),
|
|
802
|
+
url: comment.htmlUrl,
|
|
803
|
+
author: comment.user?.login ?? null,
|
|
804
|
+
body: comment.body,
|
|
805
|
+
createdAt: comment.createdAt,
|
|
806
|
+
updatedAt: comment.updatedAt
|
|
807
|
+
};
|
|
3905
808
|
}
|
|
3906
809
|
function parseReview(review) {
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
};
|
|
810
|
+
return {
|
|
811
|
+
id: String(review.id),
|
|
812
|
+
url: review.htmlUrl,
|
|
813
|
+
author: review.user?.login ?? null,
|
|
814
|
+
body: review.body?.trim() ? review.body : null,
|
|
815
|
+
state: {
|
|
816
|
+
PENDING: "pending",
|
|
817
|
+
COMMENTED: "commented",
|
|
818
|
+
APPROVED: "approved",
|
|
819
|
+
CHANGES_REQUESTED: "changes-requested",
|
|
820
|
+
DISMISSED: "dismissed"
|
|
821
|
+
}[review.state],
|
|
822
|
+
commitId: review.commitId,
|
|
823
|
+
submittedAt: review.submittedAt
|
|
824
|
+
};
|
|
3923
825
|
}
|
|
3924
826
|
function parseReviewComment(comment) {
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
827
|
+
return {
|
|
828
|
+
...parseComment(comment),
|
|
829
|
+
path: comment.path,
|
|
830
|
+
line: comment.line,
|
|
831
|
+
side: comment.side?.toLowerCase(),
|
|
832
|
+
commitId: comment.commitId,
|
|
833
|
+
replyToId: comment.replyToId === null ? null : String(comment.replyToId)
|
|
834
|
+
};
|
|
3933
835
|
}
|
|
3934
836
|
function requireGithubConnection(connection) {
|
|
3935
|
-
|
|
3936
|
-
throw new Error("GitHub capabilities require a GitHub connection.");
|
|
3937
|
-
}
|
|
837
|
+
if (connection.type !== "app-installation" && connection.type !== "oauth") throw new Error("GitHub capabilities require a GitHub connection.");
|
|
3938
838
|
}
|
|
3939
839
|
function requireSingleSource(sourceIds, message) {
|
|
3940
|
-
|
|
3941
|
-
|
|
840
|
+
if (sourceIds.length !== 1) throw new Error(message);
|
|
841
|
+
return sourceIds[0];
|
|
3942
842
|
}
|
|
3943
843
|
function requireSource(sourceId, message) {
|
|
3944
|
-
|
|
3945
|
-
|
|
844
|
+
if (!sourceId) throw new Error(message);
|
|
845
|
+
return sourceId;
|
|
3946
846
|
}
|
|
3947
847
|
function normalizeLabels(labels) {
|
|
3948
|
-
|
|
848
|
+
return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
|
|
3949
849
|
}
|
|
3950
850
|
function parsePositiveCursor(cursor) {
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
851
|
+
if (cursor === void 0) return 1;
|
|
852
|
+
const parsed = parsePositiveInteger(cursor);
|
|
853
|
+
if (parsed === null) throw new Error("GitHub cursor must be a positive page number.");
|
|
854
|
+
return parsed;
|
|
3955
855
|
}
|
|
3956
856
|
function parsePositiveInteger(value) {
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
857
|
+
if (!/^\d+$/.test(value)) return null;
|
|
858
|
+
const parsed = Number(value);
|
|
859
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
3960
860
|
}
|
|
3961
861
|
function parseGithubExternalTarget(externalId) {
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
862
|
+
const match = externalId.match(/^(.+\/.+):(\d+)$/) ?? externalId.match(/^github:(\d+):(?:issue|pull-request):(\d+)$/) ?? externalId.match(/^(\d+):(\d+)$/);
|
|
863
|
+
if (!match?.[1] || !match[2] || parsePositiveInteger(match[2]) === null) return null;
|
|
864
|
+
return {
|
|
865
|
+
repository: match[1],
|
|
866
|
+
issueId: match[2]
|
|
867
|
+
};
|
|
3965
868
|
}
|
|
3966
869
|
function optionalPositiveIntegerEnv(name) {
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
870
|
+
const value = process.env[name]?.trim();
|
|
871
|
+
if (!value) return void 0;
|
|
872
|
+
const parsed = parsePositiveInteger(value);
|
|
873
|
+
if (parsed === null) throw new Error(`${name} must be a positive integer.`);
|
|
874
|
+
return parsed;
|
|
3972
875
|
}
|
|
3973
876
|
function requirePositiveId(value, resource) {
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
877
|
+
const parsed = parsePositiveInteger(value);
|
|
878
|
+
if (parsed === null) throw new Error(`GitHub ${resource} id must be a positive integer.`);
|
|
879
|
+
return parsed;
|
|
3977
880
|
}
|
|
3978
881
|
function reviewEvent(event) {
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
882
|
+
if (event === "approve") return "APPROVE";
|
|
883
|
+
if (event === "request-changes") return "REQUEST_CHANGES";
|
|
884
|
+
return "COMMENT";
|
|
3982
885
|
}
|
|
3983
886
|
function isNotFound(error) {
|
|
3984
|
-
|
|
887
|
+
return error instanceof PlatformApiError && error.status === 404;
|
|
3985
888
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
889
|
+
//#endregion
|
|
890
|
+
export { PlatformGithubIntegration };
|
|
891
|
+
|
|
3989
892
|
//# sourceMappingURL=integration.js.map
|