@mastra/factory 0.2.1-alpha.2 → 0.2.1-alpha.4
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 +22 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3462 -3583
- package/dist/factory.js.map +1 -1
- package/dist/index.js +3462 -3583
- package/dist/index.js.map +1 -1
- package/dist/integrations/base.d.ts +13 -25
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +625 -217
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -4
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +113 -203
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +30 -0
- package/dist/integrations/github/rules.d.ts.map +1 -0
- package/dist/{rules/github-service.js → integrations/github/rules.js} +27 -8
- package/dist/integrations/github/rules.js.map +1 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +352 -3
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts +2 -2
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +2 -2
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +42 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -0
- package/dist/{rules/linear-service.js → integrations/linear/rules.js} +14 -4
- package/dist/integrations/linear/rules.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +626 -218
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +353 -3
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/routes/oauth.js +6 -39
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/surface.d.ts +3 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +230 -619
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +74 -75
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/package.json +5 -5
- package/dist/integrations/github/project-lock.d.ts +0 -100
- package/dist/integrations/github/project-lock.d.ts.map +0 -1
- package/dist/integrations/github/project-lock.js +0 -103
- package/dist/integrations/github/project-lock.js.map +0 -1
- package/dist/rules/github-service.d.ts +0 -29
- package/dist/rules/github-service.d.ts.map +0 -1
- package/dist/rules/github-service.js.map +0 -1
- package/dist/rules/linear-service.d.ts +0 -27
- package/dist/rules/linear-service.d.ts.map +0 -1
- package/dist/rules/linear-service.js.map +0 -1
|
@@ -172,95 +172,8 @@ async function clearGithubPat(storage, orgId, kind = "default") {
|
|
|
172
172
|
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// src/integrations/github/project-lock.ts
|
|
176
|
-
import { createHash } from "crypto";
|
|
177
|
-
var inProcessLocks = /* @__PURE__ */ new Map();
|
|
178
|
-
var DEFAULT_PROJECT_LOCK_TIMEOUT_MS = 6e4;
|
|
179
|
-
var ProjectLockTimeoutError = class extends Error {
|
|
180
|
-
key;
|
|
181
|
-
timeoutMs;
|
|
182
|
-
constructor(key, timeoutMs) {
|
|
183
|
-
super(`Project lock critical section for "${key}" exceeded ${timeoutMs}ms`);
|
|
184
|
-
this.name = "ProjectLockTimeoutError";
|
|
185
|
-
this.key = key;
|
|
186
|
-
this.timeoutMs = timeoutMs;
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
async function runWithTimeout(key, timeoutMs, fn) {
|
|
190
|
-
const controller = new AbortController();
|
|
191
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
192
|
-
const abortError = new Promise((_, reject) => {
|
|
193
|
-
controller.signal.addEventListener("abort", () => reject(new ProjectLockTimeoutError(key, timeoutMs)), {
|
|
194
|
-
once: true
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
const work = fn(controller.signal);
|
|
198
|
-
work.catch(() => {
|
|
199
|
-
});
|
|
200
|
-
try {
|
|
201
|
-
return await Promise.race([work, abortError]);
|
|
202
|
-
} finally {
|
|
203
|
-
clearTimeout(timer);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
function hashKey(key) {
|
|
207
|
-
const digest = createHash("sha256").update(key).digest();
|
|
208
|
-
const a = digest.readInt32BE(0);
|
|
209
|
-
const b = digest.readInt32BE(4);
|
|
210
|
-
return [a, b];
|
|
211
|
-
}
|
|
212
|
-
function withProjectLock(options) {
|
|
213
|
-
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
214
|
-
const prev = inProcessLocks.get(key) ?? Promise.resolve();
|
|
215
|
-
const run = () => withDbAdvisoryLock({ key, storage, fn, pool, timeoutMs });
|
|
216
|
-
const next = prev.then(run, run);
|
|
217
|
-
const tail = next.then(
|
|
218
|
-
() => void 0,
|
|
219
|
-
() => void 0
|
|
220
|
-
);
|
|
221
|
-
inProcessLocks.set(key, tail);
|
|
222
|
-
void tail.then(() => {
|
|
223
|
-
if (inProcessLocks.get(key) === tail) {
|
|
224
|
-
inProcessLocks.delete(key);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
return next;
|
|
228
|
-
}
|
|
229
|
-
async function withDbAdvisoryLock(options) {
|
|
230
|
-
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
231
|
-
if (process.env.MASTRACODE_DISTRIBUTED_LOCK === "0") {
|
|
232
|
-
return runWithTimeout(key, timeoutMs, fn);
|
|
233
|
-
}
|
|
234
|
-
if (pool) return advisoryLockOver(pool, key, timeoutMs, fn);
|
|
235
|
-
if (typeof storage?.withDistributedLock !== "function") {
|
|
236
|
-
return runWithTimeout(key, timeoutMs, fn);
|
|
237
|
-
}
|
|
238
|
-
return storage.withDistributedLock(key, () => runWithTimeout(key, timeoutMs, fn));
|
|
239
|
-
}
|
|
240
|
-
async function advisoryLockOver(pool, key, timeoutMs, fn) {
|
|
241
|
-
const [k1, k2] = hashKey(key);
|
|
242
|
-
const client = await pool.connect();
|
|
243
|
-
try {
|
|
244
|
-
await client.query("BEGIN");
|
|
245
|
-
await client.query("SELECT pg_advisory_xact_lock($1, $2)", [k1, k2]);
|
|
246
|
-
try {
|
|
247
|
-
const result = await runWithTimeout(key, timeoutMs, fn);
|
|
248
|
-
await client.query("COMMIT");
|
|
249
|
-
return result;
|
|
250
|
-
} catch (err) {
|
|
251
|
-
try {
|
|
252
|
-
await client.query("ROLLBACK");
|
|
253
|
-
} catch {
|
|
254
|
-
}
|
|
255
|
-
throw err;
|
|
256
|
-
}
|
|
257
|
-
} finally {
|
|
258
|
-
client.release();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
175
|
// src/integrations/github/sandbox.ts
|
|
263
|
-
import { createHash
|
|
176
|
+
import { createHash } from "crypto";
|
|
264
177
|
function bindingStore(row, storage) {
|
|
265
178
|
return {
|
|
266
179
|
sandboxId: row.sandboxId,
|
|
@@ -456,7 +369,7 @@ var WorktreeError = class extends Error {
|
|
|
456
369
|
function safeBranchDir(branch) {
|
|
457
370
|
const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
|
|
458
371
|
if (sanitized === branch) return sanitized;
|
|
459
|
-
const hash =
|
|
372
|
+
const hash = createHash("sha256").update(branch).digest("hex").slice(0, 8);
|
|
460
373
|
return `${sanitized}-${hash}`;
|
|
461
374
|
}
|
|
462
375
|
function computeWorktreePath(repoWorkdir, branch) {
|
|
@@ -878,6 +791,20 @@ async function handleGithubWebhook(c, options) {
|
|
|
878
791
|
}
|
|
879
792
|
|
|
880
793
|
// src/integrations/github/routes.ts
|
|
794
|
+
var sessionOperationLocks = /* @__PURE__ */ new Map();
|
|
795
|
+
function withSessionOperationLock(sessionId, fn) {
|
|
796
|
+
const previous = sessionOperationLocks.get(sessionId) ?? Promise.resolve();
|
|
797
|
+
const next = previous.then(fn, fn);
|
|
798
|
+
const tail = next.then(
|
|
799
|
+
() => void 0,
|
|
800
|
+
() => void 0
|
|
801
|
+
);
|
|
802
|
+
sessionOperationLocks.set(sessionId, tail);
|
|
803
|
+
void tail.then(() => {
|
|
804
|
+
if (sessionOperationLocks.get(sessionId) === tail) sessionOperationLocks.delete(sessionId);
|
|
805
|
+
});
|
|
806
|
+
return next;
|
|
807
|
+
}
|
|
881
808
|
function loose(c) {
|
|
882
809
|
return c;
|
|
883
810
|
}
|
|
@@ -897,8 +824,8 @@ function pullRequestNumberFromUrl(value, expectedRepo) {
|
|
|
897
824
|
if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) {
|
|
898
825
|
return void 0;
|
|
899
826
|
}
|
|
900
|
-
const
|
|
901
|
-
return Number.isInteger(
|
|
827
|
+
const number2 = Number(match[2]);
|
|
828
|
+
return Number.isInteger(number2) && number2 > 0 ? number2 : void 0;
|
|
902
829
|
} catch {
|
|
903
830
|
return void 0;
|
|
904
831
|
}
|
|
@@ -1562,7 +1489,7 @@ function buildGithubRoutes(options) {
|
|
|
1562
1489
|
}
|
|
1563
1490
|
})
|
|
1564
1491
|
);
|
|
1565
|
-
routes.push(...buildProjectGitRoutes({ github, auth, fleet,
|
|
1492
|
+
routes.push(...buildProjectGitRoutes({ github, auth, fleet, emitAudit }));
|
|
1566
1493
|
return routes;
|
|
1567
1494
|
}
|
|
1568
1495
|
async function loadOrgProject(options) {
|
|
@@ -1679,7 +1606,6 @@ function buildProjectGitRoutes({
|
|
|
1679
1606
|
github,
|
|
1680
1607
|
auth,
|
|
1681
1608
|
fleet,
|
|
1682
|
-
storage,
|
|
1683
1609
|
emitAudit
|
|
1684
1610
|
}) {
|
|
1685
1611
|
return [
|
|
@@ -1800,31 +1726,27 @@ function buildProjectGitRoutes({
|
|
|
1800
1726
|
}
|
|
1801
1727
|
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1802
1728
|
try {
|
|
1803
|
-
return await
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
metadata: { sessionId: sessionWorkspace.session.sessionId }
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
return c.json({ committed: result.committed });
|
|
1729
|
+
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1730
|
+
const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
|
|
1731
|
+
const result = await commitAll(
|
|
1732
|
+
sandbox,
|
|
1733
|
+
workdir,
|
|
1734
|
+
body.message,
|
|
1735
|
+
identityFromUser(await auth.ensureUser(loose(c)))
|
|
1736
|
+
);
|
|
1737
|
+
if (result.committed) {
|
|
1738
|
+
await emitAudit?.({
|
|
1739
|
+
context: loose(c),
|
|
1740
|
+
input: {
|
|
1741
|
+
action: "factory.git.commit",
|
|
1742
|
+
factoryProjectId: project.factoryProjectId,
|
|
1743
|
+
projectRepositoryId: project.id,
|
|
1744
|
+
targets: [{ type: "session", id: sessionWorkspace.session.sessionId }],
|
|
1745
|
+
metadata: { sessionId: sessionWorkspace.session.sessionId }
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1827
1748
|
}
|
|
1749
|
+
return c.json({ committed: result.committed });
|
|
1828
1750
|
});
|
|
1829
1751
|
} catch (err) {
|
|
1830
1752
|
return gitErrorResponse(loose(c), err);
|
|
@@ -1855,29 +1777,25 @@ function buildProjectGitRoutes({
|
|
|
1855
1777
|
}
|
|
1856
1778
|
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1857
1779
|
try {
|
|
1858
|
-
return await
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
}
|
|
1878
|
-
});
|
|
1879
|
-
return c.json({ pushed: true, branch });
|
|
1880
|
-
}
|
|
1780
|
+
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1781
|
+
const sandbox = await resolveProjectSandbox({ fleet, sandboxRow: sandboxBinding });
|
|
1782
|
+
const access = await github.versionControl.getRepositoryAccess({
|
|
1783
|
+
orgId,
|
|
1784
|
+
repositoryId: project.repository.id
|
|
1785
|
+
});
|
|
1786
|
+
if (!access.authorization) throw new Error("Repository access did not include a bearer token.");
|
|
1787
|
+
await pushBranch(sandbox, workdir, branch, access.authorization.token, project.repository.slug);
|
|
1788
|
+
await emitAudit?.({
|
|
1789
|
+
context: loose(c),
|
|
1790
|
+
input: {
|
|
1791
|
+
action: "factory.git.push",
|
|
1792
|
+
factoryProjectId: project.factoryProjectId,
|
|
1793
|
+
projectRepositoryId: project.id,
|
|
1794
|
+
targets: [{ type: "branch", id: branch }],
|
|
1795
|
+
metadata: { branch, sessionId: sessionWorkspace.session.sessionId }
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
return c.json({ pushed: true, branch });
|
|
1881
1799
|
});
|
|
1882
1800
|
} catch (err) {
|
|
1883
1801
|
return gitErrorResponse(loose(c), err);
|
|
@@ -1919,60 +1837,56 @@ function buildProjectGitRoutes({
|
|
|
1919
1837
|
return c.json({ error: "Invalid sessionId" }, 400);
|
|
1920
1838
|
}
|
|
1921
1839
|
try {
|
|
1922
|
-
return await
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1840
|
+
return await withSessionOperationLock(sessionWorkspace.session.sessionId, async () => {
|
|
1841
|
+
const result = await github.versionControl.createPullRequest({
|
|
1842
|
+
connection: {
|
|
1843
|
+
type: "app-installation",
|
|
1844
|
+
installationId: Number(project.installation.externalId)
|
|
1845
|
+
},
|
|
1846
|
+
sourceId: project.repository.slug,
|
|
1847
|
+
baseBranch: base,
|
|
1848
|
+
headBranch: head,
|
|
1849
|
+
title,
|
|
1850
|
+
body: prBody,
|
|
1851
|
+
actingUserId: userId
|
|
1852
|
+
});
|
|
1853
|
+
await emitAudit?.({
|
|
1854
|
+
context: loose(c),
|
|
1855
|
+
input: {
|
|
1856
|
+
action: "factory.git.pr_opened",
|
|
1857
|
+
factoryProjectId: project.factoryProjectId,
|
|
1858
|
+
projectRepositoryId: project.id,
|
|
1859
|
+
targets: [{ type: "pull_request", id: result.url, name: title }],
|
|
1860
|
+
metadata: { branch: head, base, url: result.url }
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
|
|
1864
|
+
if (pullRequestNumber) {
|
|
1865
|
+
const sessionId = sessionWorkspace.session.sessionId;
|
|
1866
|
+
await subscribeToPullRequest(
|
|
1867
|
+
{
|
|
1868
|
+
orgId,
|
|
1869
|
+
installationExternalId: project.installation.externalId,
|
|
1943
1870
|
projectRepositoryId: project.id,
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1871
|
+
repositoryExternalId: project.repository.externalId,
|
|
1872
|
+
repositorySlug: project.repository.slug,
|
|
1873
|
+
changeRequestId: pullRequestNumber.toString(),
|
|
1874
|
+
sessionId,
|
|
1875
|
+
ownerId: userId,
|
|
1876
|
+
resourceId: sessionId,
|
|
1877
|
+
threadId: sessionId,
|
|
1878
|
+
source: "factory-pr-create",
|
|
1879
|
+
subscribedByUserId: userId
|
|
1880
|
+
},
|
|
1881
|
+
github.integrationStorage
|
|
1882
|
+
).catch((error) => {
|
|
1883
|
+
console.warn(
|
|
1884
|
+
`[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
|
|
1885
|
+
error
|
|
1886
|
+
);
|
|
1947
1887
|
});
|
|
1948
|
-
const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
|
|
1949
|
-
if (pullRequestNumber) {
|
|
1950
|
-
const sessionId = sessionWorkspace.session.sessionId;
|
|
1951
|
-
await subscribeToPullRequest(
|
|
1952
|
-
{
|
|
1953
|
-
orgId,
|
|
1954
|
-
installationExternalId: project.installation.externalId,
|
|
1955
|
-
projectRepositoryId: project.id,
|
|
1956
|
-
repositoryExternalId: project.repository.externalId,
|
|
1957
|
-
repositorySlug: project.repository.slug,
|
|
1958
|
-
changeRequestId: pullRequestNumber.toString(),
|
|
1959
|
-
sessionId,
|
|
1960
|
-
ownerId: userId,
|
|
1961
|
-
resourceId: sessionId,
|
|
1962
|
-
threadId: sessionId,
|
|
1963
|
-
source: "factory-pr-create",
|
|
1964
|
-
subscribedByUserId: userId
|
|
1965
|
-
},
|
|
1966
|
-
github.integrationStorage
|
|
1967
|
-
).catch((error) => {
|
|
1968
|
-
console.warn(
|
|
1969
|
-
`[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
|
|
1970
|
-
error
|
|
1971
|
-
);
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
return c.json({ url: result.url });
|
|
1975
1888
|
}
|
|
1889
|
+
return c.json({ url: result.url });
|
|
1976
1890
|
});
|
|
1977
1891
|
} catch (err) {
|
|
1978
1892
|
return c.json(
|
|
@@ -1992,24 +1906,20 @@ function buildProjectGitRoutes({
|
|
|
1992
1906
|
handler: async (c) => {
|
|
1993
1907
|
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1994
1908
|
if ("response" in owned) return owned.response;
|
|
1995
|
-
const {
|
|
1909
|
+
const { sandboxRow } = owned;
|
|
1996
1910
|
if (!sandboxRow.sandboxId) {
|
|
1997
1911
|
return c.json({ tornDown: false });
|
|
1998
1912
|
}
|
|
1999
1913
|
try {
|
|
2000
|
-
return await
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
sandbox
|
|
2010
|
-
});
|
|
2011
|
-
return c.json({ tornDown: true });
|
|
2012
|
-
}
|
|
1914
|
+
return await withSessionOperationLock(`sandbox:${sandboxRow.id}`, async () => {
|
|
1915
|
+
const sandbox = await fleet.reattachSandbox(sandboxRow.sandboxId);
|
|
1916
|
+
await teardownProjectSandbox({
|
|
1917
|
+
fleet,
|
|
1918
|
+
row: sandboxRow,
|
|
1919
|
+
storage: github.sourceControlStorage.sandboxes,
|
|
1920
|
+
sandbox
|
|
1921
|
+
});
|
|
1922
|
+
return c.json({ tornDown: true });
|
|
2013
1923
|
});
|
|
2014
1924
|
} catch (err) {
|
|
2015
1925
|
return gitErrorResponse(loose(c), err);
|
|
@@ -2041,6 +1951,503 @@ async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
|
|
|
2041
1951
|
};
|
|
2042
1952
|
}
|
|
2043
1953
|
|
|
1954
|
+
// src/rules/resolve.ts
|
|
1955
|
+
function resolveFactoryGithubRule(rules, event) {
|
|
1956
|
+
return rules.github[event]?.onEvent;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/rules/types.ts
|
|
1960
|
+
var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
|
|
1961
|
+
var FACTORY_RULE_BOARDS = ["work", "review"];
|
|
1962
|
+
|
|
1963
|
+
// src/rules/validation.ts
|
|
1964
|
+
var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
|
|
1965
|
+
var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
1966
|
+
var MAX_REASON_LENGTH = 512;
|
|
1967
|
+
var MAX_TITLE_LENGTH = 512;
|
|
1968
|
+
var MAX_MESSAGE_LENGTH = 8192;
|
|
1969
|
+
var MAX_ARGUMENTS_LENGTH = 4096;
|
|
1970
|
+
var MAX_ROLE_LENGTH = 32;
|
|
1971
|
+
var MAX_SKILL_NAME_LENGTH = 128;
|
|
1972
|
+
var MAX_SOURCE_KEY_LENGTH = 256;
|
|
1973
|
+
var MAX_URL_LENGTH = 2048;
|
|
1974
|
+
var MAX_METADATA_JSON_LENGTH = 16384;
|
|
1975
|
+
var MAX_JSON_DEPTH = 8;
|
|
1976
|
+
var MAX_JSON_COLLECTION_SIZE = 100;
|
|
1977
|
+
var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
1978
|
+
var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1979
|
+
var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
|
|
1980
|
+
var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
|
|
1981
|
+
var REJECTION_CODES = [
|
|
1982
|
+
"forbidden",
|
|
1983
|
+
"invalid_transition",
|
|
1984
|
+
"missing_binding",
|
|
1985
|
+
"stale",
|
|
1986
|
+
"timeout",
|
|
1987
|
+
"rule_error",
|
|
1988
|
+
"causal_depth_exceeded",
|
|
1989
|
+
"repeated_transition"
|
|
1990
|
+
];
|
|
1991
|
+
var FactoryRuleValidationError = class extends Error {
|
|
1992
|
+
code = "invalid_factory_rule";
|
|
1993
|
+
constructor(message) {
|
|
1994
|
+
super(message);
|
|
1995
|
+
this.name = "FactoryRuleValidationError";
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
function isPlainObject(value) {
|
|
1999
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2000
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2001
|
+
return prototype === Object.prototype || prototype === null;
|
|
2002
|
+
}
|
|
2003
|
+
function assertExactKeys(value, keys, label) {
|
|
2004
|
+
const allowed = new Set(keys);
|
|
2005
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
|
2006
|
+
throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function boundedString(value, label, max, pattern) {
|
|
2010
|
+
if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
|
|
2011
|
+
const normalized = value.trim();
|
|
2012
|
+
if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
|
|
2013
|
+
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
2014
|
+
}
|
|
2015
|
+
return normalized;
|
|
2016
|
+
}
|
|
2017
|
+
function optionalBoundedString(value, label, max) {
|
|
2018
|
+
if (value === void 0) return void 0;
|
|
2019
|
+
return boundedString(value, label, max);
|
|
2020
|
+
}
|
|
2021
|
+
function enumValue(value, allowed, label) {
|
|
2022
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
2023
|
+
throw new FactoryRuleValidationError(`${label} is invalid.`);
|
|
2024
|
+
}
|
|
2025
|
+
return value;
|
|
2026
|
+
}
|
|
2027
|
+
function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
2028
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") return value;
|
|
2029
|
+
if (typeof value === "number") {
|
|
2030
|
+
if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
|
|
2031
|
+
return value;
|
|
2032
|
+
}
|
|
2033
|
+
if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
|
|
2034
|
+
throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
|
|
2035
|
+
}
|
|
2036
|
+
if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
|
|
2037
|
+
seen.add(value);
|
|
2038
|
+
try {
|
|
2039
|
+
if (Array.isArray(value)) {
|
|
2040
|
+
if (value.length > MAX_JSON_COLLECTION_SIZE) {
|
|
2041
|
+
throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
|
|
2042
|
+
}
|
|
2043
|
+
return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
|
|
2044
|
+
}
|
|
2045
|
+
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
|
|
2046
|
+
const entries = Object.entries(value);
|
|
2047
|
+
if (entries.length > MAX_JSON_COLLECTION_SIZE) {
|
|
2048
|
+
throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
|
|
2049
|
+
}
|
|
2050
|
+
const sanitized = {};
|
|
2051
|
+
for (const [key, entry] of entries) {
|
|
2052
|
+
const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
|
|
2053
|
+
sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
|
|
2054
|
+
}
|
|
2055
|
+
return sanitized;
|
|
2056
|
+
} finally {
|
|
2057
|
+
seen.delete(value);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function sanitizeMetadata(value) {
|
|
2061
|
+
if (value === void 0) return void 0;
|
|
2062
|
+
const sanitized = normalizeFactoryRuleJsonValue(value);
|
|
2063
|
+
if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
|
|
2064
|
+
if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
|
|
2065
|
+
throw new FactoryRuleValidationError("Rule metadata is too large.");
|
|
2066
|
+
}
|
|
2067
|
+
return sanitized;
|
|
2068
|
+
}
|
|
2069
|
+
function commonCommitFields(value) {
|
|
2070
|
+
return {
|
|
2071
|
+
idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
function validateFactoryRuleDecision(value, causalDepth = 0) {
|
|
2075
|
+
if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
|
|
2076
|
+
throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
|
|
2077
|
+
}
|
|
2078
|
+
if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
|
|
2079
|
+
const type = value.type;
|
|
2080
|
+
if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
|
|
2081
|
+
switch (type) {
|
|
2082
|
+
case "reject": {
|
|
2083
|
+
assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
|
|
2084
|
+
return {
|
|
2085
|
+
type,
|
|
2086
|
+
code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
|
|
2087
|
+
reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
case "transition": {
|
|
2091
|
+
assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
|
|
2092
|
+
return {
|
|
2093
|
+
type,
|
|
2094
|
+
...commonCommitFields(value),
|
|
2095
|
+
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
|
|
2096
|
+
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
case "upsertLinkedWorkItem": {
|
|
2100
|
+
assertExactKeys(
|
|
2101
|
+
value,
|
|
2102
|
+
["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
|
|
2103
|
+
"Factory linked work item decision"
|
|
2104
|
+
);
|
|
2105
|
+
const url = value.url;
|
|
2106
|
+
if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
|
|
2107
|
+
throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
|
|
2108
|
+
}
|
|
2109
|
+
const metadata = sanitizeMetadata(value.metadata);
|
|
2110
|
+
return {
|
|
2111
|
+
type,
|
|
2112
|
+
...commonCommitFields(value),
|
|
2113
|
+
board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
|
|
2114
|
+
source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
|
|
2115
|
+
sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
|
|
2116
|
+
title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
|
|
2117
|
+
url,
|
|
2118
|
+
stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
|
|
2119
|
+
...metadata ? { metadata } : {}
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
case "invokeSkill": {
|
|
2123
|
+
assertExactKeys(
|
|
2124
|
+
value,
|
|
2125
|
+
["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
|
|
2126
|
+
"Factory invoke skill decision"
|
|
2127
|
+
);
|
|
2128
|
+
const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
|
|
2129
|
+
const precedingMessage = optionalBoundedString(
|
|
2130
|
+
value.precedingMessage,
|
|
2131
|
+
"Factory skill preceding message",
|
|
2132
|
+
MAX_MESSAGE_LENGTH
|
|
2133
|
+
);
|
|
2134
|
+
return {
|
|
2135
|
+
type,
|
|
2136
|
+
...commonCommitFields(value),
|
|
2137
|
+
role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
2138
|
+
skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
|
|
2139
|
+
...args ? { arguments: args } : {},
|
|
2140
|
+
...precedingMessage ? { precedingMessage } : {}
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
case "sendMessage": {
|
|
2144
|
+
assertExactKeys(
|
|
2145
|
+
value,
|
|
2146
|
+
["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
|
|
2147
|
+
"Factory send message decision"
|
|
2148
|
+
);
|
|
2149
|
+
const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
|
|
2150
|
+
const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
|
|
2151
|
+
if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
|
|
2152
|
+
throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
|
|
2153
|
+
}
|
|
2154
|
+
return {
|
|
2155
|
+
type,
|
|
2156
|
+
...commonCommitFields(value),
|
|
2157
|
+
role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
|
|
2158
|
+
message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
|
|
2159
|
+
...priority ? { priority } : {},
|
|
2160
|
+
...idleBehavior ? { idleBehavior } : {},
|
|
2161
|
+
...value.prepareBinding === true ? { prepareBinding: true } : {}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
case "notify": {
|
|
2165
|
+
assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
|
|
2166
|
+
const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
|
|
2167
|
+
const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
|
|
2168
|
+
return {
|
|
2169
|
+
type,
|
|
2170
|
+
...commonCommitFields(value),
|
|
2171
|
+
title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
|
|
2172
|
+
...body ? { body } : {},
|
|
2173
|
+
...level ? { level } : {}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
default:
|
|
2177
|
+
throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
function validateFactoryRuleDecisions(values, causalDepth = 0) {
|
|
2181
|
+
if (values.length > MAX_JSON_COLLECTION_SIZE) {
|
|
2182
|
+
throw new FactoryRuleValidationError("Factory rule produced too many decisions.");
|
|
2183
|
+
}
|
|
2184
|
+
const decisions = [];
|
|
2185
|
+
for (const value of values) {
|
|
2186
|
+
const decision = validateFactoryRuleDecision(value, causalDepth);
|
|
2187
|
+
if (decision.type === "reject") {
|
|
2188
|
+
throw new FactoryRuleValidationError("A rejection cannot be persisted with commit decisions.");
|
|
2189
|
+
}
|
|
2190
|
+
decisions.push(decision);
|
|
2191
|
+
}
|
|
2192
|
+
const keys = decisions.map((decision) => decision.idempotencyKey);
|
|
2193
|
+
if (new Set(keys).size !== keys.length) {
|
|
2194
|
+
throw new FactoryRuleValidationError("Factory decisions require unique idempotency keys.");
|
|
2195
|
+
}
|
|
2196
|
+
return decisions;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/integrations/github/rules.ts
|
|
2200
|
+
var TRUSTED_PERMISSIONS = /* @__PURE__ */ new Set(["write", "admin"]);
|
|
2201
|
+
var RULE_TIMEOUT_MS = 5e3;
|
|
2202
|
+
async function withRuleTimeout(promise) {
|
|
2203
|
+
let timeout;
|
|
2204
|
+
try {
|
|
2205
|
+
return await Promise.race([
|
|
2206
|
+
promise,
|
|
2207
|
+
new Promise((_, reject) => {
|
|
2208
|
+
timeout = setTimeout(() => reject(new Error("FACTORY_RULE_TIMEOUT")), RULE_TIMEOUT_MS);
|
|
2209
|
+
})
|
|
2210
|
+
]);
|
|
2211
|
+
} finally {
|
|
2212
|
+
if (timeout) clearTimeout(timeout);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
function object(value) {
|
|
2216
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2217
|
+
}
|
|
2218
|
+
function string(value) {
|
|
2219
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2220
|
+
}
|
|
2221
|
+
function number(value) {
|
|
2222
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2223
|
+
}
|
|
2224
|
+
function boolean(value) {
|
|
2225
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2226
|
+
}
|
|
2227
|
+
function eventName(parsed) {
|
|
2228
|
+
const action = string(parsed.payload.action);
|
|
2229
|
+
if (parsed.event === "issues" && action === "opened") return "issueOpened";
|
|
2230
|
+
if (parsed.event === "pull_request" && action === "opened") return "pullRequestOpened";
|
|
2231
|
+
if (parsed.event === "pull_request" && action === "synchronize") return "pullRequestUpdated";
|
|
2232
|
+
if (parsed.event === "pull_request" && action === "closed" && boolean(object(parsed.payload.pull_request)?.merged)) {
|
|
2233
|
+
return "pullRequestMerged";
|
|
2234
|
+
}
|
|
2235
|
+
if (parsed.event === "pull_request" && action === "review_requested") return "pullRequestReviewRequested";
|
|
2236
|
+
return void 0;
|
|
2237
|
+
}
|
|
2238
|
+
function canonicalSourceKey(kind, itemNumber) {
|
|
2239
|
+
return kind === "issue" ? `github-issue:${itemNumber}` : `github-pr:${itemNumber}`;
|
|
2240
|
+
}
|
|
2241
|
+
function legacySourceKey(repositoryId, kind, itemNumber) {
|
|
2242
|
+
return `github:${repositoryId}:${kind}:${itemNumber}`;
|
|
2243
|
+
}
|
|
2244
|
+
function provenanceTarget(repositoryId, pullRequestNumber) {
|
|
2245
|
+
return `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;
|
|
2246
|
+
}
|
|
2247
|
+
function workItemSource(item) {
|
|
2248
|
+
if (!item.externalSource) return "manual";
|
|
2249
|
+
return item.externalSource.type === "pull-request" ? "github-pr" : "github-issue";
|
|
2250
|
+
}
|
|
2251
|
+
function workItemSourceKey(item) {
|
|
2252
|
+
return item.externalSource?.externalId ?? null;
|
|
2253
|
+
}
|
|
2254
|
+
async function githubActor(github, input) {
|
|
2255
|
+
let trusted = false;
|
|
2256
|
+
try {
|
|
2257
|
+
const permission = await github.getRepositoryCollaboratorPermission(
|
|
2258
|
+
input.installationId,
|
|
2259
|
+
input.repository,
|
|
2260
|
+
input.login
|
|
2261
|
+
);
|
|
2262
|
+
trusted = permission !== void 0 && TRUSTED_PERMISSIONS.has(permission);
|
|
2263
|
+
} catch {
|
|
2264
|
+
trusted = false;
|
|
2265
|
+
}
|
|
2266
|
+
return { type: "github", login: input.login, trusted, factoryAuthored: input.factoryAuthored };
|
|
2267
|
+
}
|
|
2268
|
+
function pullRequestProvenance(data) {
|
|
2269
|
+
if (!data || data.kind !== "factory-pr-provenance" || typeof data.workItemId !== "string") return null;
|
|
2270
|
+
return { kind: "factory-pr-provenance", workItemId: data.workItemId };
|
|
2271
|
+
}
|
|
2272
|
+
var GithubRules = class {
|
|
2273
|
+
constructor(options) {
|
|
2274
|
+
this.options = options;
|
|
2275
|
+
}
|
|
2276
|
+
options;
|
|
2277
|
+
async ingest(parsed) {
|
|
2278
|
+
const event = eventName(parsed);
|
|
2279
|
+
const repository = object(parsed.payload.repository);
|
|
2280
|
+
const installationId = number(object(parsed.payload.installation)?.id);
|
|
2281
|
+
const repositoryId = number(repository?.id);
|
|
2282
|
+
const repositoryName = string(repository?.full_name);
|
|
2283
|
+
const login = string(object(parsed.payload.sender)?.login);
|
|
2284
|
+
if (!event || !installationId || !repositoryId || !repositoryName || !login) return { status: "ignored" };
|
|
2285
|
+
const projects = await this.options.sourceControl.projectRepositories.listByExternalRepository({
|
|
2286
|
+
installationExternalId: String(installationId),
|
|
2287
|
+
repositoryExternalId: String(repositoryId)
|
|
2288
|
+
});
|
|
2289
|
+
if (projects.length === 0) return { status: "ignored" };
|
|
2290
|
+
const results = [];
|
|
2291
|
+
for (const project of projects) {
|
|
2292
|
+
results.push(
|
|
2293
|
+
await this.#ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project)
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
if (results.some((result) => result.status === "committed")) return { status: "committed" };
|
|
2297
|
+
if (results.some((result) => result.status === "replayed")) return { status: "replayed" };
|
|
2298
|
+
return results[0] ?? { status: "ignored" };
|
|
2299
|
+
}
|
|
2300
|
+
async #ingestProject(parsed, event, installationId, repositoryId, repositoryName, login, project) {
|
|
2301
|
+
const factoryProject = await this.options.projects.get({
|
|
2302
|
+
orgId: project.orgId,
|
|
2303
|
+
id: project.factoryProjectId
|
|
2304
|
+
});
|
|
2305
|
+
if (!factoryProject) return { status: "missing" };
|
|
2306
|
+
const issue = object(parsed.payload.issue);
|
|
2307
|
+
const pullRequest = object(parsed.payload.pull_request);
|
|
2308
|
+
const issueNumber = number(issue?.number);
|
|
2309
|
+
const pullRequestNumber = number(pullRequest?.number);
|
|
2310
|
+
const provenance = pullRequestNumber ? pullRequestProvenance(
|
|
2311
|
+
(await this.options.integrationStorage.subscriptions.listByTarget(
|
|
2312
|
+
provenanceTarget(repositoryId, pullRequestNumber),
|
|
2313
|
+
{ status: "active" }
|
|
2314
|
+
)).find((subscription) => subscription.orgId === project.orgId)?.data
|
|
2315
|
+
) : null;
|
|
2316
|
+
const relatedItem = await this.#relatedItem(
|
|
2317
|
+
project.orgId,
|
|
2318
|
+
project.factoryProjectId,
|
|
2319
|
+
repositoryId,
|
|
2320
|
+
issueNumber,
|
|
2321
|
+
pullRequestNumber,
|
|
2322
|
+
string(object(pullRequest?.head)?.ref),
|
|
2323
|
+
provenance
|
|
2324
|
+
);
|
|
2325
|
+
const actor = await githubActor(this.options.github, {
|
|
2326
|
+
installationId,
|
|
2327
|
+
repository: repositoryName,
|
|
2328
|
+
login,
|
|
2329
|
+
factoryAuthored: provenance !== null
|
|
2330
|
+
});
|
|
2331
|
+
const context = {
|
|
2332
|
+
tenant: { orgId: project.orgId, projectId: project.factoryProjectId },
|
|
2333
|
+
actor,
|
|
2334
|
+
ingress: { type: "github", id: `${installationId}:${parsed.deliveryId}` },
|
|
2335
|
+
cause: `github.${event}`,
|
|
2336
|
+
causalChain: [],
|
|
2337
|
+
ruleSetVersion: this.options.rules.version,
|
|
2338
|
+
...relatedItem ? {
|
|
2339
|
+
item: {
|
|
2340
|
+
id: relatedItem.id,
|
|
2341
|
+
source: workItemSource(relatedItem),
|
|
2342
|
+
sourceKey: workItemSourceKey(relatedItem),
|
|
2343
|
+
parentWorkItemId: relatedItem.parentWorkItemId,
|
|
2344
|
+
title: relatedItem.title,
|
|
2345
|
+
url: relatedItem.externalSource?.url ?? null,
|
|
2346
|
+
stages: relatedItem.stages
|
|
2347
|
+
},
|
|
2348
|
+
board: relatedItem.externalSource?.type === "pull-request" ? "review" : "work",
|
|
2349
|
+
itemRevision: relatedItem.revision
|
|
2350
|
+
} : {},
|
|
2351
|
+
event,
|
|
2352
|
+
deliveryId: parsed.deliveryId,
|
|
2353
|
+
factory: { createdAt: factoryProject.createdAt.toISOString() },
|
|
2354
|
+
repository: { id: repositoryId, fullName: repositoryName },
|
|
2355
|
+
...issueNumber && string(issue?.title) && string(issue?.html_url) ? {
|
|
2356
|
+
issue: {
|
|
2357
|
+
number: issueNumber,
|
|
2358
|
+
title: string(issue?.title),
|
|
2359
|
+
url: string(issue?.html_url),
|
|
2360
|
+
...string(issue?.created_at) ? { createdAt: string(issue?.created_at) } : {}
|
|
2361
|
+
}
|
|
2362
|
+
} : {},
|
|
2363
|
+
...pullRequestNumber && string(pullRequest?.title) && string(pullRequest?.html_url) ? {
|
|
2364
|
+
pullRequest: {
|
|
2365
|
+
number: pullRequestNumber,
|
|
2366
|
+
title: string(pullRequest?.title),
|
|
2367
|
+
url: string(pullRequest?.html_url),
|
|
2368
|
+
...string(pullRequest?.created_at) ? { createdAt: string(pullRequest?.created_at) } : {},
|
|
2369
|
+
state: string(pullRequest?.state) === "closed" ? "closed" : "open",
|
|
2370
|
+
merged: boolean(pullRequest?.merged) ?? false,
|
|
2371
|
+
headBranch: string(object(pullRequest?.head)?.ref) ?? "",
|
|
2372
|
+
baseBranch: string(object(pullRequest?.base)?.ref) ?? ""
|
|
2373
|
+
}
|
|
2374
|
+
} : {},
|
|
2375
|
+
...object(parsed.payload.review) ? {
|
|
2376
|
+
review: {
|
|
2377
|
+
id: number(object(parsed.payload.review)?.id) ?? 0,
|
|
2378
|
+
state: string(object(parsed.payload.review)?.state) ?? "unknown",
|
|
2379
|
+
url: string(object(parsed.payload.review)?.html_url) ?? ""
|
|
2380
|
+
}
|
|
2381
|
+
} : {}
|
|
2382
|
+
};
|
|
2383
|
+
const rule = resolveFactoryGithubRule(this.options.rules, event);
|
|
2384
|
+
let decision;
|
|
2385
|
+
let decisions = [];
|
|
2386
|
+
let outcome = { status: "accepted" };
|
|
2387
|
+
try {
|
|
2388
|
+
decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : void 0;
|
|
2389
|
+
if (decision?.type === "reject") {
|
|
2390
|
+
outcome = { status: "rejected", code: decision.code, reason: decision.reason };
|
|
2391
|
+
} else if (decision) {
|
|
2392
|
+
decisions = validateFactoryRuleDecisions([decision]).map((entry) => ({ ...entry }));
|
|
2393
|
+
}
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
const timedOut = error instanceof Error && error.message === "FACTORY_RULE_TIMEOUT";
|
|
2396
|
+
outcome = {
|
|
2397
|
+
status: "rejected",
|
|
2398
|
+
code: timedOut ? "timeout" : "rule_error",
|
|
2399
|
+
reason: timedOut ? "Factory rule evaluation timed out." : error instanceof Error ? error.message.slice(0, 2e3) : "Factory GitHub rule failed."
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
const committed = await this.options.storage.commitRuleEvaluation({
|
|
2403
|
+
orgId: project.orgId,
|
|
2404
|
+
factoryProjectId: project.factoryProjectId,
|
|
2405
|
+
workItemId: relatedItem?.id ?? null,
|
|
2406
|
+
ingress: { identity: `${installationId}:${parsed.deliveryId}`, triggerType: `github.${event}` },
|
|
2407
|
+
ruleSetVersion: this.options.rules.version,
|
|
2408
|
+
expectedRevision: relatedItem?.revision ?? null,
|
|
2409
|
+
actor: { ...actor },
|
|
2410
|
+
outcome,
|
|
2411
|
+
decisions,
|
|
2412
|
+
causalChain: [],
|
|
2413
|
+
now: /* @__PURE__ */ new Date()
|
|
2414
|
+
});
|
|
2415
|
+
return { status: committed.status };
|
|
2416
|
+
}
|
|
2417
|
+
async #relatedItem(orgId, projectId, repositoryId, issueNumber, pullRequestNumber, pullRequestHeadBranch, provenance) {
|
|
2418
|
+
const items = await this.options.storage.list({ orgId, factoryProjectId: projectId });
|
|
2419
|
+
if (provenance) return items.find((item) => item.id === provenance.workItemId);
|
|
2420
|
+
if (issueNumber) {
|
|
2421
|
+
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("issue", issueNumber)) ?? items.find((item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "issue", issueNumber));
|
|
2422
|
+
}
|
|
2423
|
+
if (pullRequestNumber) {
|
|
2424
|
+
return items.find((item) => item.externalSource?.externalId === canonicalSourceKey("pull-request", pullRequestNumber)) ?? items.find(
|
|
2425
|
+
(item) => item.externalSource?.externalId === legacySourceKey(repositoryId, "pull-request", pullRequestNumber)
|
|
2426
|
+
) ?? // Provenance fallback: a PR pushed from a work item's session branch
|
|
2427
|
+
// belongs to that item even when no gh-pr-create provenance was
|
|
2428
|
+
// recorded (session predating state seeding, or the PR was opened
|
|
2429
|
+
// outside the tracked tool call). Session branches are per-item
|
|
2430
|
+
// (`factory/issue-N`), so a head-branch match is unambiguous.
|
|
2431
|
+
(pullRequestHeadBranch ? items.find(
|
|
2432
|
+
(item) => item.externalSource?.type !== "pull-request" && Object.values(item.sessions).some((session) => session.branch === pullRequestHeadBranch)
|
|
2433
|
+
) : void 0);
|
|
2434
|
+
}
|
|
2435
|
+
return void 0;
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
function attachGithubRules(github, context) {
|
|
2439
|
+
if (!context.rules) return void 0;
|
|
2440
|
+
const rules = new GithubRules({
|
|
2441
|
+
github,
|
|
2442
|
+
sourceControl: context.storage.sourceControl,
|
|
2443
|
+
integrationStorage: context.storage.generic,
|
|
2444
|
+
projects: context.storage.projects,
|
|
2445
|
+
storage: context.rules.workItems,
|
|
2446
|
+
rules: context.rules.config
|
|
2447
|
+
});
|
|
2448
|
+
return (event) => rules.ingest(event);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2044
2451
|
// src/integrations/github/session-subscriptions.ts
|
|
2045
2452
|
import { createTool } from "@mastra/core/tools";
|
|
2046
2453
|
import { z } from "zod";
|
|
@@ -2136,16 +2543,16 @@ async function subscriptionInput(target, pullRequestNumber) {
|
|
|
2136
2543
|
async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
|
|
2137
2544
|
if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
|
|
2138
2545
|
const target = await resolveSessionTarget(requestContext, github);
|
|
2139
|
-
const
|
|
2140
|
-
await verifyPullRequest(target,
|
|
2141
|
-
await subscribeToPullRequest({ ...await subscriptionInput(target,
|
|
2142
|
-
return
|
|
2546
|
+
const number2 = parsePullRequest(pullRequest, target.repository.slug);
|
|
2547
|
+
await verifyPullRequest(target, number2, github);
|
|
2548
|
+
await subscribeToPullRequest({ ...await subscriptionInput(target, number2), source }, github.integrationStorage);
|
|
2549
|
+
return number2;
|
|
2143
2550
|
}
|
|
2144
2551
|
async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
|
|
2145
2552
|
const target = await resolveSessionTarget(requestContext, github);
|
|
2146
|
-
const
|
|
2147
|
-
await unsubscribeFromPullRequest(await subscriptionInput(target,
|
|
2148
|
-
return
|
|
2553
|
+
const number2 = parsePullRequest(pullRequest, target.repository.slug);
|
|
2554
|
+
await unsubscribeFromPullRequest(await subscriptionInput(target, number2), github.integrationStorage);
|
|
2555
|
+
return number2;
|
|
2149
2556
|
}
|
|
2150
2557
|
async function refreshGithubToken(requestContext, github) {
|
|
2151
2558
|
const target = await resolveSessionTarget(requestContext, github);
|
|
@@ -2185,8 +2592,8 @@ function createGithubSubscriptionTools(requestContext, github) {
|
|
|
2185
2592
|
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.",
|
|
2186
2593
|
inputSchema: pullRequestInputSchema,
|
|
2187
2594
|
execute: async ({ pullRequest }) => {
|
|
2188
|
-
const
|
|
2189
|
-
return { subscribed: true, pullRequestNumber:
|
|
2595
|
+
const number2 = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
|
|
2596
|
+
return { subscribed: true, pullRequestNumber: number2 };
|
|
2190
2597
|
}
|
|
2191
2598
|
}),
|
|
2192
2599
|
github_unsubscribe_pr: createTool({
|
|
@@ -2194,8 +2601,8 @@ function createGithubSubscriptionTools(requestContext, github) {
|
|
|
2194
2601
|
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.",
|
|
2195
2602
|
inputSchema: pullRequestInputSchema,
|
|
2196
2603
|
execute: async ({ pullRequest }) => {
|
|
2197
|
-
const
|
|
2198
|
-
return { subscribed: false, pullRequestNumber:
|
|
2604
|
+
const number2 = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
|
|
2605
|
+
return { subscribed: false, pullRequestNumber: number2 };
|
|
2199
2606
|
}
|
|
2200
2607
|
})
|
|
2201
2608
|
};
|
|
@@ -3090,6 +3497,7 @@ var GithubIntegration = class {
|
|
|
3090
3497
|
*/
|
|
3091
3498
|
routes(ctx) {
|
|
3092
3499
|
this.#storage = ctx.storage;
|
|
3500
|
+
const ingestFactoryEvent = attachGithubRules(this, ctx);
|
|
3093
3501
|
return buildGithubRoutes({
|
|
3094
3502
|
github: this,
|
|
3095
3503
|
auth: ctx.auth,
|
|
@@ -3101,7 +3509,7 @@ var GithubIntegration = class {
|
|
|
3101
3509
|
runIssueTriage: ctx.controller ? (input) => runGithubIssueTriage({ controller: ctx.controller, input }) : void 0,
|
|
3102
3510
|
emitAudit: ctx.hooks?.emitAudit,
|
|
3103
3511
|
projects: ctx.storage.projects,
|
|
3104
|
-
ingestFactoryEvent
|
|
3512
|
+
ingestFactoryEvent
|
|
3105
3513
|
});
|
|
3106
3514
|
}
|
|
3107
3515
|
/**
|