@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
|
@@ -93,95 +93,8 @@ async function clearGithubPat(storage, orgId, kind = "default") {
|
|
|
93
93
|
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
// src/integrations/github/project-lock.ts
|
|
97
|
-
import { createHash } from "crypto";
|
|
98
|
-
var inProcessLocks = /* @__PURE__ */ new Map();
|
|
99
|
-
var DEFAULT_PROJECT_LOCK_TIMEOUT_MS = 6e4;
|
|
100
|
-
var ProjectLockTimeoutError = class extends Error {
|
|
101
|
-
key;
|
|
102
|
-
timeoutMs;
|
|
103
|
-
constructor(key, timeoutMs) {
|
|
104
|
-
super(`Project lock critical section for "${key}" exceeded ${timeoutMs}ms`);
|
|
105
|
-
this.name = "ProjectLockTimeoutError";
|
|
106
|
-
this.key = key;
|
|
107
|
-
this.timeoutMs = timeoutMs;
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
async function runWithTimeout(key, timeoutMs, fn) {
|
|
111
|
-
const controller = new AbortController();
|
|
112
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
113
|
-
const abortError = new Promise((_, reject) => {
|
|
114
|
-
controller.signal.addEventListener("abort", () => reject(new ProjectLockTimeoutError(key, timeoutMs)), {
|
|
115
|
-
once: true
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
const work = fn(controller.signal);
|
|
119
|
-
work.catch(() => {
|
|
120
|
-
});
|
|
121
|
-
try {
|
|
122
|
-
return await Promise.race([work, abortError]);
|
|
123
|
-
} finally {
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
function hashKey(key) {
|
|
128
|
-
const digest = createHash("sha256").update(key).digest();
|
|
129
|
-
const a = digest.readInt32BE(0);
|
|
130
|
-
const b = digest.readInt32BE(4);
|
|
131
|
-
return [a, b];
|
|
132
|
-
}
|
|
133
|
-
function withProjectLock(options) {
|
|
134
|
-
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
135
|
-
const prev = inProcessLocks.get(key) ?? Promise.resolve();
|
|
136
|
-
const run = () => withDbAdvisoryLock({ key, storage, fn, pool, timeoutMs });
|
|
137
|
-
const next = prev.then(run, run);
|
|
138
|
-
const tail = next.then(
|
|
139
|
-
() => void 0,
|
|
140
|
-
() => void 0
|
|
141
|
-
);
|
|
142
|
-
inProcessLocks.set(key, tail);
|
|
143
|
-
void tail.then(() => {
|
|
144
|
-
if (inProcessLocks.get(key) === tail) {
|
|
145
|
-
inProcessLocks.delete(key);
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
return next;
|
|
149
|
-
}
|
|
150
|
-
async function withDbAdvisoryLock(options) {
|
|
151
|
-
const { key, storage, fn, pool, timeoutMs = DEFAULT_PROJECT_LOCK_TIMEOUT_MS } = options;
|
|
152
|
-
if (process.env.MASTRACODE_DISTRIBUTED_LOCK === "0") {
|
|
153
|
-
return runWithTimeout(key, timeoutMs, fn);
|
|
154
|
-
}
|
|
155
|
-
if (pool) return advisoryLockOver(pool, key, timeoutMs, fn);
|
|
156
|
-
if (typeof storage?.withDistributedLock !== "function") {
|
|
157
|
-
return runWithTimeout(key, timeoutMs, fn);
|
|
158
|
-
}
|
|
159
|
-
return storage.withDistributedLock(key, () => runWithTimeout(key, timeoutMs, fn));
|
|
160
|
-
}
|
|
161
|
-
async function advisoryLockOver(pool, key, timeoutMs, fn) {
|
|
162
|
-
const [k1, k2] = hashKey(key);
|
|
163
|
-
const client = await pool.connect();
|
|
164
|
-
try {
|
|
165
|
-
await client.query("BEGIN");
|
|
166
|
-
await client.query("SELECT pg_advisory_xact_lock($1, $2)", [k1, k2]);
|
|
167
|
-
try {
|
|
168
|
-
const result = await runWithTimeout(key, timeoutMs, fn);
|
|
169
|
-
await client.query("COMMIT");
|
|
170
|
-
return result;
|
|
171
|
-
} catch (err) {
|
|
172
|
-
try {
|
|
173
|
-
await client.query("ROLLBACK");
|
|
174
|
-
} catch {
|
|
175
|
-
}
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
} finally {
|
|
179
|
-
client.release();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
96
|
// src/integrations/github/sandbox.ts
|
|
184
|
-
import { createHash
|
|
97
|
+
import { createHash } from "crypto";
|
|
185
98
|
function bindingStore(row, storage) {
|
|
186
99
|
return {
|
|
187
100
|
sandboxId: row.sandboxId,
|
|
@@ -377,7 +290,7 @@ var WorktreeError = class extends Error {
|
|
|
377
290
|
function safeBranchDir(branch) {
|
|
378
291
|
const sanitized = branch.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\/+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 100) || "work";
|
|
379
292
|
if (sanitized === branch) return sanitized;
|
|
380
|
-
const hash =
|
|
293
|
+
const hash = createHash("sha256").update(branch).digest("hex").slice(0, 8);
|
|
381
294
|
return `${sanitized}-${hash}`;
|
|
382
295
|
}
|
|
383
296
|
function computeWorktreePath(repoWorkdir, branch) {
|
|
@@ -799,6 +712,20 @@ async function handleGithubWebhook(c, options) {
|
|
|
799
712
|
}
|
|
800
713
|
|
|
801
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
|
+
}
|
|
802
729
|
function loose(c) {
|
|
803
730
|
return c;
|
|
804
731
|
}
|
|
@@ -818,8 +745,8 @@ function pullRequestNumberFromUrl(value, expectedRepo) {
|
|
|
818
745
|
if (url.protocol !== "https:" || url.hostname !== "github.com" || match?.[1]?.toLowerCase() !== expectedRepo.toLowerCase()) {
|
|
819
746
|
return void 0;
|
|
820
747
|
}
|
|
821
|
-
const
|
|
822
|
-
return Number.isInteger(
|
|
748
|
+
const number2 = Number(match[2]);
|
|
749
|
+
return Number.isInteger(number2) && number2 > 0 ? number2 : void 0;
|
|
823
750
|
} catch {
|
|
824
751
|
return void 0;
|
|
825
752
|
}
|
|
@@ -1483,7 +1410,7 @@ function buildGithubRoutes(options) {
|
|
|
1483
1410
|
}
|
|
1484
1411
|
})
|
|
1485
1412
|
);
|
|
1486
|
-
routes.push(...buildProjectGitRoutes({ github, auth, fleet,
|
|
1413
|
+
routes.push(...buildProjectGitRoutes({ github, auth, fleet, emitAudit }));
|
|
1487
1414
|
return routes;
|
|
1488
1415
|
}
|
|
1489
1416
|
async function loadOrgProject(options) {
|
|
@@ -1600,7 +1527,6 @@ function buildProjectGitRoutes({
|
|
|
1600
1527
|
github,
|
|
1601
1528
|
auth,
|
|
1602
1529
|
fleet,
|
|
1603
|
-
storage,
|
|
1604
1530
|
emitAudit
|
|
1605
1531
|
}) {
|
|
1606
1532
|
return [
|
|
@@ -1721,31 +1647,27 @@ function buildProjectGitRoutes({
|
|
|
1721
1647
|
}
|
|
1722
1648
|
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1723
1649
|
try {
|
|
1724
|
-
return await
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
metadata: { sessionId: sessionWorkspace.session.sessionId }
|
|
1744
|
-
}
|
|
1745
|
-
});
|
|
1746
|
-
}
|
|
1747
|
-
return c.json({ committed: result.committed });
|
|
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
|
+
});
|
|
1748
1669
|
}
|
|
1670
|
+
return c.json({ committed: result.committed });
|
|
1749
1671
|
});
|
|
1750
1672
|
} catch (err) {
|
|
1751
1673
|
return gitErrorResponse(loose(c), err);
|
|
@@ -1776,29 +1698,25 @@ function buildProjectGitRoutes({
|
|
|
1776
1698
|
}
|
|
1777
1699
|
const { workdir, sandboxBinding } = sessionWorkspace;
|
|
1778
1700
|
try {
|
|
1779
|
-
return await
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
}
|
|
1799
|
-
});
|
|
1800
|
-
return c.json({ pushed: true, branch });
|
|
1801
|
-
}
|
|
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 });
|
|
1802
1720
|
});
|
|
1803
1721
|
} catch (err) {
|
|
1804
1722
|
return gitErrorResponse(loose(c), err);
|
|
@@ -1840,60 +1758,56 @@ function buildProjectGitRoutes({
|
|
|
1840
1758
|
return c.json({ error: "Invalid sessionId" }, 400);
|
|
1841
1759
|
}
|
|
1842
1760
|
try {
|
|
1843
|
-
return await
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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,
|
|
1864
1791
|
projectRepositoryId: project.id,
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
+
);
|
|
1868
1808
|
});
|
|
1869
|
-
const pullRequestNumber = pullRequestNumberFromUrl(result.url, project.repository.slug);
|
|
1870
|
-
if (pullRequestNumber) {
|
|
1871
|
-
const sessionId = sessionWorkspace.session.sessionId;
|
|
1872
|
-
await subscribeToPullRequest(
|
|
1873
|
-
{
|
|
1874
|
-
orgId,
|
|
1875
|
-
installationExternalId: project.installation.externalId,
|
|
1876
|
-
projectRepositoryId: project.id,
|
|
1877
|
-
repositoryExternalId: project.repository.externalId,
|
|
1878
|
-
repositorySlug: project.repository.slug,
|
|
1879
|
-
changeRequestId: pullRequestNumber.toString(),
|
|
1880
|
-
sessionId,
|
|
1881
|
-
ownerId: userId,
|
|
1882
|
-
resourceId: sessionId,
|
|
1883
|
-
threadId: sessionId,
|
|
1884
|
-
source: "factory-pr-create",
|
|
1885
|
-
subscribedByUserId: userId
|
|
1886
|
-
},
|
|
1887
|
-
github.integrationStorage
|
|
1888
|
-
).catch((error) => {
|
|
1889
|
-
console.warn(
|
|
1890
|
-
`[GitHub] Pull request ${result.url} was created but automatic subscription failed.`,
|
|
1891
|
-
error
|
|
1892
|
-
);
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
return c.json({ url: result.url });
|
|
1896
1809
|
}
|
|
1810
|
+
return c.json({ url: result.url });
|
|
1897
1811
|
});
|
|
1898
1812
|
} catch (err) {
|
|
1899
1813
|
return c.json(
|
|
@@ -1913,24 +1827,20 @@ function buildProjectGitRoutes({
|
|
|
1913
1827
|
handler: async (c) => {
|
|
1914
1828
|
const owned = await loadOwnedProject({ github, auth, fleet, c: loose(c) });
|
|
1915
1829
|
if ("response" in owned) return owned.response;
|
|
1916
|
-
const {
|
|
1830
|
+
const { sandboxRow } = owned;
|
|
1917
1831
|
if (!sandboxRow.sandboxId) {
|
|
1918
1832
|
return c.json({ tornDown: false });
|
|
1919
1833
|
}
|
|
1920
1834
|
try {
|
|
1921
|
-
return await
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
sandbox
|
|
1931
|
-
});
|
|
1932
|
-
return c.json({ tornDown: true });
|
|
1933
|
-
}
|
|
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 });
|
|
1934
1844
|
});
|
|
1935
1845
|
} catch (err) {
|
|
1936
1846
|
return gitErrorResponse(loose(c), err);
|
|
@@ -1962,6 +1872,503 @@ async function resolveSessionWorkspace(github, projectId, userId, sessionId) {
|
|
|
1962
1872
|
};
|
|
1963
1873
|
}
|
|
1964
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
|
+
|
|
1965
2372
|
// src/integrations/github/session-subscriptions.ts
|
|
1966
2373
|
import { createTool } from "@mastra/core/tools";
|
|
1967
2374
|
import { z } from "zod";
|
|
@@ -2057,16 +2464,16 @@ async function subscriptionInput(target, pullRequestNumber) {
|
|
|
2057
2464
|
async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
|
|
2058
2465
|
if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
|
|
2059
2466
|
const target = await resolveSessionTarget(requestContext, github);
|
|
2060
|
-
const
|
|
2061
|
-
await verifyPullRequest(target,
|
|
2062
|
-
await subscribeToPullRequest({ ...await subscriptionInput(target,
|
|
2063
|
-
return
|
|
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;
|
|
2064
2471
|
}
|
|
2065
2472
|
async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
|
|
2066
2473
|
const target = await resolveSessionTarget(requestContext, github);
|
|
2067
|
-
const
|
|
2068
|
-
await unsubscribeFromPullRequest(await subscriptionInput(target,
|
|
2069
|
-
return
|
|
2474
|
+
const number2 = parsePullRequest(pullRequest, target.repository.slug);
|
|
2475
|
+
await unsubscribeFromPullRequest(await subscriptionInput(target, number2), github.integrationStorage);
|
|
2476
|
+
return number2;
|
|
2070
2477
|
}
|
|
2071
2478
|
async function refreshGithubToken(requestContext, github) {
|
|
2072
2479
|
const target = await resolveSessionTarget(requestContext, github);
|
|
@@ -2106,8 +2513,8 @@ function createGithubSubscriptionTools(requestContext, github) {
|
|
|
2106
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.",
|
|
2107
2514
|
inputSchema: pullRequestInputSchema,
|
|
2108
2515
|
execute: async ({ pullRequest }) => {
|
|
2109
|
-
const
|
|
2110
|
-
return { subscribed: true, pullRequestNumber:
|
|
2516
|
+
const number2 = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github);
|
|
2517
|
+
return { subscribed: true, pullRequestNumber: number2 };
|
|
2111
2518
|
}
|
|
2112
2519
|
}),
|
|
2113
2520
|
github_unsubscribe_pr: createTool({
|
|
@@ -2115,8 +2522,8 @@ function createGithubSubscriptionTools(requestContext, github) {
|
|
|
2115
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.",
|
|
2116
2523
|
inputSchema: pullRequestInputSchema,
|
|
2117
2524
|
execute: async ({ pullRequest }) => {
|
|
2118
|
-
const
|
|
2119
|
-
return { subscribed: false, pullRequestNumber:
|
|
2525
|
+
const number2 = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);
|
|
2526
|
+
return { subscribed: false, pullRequestNumber: number2 };
|
|
2120
2527
|
}
|
|
2121
2528
|
})
|
|
2122
2529
|
};
|
|
@@ -2844,6 +3251,7 @@ var PlatformGithubIntegration = class {
|
|
|
2844
3251
|
});
|
|
2845
3252
|
}
|
|
2846
3253
|
routes(ctx) {
|
|
3254
|
+
const ingestFactoryEvent = attachGithubRules(this, ctx);
|
|
2847
3255
|
return [
|
|
2848
3256
|
this.#statusRoute(ctx),
|
|
2849
3257
|
this.#connectRoute(ctx),
|
|
@@ -2858,7 +3266,7 @@ var PlatformGithubIntegration = class {
|
|
|
2858
3266
|
controller: ctx.controller,
|
|
2859
3267
|
projects: ctx.storage.projects,
|
|
2860
3268
|
emitAudit: ctx.hooks?.emitAudit,
|
|
2861
|
-
ingestFactoryEvent
|
|
3269
|
+
ingestFactoryEvent
|
|
2862
3270
|
}).filter(
|
|
2863
3271
|
(route) => route.path !== "/web/github/status" && route.path !== "/web/github/webhook" && !route.path.startsWith("/auth/github/")
|
|
2864
3272
|
)
|
|
@@ -3016,7 +3424,7 @@ var PlatformGithubIntegration = class {
|
|
|
3016
3424
|
controller: ctx.controller,
|
|
3017
3425
|
github: this,
|
|
3018
3426
|
storage: ctx.storage.generic,
|
|
3019
|
-
ingestFactoryEvent: ctx
|
|
3427
|
+
ingestFactoryEvent: attachGithubRules(this, ctx),
|
|
3020
3428
|
intervalMs: this.#pollingIntervalMs
|
|
3021
3429
|
})
|
|
3022
3430
|
];
|