@mastra/factory 0.5.1-alpha.0 → 0.6.0-alpha.3
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 +151 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +2 -0
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/intake.d.ts +1 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/capabilities/version-control.d.ts +3 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +10 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +15 -2
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +6 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts +14 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +61 -3
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-reconciler.d.ts +24 -0
- package/dist/integrations/github/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/github/issue-reconciler.js +104 -0
- package/dist/integrations/github/issue-reconciler.js.map +1 -0
- package/dist/integrations/github/reconcile-worker.d.ts +8 -0
- package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +38 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +65 -52
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +37 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +150 -14
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox-release.d.ts +16 -1
- package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
- package/dist/integrations/github/sandbox-release.js +42 -1
- package/dist/integrations/github/sandbox-release.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +74 -8
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.d.ts +19 -0
- package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -0
- package/dist/integrations/issue-reconcile-worker.js +98 -0
- package/dist/integrations/issue-reconcile-worker.js.map +1 -0
- package/dist/integrations/issue-reconciler.d.ts +47 -0
- package/dist/integrations/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/issue-reconciler.js +104 -0
- package/dist/integrations/issue-reconciler.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +4 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +19 -2
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.d.ts +6 -0
- package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/linear/issue-reconciler.js +64 -0
- package/dist/integrations/linear/issue-reconciler.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +1 -0
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +2 -0
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +5 -2
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.d.ts +7 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +24 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts +13 -3
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +42 -1
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.d.ts +105 -0
- package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/linear/event-worker.js +380 -0
- package/dist/integrations/platform/linear/event-worker.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +9 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +37 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +3 -2
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +37 -110
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +49 -44
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/config.d.ts +25 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +63 -1
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/fs.d.ts +16 -2
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +47 -5
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/surface.d.ts +13 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +20 -9
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/binding-context.d.ts +28 -1
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +66 -1
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.d.ts.map +1 -1
- package/dist/rules/defaults.js +71 -5
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +7 -26
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.d.ts +2 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +15 -6
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/types.d.ts +17 -2
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +2 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/session/factory-session.d.ts +116 -0
- package/dist/session/factory-session.d.ts.map +1 -0
- package/dist/session/factory-session.js +162 -0
- package/dist/session/factory-session.js.map +1 -0
- package/dist/session/filesystem-capture.d.ts +26 -0
- package/dist/session/filesystem-capture.d.ts.map +1 -0
- package/dist/session/filesystem-capture.js +75 -0
- package/dist/session/filesystem-capture.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +9 -2
- package/dist/storage/domains/audit/domain.d.ts.map +1 -1
- package/dist/storage/domains/audit/domain.js +106 -4
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/filesystem/base.d.ts +26 -0
- package/dist/storage/domains/filesystem/base.d.ts.map +1 -0
- package/dist/storage/domains/filesystem/base.js +74 -0
- package/dist/storage/domains/filesystem/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +1 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +3 -0
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +2 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +6 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +3 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +11 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +28 -0
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/factory-skills/factory-triage/SKILL.md +45 -12
- package/package.json +5 -5
- package/dist/integrations/github/factory-session.d.ts +0 -12
- package/dist/integrations/github/factory-session.d.ts.map +0 -1
- package/dist/integrations/github/factory-session.js +0 -37
- package/dist/integrations/github/factory-session.js.map +0 -1
|
@@ -13,6 +13,7 @@ const LEASE_KEY = "github:pull-request-reconcile";
|
|
|
13
13
|
var GithubReconcileWorker = class extends MastraWorker {
|
|
14
14
|
name = "github-pull-request-reconcile";
|
|
15
15
|
#reconcile;
|
|
16
|
+
#reconcileIssues;
|
|
16
17
|
#sourceControl;
|
|
17
18
|
#intervalMs;
|
|
18
19
|
#leaseTtlMs;
|
|
@@ -24,6 +25,7 @@ var GithubReconcileWorker = class extends MastraWorker {
|
|
|
24
25
|
constructor(config) {
|
|
25
26
|
super();
|
|
26
27
|
this.#reconcile = config.reconcile;
|
|
28
|
+
this.#reconcileIssues = config.reconcileIssues;
|
|
27
29
|
this.#sourceControl = config.sourceControl;
|
|
28
30
|
this.#intervalMs = config.intervalMs ?? 3e5;
|
|
29
31
|
if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) throw new Error("GitHub pull request reconcile interval must be a positive number.");
|
|
@@ -64,6 +66,21 @@ var GithubReconcileWorker = class extends MastraWorker {
|
|
|
64
66
|
}
|
|
65
67
|
async #tick() {
|
|
66
68
|
if (!(await this.#leaseProvider.acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs).catch(() => ({ acquired: false }))).acquired) return;
|
|
69
|
+
let hasLease = true;
|
|
70
|
+
const renewalTimer = setInterval(() => {
|
|
71
|
+
this.#leaseProvider.renewLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs).then((renewed) => {
|
|
72
|
+
if (!renewed) {
|
|
73
|
+
hasLease = false;
|
|
74
|
+
clearInterval(renewalTimer);
|
|
75
|
+
this.deps?.logger.warn("GitHub reconcile lease was lost mid-sweep; aborting further writes");
|
|
76
|
+
}
|
|
77
|
+
}).catch((error) => {
|
|
78
|
+
hasLease = false;
|
|
79
|
+
clearInterval(renewalTimer);
|
|
80
|
+
this.deps?.logger.warn("GitHub reconcile lease renewal failed; aborting further writes", { error: error instanceof Error ? error.message : String(error) });
|
|
81
|
+
});
|
|
82
|
+
}, Math.max(1e3, Math.floor(this.#leaseTtlMs / 3)));
|
|
83
|
+
renewalTimer.unref?.();
|
|
67
84
|
try {
|
|
68
85
|
const targets = await this.#targets();
|
|
69
86
|
if (targets.length === 0) return;
|
|
@@ -80,10 +97,30 @@ var GithubReconcileWorker = class extends MastraWorker {
|
|
|
80
97
|
});
|
|
81
98
|
else if (counts.merged > 0 || counts.closed > 0) this.deps?.logger.info("GitHub pull request reconcile replayed missed merges/closes", context);
|
|
82
99
|
else this.deps?.logger.debug("GitHub pull request reconcile sweep completed", context);
|
|
100
|
+
if (this.#reconcileIssues && hasLease) {
|
|
101
|
+
const issueStartedAt = Date.now();
|
|
102
|
+
try {
|
|
103
|
+
const { errors: issueErrors, ...issueCounts } = await this.#reconcileIssues(targets);
|
|
104
|
+
const issueContext = {
|
|
105
|
+
...issueCounts,
|
|
106
|
+
candidateRepositories: targets.length,
|
|
107
|
+
durationMs: Date.now() - issueStartedAt
|
|
108
|
+
};
|
|
109
|
+
if (issueCounts.failed > 0) this.deps?.logger.warn("GitHub issue reconcile sweep completed with failures", {
|
|
110
|
+
...issueContext,
|
|
111
|
+
errors: issueErrors
|
|
112
|
+
});
|
|
113
|
+
else if (issueCounts.updated > 0) this.deps?.logger.info("GitHub issue reconcile patched stale metadata", issueContext);
|
|
114
|
+
else this.deps?.logger.debug("GitHub issue reconcile sweep completed", issueContext);
|
|
115
|
+
} catch (issueError) {
|
|
116
|
+
this.deps?.logger.warn("GitHub issue reconcile sweep failed", { error: issueError instanceof Error ? issueError.message : String(issueError) });
|
|
117
|
+
}
|
|
118
|
+
} else if (this.#reconcileIssues && !hasLease) this.deps?.logger.debug("GitHub issue reconcile skipped: lease lost during pull-request sweep");
|
|
83
119
|
} catch (error) {
|
|
84
120
|
this.deps?.logger.warn("GitHub pull request reconcile sweep failed", { error: error instanceof Error ? error.message : String(error) });
|
|
85
121
|
} finally {
|
|
86
|
-
|
|
122
|
+
clearInterval(renewalTimer);
|
|
123
|
+
if (hasLease) await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => void 0);
|
|
87
124
|
}
|
|
88
125
|
}
|
|
89
126
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-worker.js","names":["#reconcile","#sourceControl","#intervalMs","#leaseTtlMs","#leaseOwner","#leaseProvider","#running","#schedule","#timer","#inFlight","#tick","#targets"],"sources":["../../../src/integrations/github/reconcile-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type {\n ConfiguredExternalRepositoryKey,\n ExternalRepositoryProjectTarget,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from './rules.js';\n\nexport const DEFAULT_GITHUB_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst LEASE_KEY = 'github:pull-request-reconcile';\n\n/** Storage slice the sweep needs to turn configured repositories into targets. */\nexport interface GithubReconcileRepositorySource {\n projectRepositories: {\n listConfiguredExternalKeys(): Promise<ConfiguredExternalRepositoryKey[]>;\n listByExternalRepository(args: {\n installationExternalId: string;\n repositoryExternalId: string;\n }): Promise<ExternalRepositoryProjectTarget[]>;\n };\n repositories: {\n findByExternalId(args: { orgId: string; externalId: string }): Promise<SourceControlRepository | null>;\n };\n}\n\nexport interface GithubReconcileWorkerConfig {\n reconcile: GithubPullRequestReconciler;\n sourceControl: GithubReconcileRepositorySource;\n intervalMs?: number;\n}\n\n/**\n * Periodic merge-state sweep for the self-hosted GitHub integration: webhooks\n * are the only other writer of merge state, and a deployment GitHub cannot\n * reach never receives one, so cards would stay open forever.\n */\nexport class GithubReconcileWorker extends MastraWorker {\n readonly name = 'github-pull-request-reconcile';\n\n readonly #reconcile: GithubPullRequestReconciler;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #intervalMs: number;\n readonly #leaseTtlMs: number;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n\n constructor(config: GithubReconcileWorkerConfig) {\n super();\n this.#reconcile = config.reconcile;\n this.#sourceControl = config.sourceControl;\n this.#intervalMs = config.intervalMs ?? DEFAULT_GITHUB_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('GitHub pull request reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('GithubReconcileWorker: call init() before start()');\n this.#running = true;\n this.deps.logger.info('GitHub pull request reconcile worker started', { intervalMs: this.#intervalMs });\n // Sweep on boot: a restart is exactly when webhooks were most likely missed.\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n await this.#inFlight;\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick().finally(() => {\n this.#inFlight = undefined;\n this.#schedule(this.#intervalMs);\n });\n this.#inFlight = run;\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n // Replicas share one sweep: the ingress dedupes duplicate writes, but the\n // reads still burn the installation's rate limit.\n const lease = await this.#leaseProvider\n .acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .catch(() => ({ acquired: false }));\n if (!lease.acquired) return;\n\n try {\n const targets = await this.#targets();\n if (targets.length === 0) return;\n const startedAt = Date.now();\n const { errors, ...counts } = await this.#reconcile(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep completed with failures', { ...context, errors });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.debug('GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => undefined);\n }\n }\n\n /**\n * Configured (installation, repository) pairs resolved to the numeric ids and\n * slug the sweep addresses GitHub with. A pair whose repository row is gone\n * is skipped rather than failing the sweep for the others.\n */\n async #targets(): Promise<ReconcileRepository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const targets: ReconcileRepository[] = [];\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (!Number.isSafeInteger(installationId) || !Number.isSafeInteger(repositoryId)) continue;\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n if (!orgId) continue;\n const repository = await this.#sourceControl.repositories.findByExternalId({\n orgId,\n externalId: key.repositoryExternalId,\n });\n if (!repository?.slug) continue;\n targets.push({ id: repositoryId, fullName: repository.slug, installationId });\n }\n return targets;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n"],"mappings":";;;;AAcA,MAAa,uCAAuC,IAAI;AACxD,MAAM,mBAAmB;AACzB,MAAM,YAAY;;;;;;AA2BlB,IAAa,wBAAb,cAA2C,aAAa;CACtD,OAAgB;CAEhB;CACA;CACA;CACA;CACA,cAAuB,WAAW;CAElC,WAAW;CACX;CACA;CACA,iBAAgC;CAEhC,YAAY,QAAqC;EAC/C,MAAM;EACN,KAAKA,aAAa,OAAO;EACzB,KAAKC,iBAAiB,OAAO;EAC7B,KAAKC,cAAc,OAAO,cAAA;EAC1B,IAAI,CAAC,OAAO,SAAS,KAAKA,WAAW,KAAK,KAAKA,eAAe,GAC5D,MAAM,IAAI,MAAM,mEAAmE;EAErF,KAAKC,cAAc,KAAK,IAAI,kBAAkB,KAAKD,cAAc,CAAC;CACpE;CAEA,MAAM,KAAK,MAAiC;EAC1C,MAAM,MAAM,KAAK,IAAI;EACrB,KAAKG,iBAAiB,iBAAiB,KAAK,MAAM;CACpD;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAKC,UAAU;EACnB,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,MAAM,mDAAmD;EACnF,KAAKA,WAAW;EAChB,KAAK,KAAK,OAAO,KAAK,gDAAgD,EAAE,YAAY,KAAKJ,YAAY,CAAC;EAEtG,KAAKK,UAAU,CAAC;CAClB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAKD,UAAU;EACpB,KAAKA,WAAW;EAChB,IAAI,KAAKE,QAAQ,aAAa,KAAKA,MAAM;EACzC,KAAKA,SAAS,KAAA;EACd,MAAM,KAAKC;CACb;CAEA,IAAI,YAAqB;EACvB,OAAO,KAAKH;CACd;CAEA,UAAU,SAAuB;EAC/B,IAAI,CAAC,KAAKA,UAAU;EACpB,KAAKE,SAAS,iBAAiB;GAC7B,KAAKA,SAAS,KAAA;GACd,MAAM,MAAM,KAAKE,MAAM,CAAC,CAAC,cAAc;IACrC,KAAKD,YAAY,KAAA;IACjB,KAAKF,UAAU,KAAKL,WAAW;GACjC,CAAC;GACD,KAAKO,YAAY;EACnB,GAAG,OAAO;EACV,KAAKD,OAAO,QAAQ;CACtB;CAEA,MAAME,QAAuB;EAM3B,IAAI,EAAC,MAHe,KAAKL,eACtB,aAAa,WAAW,KAAKD,aAAa,KAAKD,WAAW,CAAC,CAC3D,aAAa,EAAE,UAAU,MAAM,EAAE,EAAA,CACzB,UAAU;EAErB,IAAI;GACF,MAAM,UAAU,MAAM,KAAKQ,SAAS;GACpC,IAAI,QAAQ,WAAW,GAAG;GAC1B,MAAM,YAAY,KAAK,IAAI;GAC3B,MAAM,EAAE,QAAQ,GAAG,WAAW,MAAM,KAAKX,WAAW,OAAO;GAC3D,MAAM,UAAU;IAAE,GAAG;IAAQ,uBAAuB,QAAQ;IAAQ,YAAY,KAAK,IAAI,IAAI;GAAU;GACvG,IAAI,OAAO,SAAS,GAClB,KAAK,MAAM,OAAO,KAAK,+DAA+D;IAAE,GAAG;IAAS;GAAO,CAAC;QACvG,IAAI,OAAO,SAAS,KAAK,OAAO,SAAS,GAC9C,KAAK,MAAM,OAAO,KAAK,+DAA+D,OAAO;QAE7F,KAAK,MAAM,OAAO,MAAM,iDAAiD,OAAO;EAEpF,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,KAAK,8CAA8C,EACnE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACH,UAAU;GACR,MAAM,KAAKK,eAAe,aAAa,WAAW,KAAKD,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EAC3F;CACF;;;;;;CAOA,MAAMO,WAA2C;EAC/C,MAAM,OAAO,MAAM,KAAKV,eAAe,oBAAoB,2BAA2B;EACtF,MAAM,UAAiC,CAAC;EACxC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,iBAAiB,OAAO,IAAI,sBAAsB;GACxD,MAAM,eAAe,OAAO,IAAI,oBAAoB;GACpD,IAAI,CAAC,OAAO,cAAc,cAAc,KAAK,CAAC,OAAO,cAAc,YAAY,GAAG;GAElF,MAAM,SAAQ,MADS,KAAKA,eAAe,oBAAoB,yBAAyB,GAAG,EAAA,CACpE,EAAE,EAAE;GAC3B,IAAI,CAAC,OAAO;GACZ,MAAM,aAAa,MAAM,KAAKA,eAAe,aAAa,iBAAiB;IACzE;IACA,YAAY,IAAI;GAClB,CAAC;GACD,IAAI,CAAC,YAAY,MAAM;GACvB,QAAQ,KAAK;IAAE,IAAI;IAAc,UAAU,WAAW;IAAM;GAAe,CAAC;EAC9E;EACA,OAAO;CACT;AACF;AAEA,SAAS,iBAAiB,QAA+B;CACvD,MAAM,cAAe,OAA2E;CAChG,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,KAAK;CAC1E,OAAO,gBAAgB,MAAM,IAAI,SAAS;AAC5C"}
|
|
1
|
+
{"version":3,"file":"reconcile-worker.js","names":["#reconcile","#reconcileIssues","#sourceControl","#intervalMs","#leaseTtlMs","#leaseOwner","#leaseProvider","#running","#schedule","#timer","#inFlight","#tick","#targets"],"sources":["../../../src/integrations/github/reconcile-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type {\n ConfiguredExternalRepositoryKey,\n ExternalRepositoryProjectTarget,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIssueReconciler } from './issue-reconciler.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from './rules.js';\n\nexport const DEFAULT_GITHUB_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst LEASE_KEY = 'github:pull-request-reconcile';\n\n/** Storage slice the sweep needs to turn configured repositories into targets. */\nexport interface GithubReconcileRepositorySource {\n projectRepositories: {\n listConfiguredExternalKeys(): Promise<ConfiguredExternalRepositoryKey[]>;\n listByExternalRepository(args: {\n installationExternalId: string;\n repositoryExternalId: string;\n }): Promise<ExternalRepositoryProjectTarget[]>;\n };\n repositories: {\n findByExternalId(args: { orgId: string; externalId: string }): Promise<SourceControlRepository | null>;\n };\n}\n\nexport interface GithubReconcileWorkerConfig {\n reconcile: GithubPullRequestReconciler;\n /**\n * Optional issue-metadata sweep. When provided, the worker folds it into the\n * same tick as the PR reconciler, sharing the lease and the same\n * `ReconcileRepository` target discovery. Mirrors how webhook-driven ingress\n * flows both issues and PRs through one path.\n */\n reconcileIssues?: GithubIssueReconciler;\n sourceControl: GithubReconcileRepositorySource;\n intervalMs?: number;\n}\n\n/**\n * Periodic merge-state sweep for the self-hosted GitHub integration: webhooks\n * are the only other writer of merge state, and a deployment GitHub cannot\n * reach never receives one, so cards would stay open forever.\n */\nexport class GithubReconcileWorker extends MastraWorker {\n readonly name = 'github-pull-request-reconcile';\n\n readonly #reconcile: GithubPullRequestReconciler;\n readonly #reconcileIssues: GithubIssueReconciler | undefined;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #intervalMs: number;\n readonly #leaseTtlMs: number;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n\n constructor(config: GithubReconcileWorkerConfig) {\n super();\n this.#reconcile = config.reconcile;\n this.#reconcileIssues = config.reconcileIssues;\n this.#sourceControl = config.sourceControl;\n this.#intervalMs = config.intervalMs ?? DEFAULT_GITHUB_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('GitHub pull request reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('GithubReconcileWorker: call init() before start()');\n this.#running = true;\n this.deps.logger.info('GitHub pull request reconcile worker started', { intervalMs: this.#intervalMs });\n // Sweep on boot: a restart is exactly when webhooks were most likely missed.\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n await this.#inFlight;\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick().finally(() => {\n this.#inFlight = undefined;\n this.#schedule(this.#intervalMs);\n });\n this.#inFlight = run;\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n // Replicas share one sweep: the ingress dedupes duplicate writes, but the\n // reads still burn the installation's rate limit.\n const lease = await this.#leaseProvider\n .acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .catch(() => ({ acquired: false }));\n if (!lease.acquired) return;\n\n // Track whether this owner still holds the lease. A `renewLease` result\n // of `false` (or a renewal error) means another replica has taken the\n // lease. Once that happens the folded issue sweep must not run — it\n // would race the new owner's writes.\n let hasLease = true;\n // Folding the issue sweep into the same tick extends how long we hold the\n // lease. Renew periodically so a replica can't grab the expired lease and\n // start an overlapping sweep partway through this one.\n const renewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n hasLease = false;\n clearInterval(renewalTimer);\n this.deps?.logger.warn('GitHub reconcile lease was lost mid-sweep; aborting further writes');\n }\n })\n .catch(error => {\n hasLease = false;\n clearInterval(renewalTimer);\n this.deps?.logger.warn('GitHub reconcile lease renewal failed; aborting further writes', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.max(1_000, Math.floor(this.#leaseTtlMs / 3)),\n );\n renewalTimer.unref?.();\n\n try {\n const targets = await this.#targets();\n if (targets.length === 0) return;\n const startedAt = Date.now();\n const { errors, ...counts } = await this.#reconcile(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep completed with failures', { ...context, errors });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.debug('GitHub pull request reconcile sweep completed', context);\n }\n // Fold the issue-metadata sweep into the same tick so a single lease\n // covers both writers of card state for these repositories. Skip it\n // if the lease was lost during the PR sweep — running would race a\n // replica that already re-acquired the lease.\n if (this.#reconcileIssues && hasLease) {\n const issueStartedAt = Date.now();\n try {\n const { errors: issueErrors, ...issueCounts } = await this.#reconcileIssues(targets);\n const issueContext = {\n ...issueCounts,\n candidateRepositories: targets.length,\n durationMs: Date.now() - issueStartedAt,\n };\n if (issueCounts.failed > 0) {\n this.deps?.logger.warn('GitHub issue reconcile sweep completed with failures', {\n ...issueContext,\n errors: issueErrors,\n });\n } else if (issueCounts.updated > 0) {\n this.deps?.logger.info('GitHub issue reconcile patched stale metadata', issueContext);\n } else {\n this.deps?.logger.debug('GitHub issue reconcile sweep completed', issueContext);\n }\n } catch (issueError) {\n this.deps?.logger.warn('GitHub issue reconcile sweep failed', {\n error: issueError instanceof Error ? issueError.message : String(issueError),\n });\n }\n } else if (this.#reconcileIssues && !hasLease) {\n this.deps?.logger.debug('GitHub issue reconcile skipped: lease lost during pull-request sweep');\n }\n } catch (error) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n clearInterval(renewalTimer);\n if (hasLease) {\n await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => undefined);\n }\n }\n }\n\n /**\n * Configured (installation, repository) pairs resolved to the numeric ids and\n * slug the sweep addresses GitHub with. A pair whose repository row is gone\n * is skipped rather than failing the sweep for the others.\n */\n async #targets(): Promise<ReconcileRepository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const targets: ReconcileRepository[] = [];\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (!Number.isSafeInteger(installationId) || !Number.isSafeInteger(repositoryId)) continue;\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n if (!orgId) continue;\n const repository = await this.#sourceControl.repositories.findByExternalId({\n orgId,\n externalId: key.repositoryExternalId,\n });\n if (!repository?.slug) continue;\n targets.push({ id: repositoryId, fullName: repository.slug, installationId });\n }\n return targets;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAuD,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEhH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AASrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA4C1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAgB/G,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,iEAAiE;IACjE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC3F,6EAA6E;IAC7E,SAAS,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAuPD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CAsqB/E"}
|
|
@@ -2,11 +2,13 @@ import { clearGithubPat, getGithubPat, getGithubPatStatus, setGithubPat } from "
|
|
|
2
2
|
import { listPullRequestSubscriptionsForThread, subscribeToPullRequest } from "./subscriptions.js";
|
|
3
3
|
import { SandboxBudgetError } from "../../sandbox/fleet.js";
|
|
4
4
|
import { MaterializeError, WorktreeError, commitAll, computeWorktreePath, ensureProjectSandbox, isValidGitRef as isValidGitRef$1, materializeRepo, pushBranch, teardownProjectSandbox } from "./sandbox.js";
|
|
5
|
-
import {
|
|
5
|
+
import { reclaimDeletedSessionSandbox } from "./sandbox-release.js";
|
|
6
|
+
import { resolveFactoryDefaultModelId } from "../../session/factory-session.js";
|
|
6
7
|
import { getGithubFeatureDiagnostics, isGithubFeatureEnabled } from "./config.js";
|
|
7
8
|
import { handleGithubWebhook } from "./webhook.js";
|
|
8
9
|
import { registerApiRoute } from "@mastra/core/server";
|
|
9
10
|
import { randomUUID } from "crypto";
|
|
11
|
+
import { UniqueViolationError } from "@mastra/core/storage";
|
|
10
12
|
import { streamSSE } from "hono/streaming";
|
|
11
13
|
//#region src/integrations/github/routes.ts
|
|
12
14
|
/**
|
|
@@ -22,6 +24,9 @@ import { streamSSE } from "hono/streaming";
|
|
|
22
24
|
* so the SPA can cleanly hide all GitHub UI.
|
|
23
25
|
*/
|
|
24
26
|
const sessionOperationLocks = /* @__PURE__ */ new Map();
|
|
27
|
+
const MAX_SESSION_TITLE_LENGTH = 80;
|
|
28
|
+
const USER_SESSION_BRANCH_PREFIX = "user/session-";
|
|
29
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
25
30
|
/**
|
|
26
31
|
* Serialize same-session mutations within one Factory process. Factory sessions
|
|
27
32
|
* normally issue these operations sequentially, so this lock is probably not
|
|
@@ -41,19 +46,6 @@ function withSessionOperationLock(sessionId, fn) {
|
|
|
41
46
|
function loose(c) {
|
|
42
47
|
return c;
|
|
43
48
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve the Factory project's default model for a triage run. Best-effort:
|
|
46
|
-
* a missing project or an uninitialized storage domain simply means "no
|
|
47
|
-
* default", never a failed run.
|
|
48
|
-
*/
|
|
49
|
-
async function resolveFactoryDefaultModelId(projects, factoryProjectId) {
|
|
50
|
-
if (!projects || !factoryProjectId) return void 0;
|
|
51
|
-
try {
|
|
52
|
-
return (await projects.getById({ id: factoryProjectId }))?.defaultModelId ?? void 0;
|
|
53
|
-
} catch {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
49
|
function pullRequestNumberFromUrl(value, expectedRepo) {
|
|
58
50
|
try {
|
|
59
51
|
const url = new URL(value);
|
|
@@ -83,6 +75,12 @@ function isCanonicalGithubIssueUrl(value, repoFullName, issueNumber) {
|
|
|
83
75
|
function isValidGitRef(value) {
|
|
84
76
|
return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9_./-]+$/.test(value);
|
|
85
77
|
}
|
|
78
|
+
function isJsonObject(value) {
|
|
79
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function normalizeSessionTitle(title) {
|
|
82
|
+
return [...title.replace(/\s+/g, " ").trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join("").trimEnd() || null;
|
|
83
|
+
}
|
|
86
84
|
/**
|
|
87
85
|
* Resolve the org-scoped tenant for a GitHub request. GitHub project features
|
|
88
86
|
* are org-owned, so they require both a signed-in user and a WorkOS
|
|
@@ -166,6 +164,7 @@ async function resolveProjectRepository(args) {
|
|
|
166
164
|
}
|
|
167
165
|
function polledIssueEvent(project, issue) {
|
|
168
166
|
const repositoryId = Number(project.repository.externalId);
|
|
167
|
+
const assigneeLogins = issue.assignees ?? (issue.assignee ? [issue.assignee] : []);
|
|
169
168
|
return {
|
|
170
169
|
event: "issues",
|
|
171
170
|
deliveryId: `poll:${repositoryId}:issue:${issue.number}:${issue.createdAt}`,
|
|
@@ -182,6 +181,7 @@ function polledIssueEvent(project, issue) {
|
|
|
182
181
|
title: issue.title,
|
|
183
182
|
html_url: issue.url,
|
|
184
183
|
created_at: issue.createdAt,
|
|
184
|
+
assignees: assigneeLogins.map((login) => ({ login })),
|
|
185
185
|
labels: issue.labels.map((name) => ({ name }))
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -207,6 +207,8 @@ function polledPullRequestEvent(project, pullRequest) {
|
|
|
207
207
|
created_at: pullRequest.createdAt,
|
|
208
208
|
state: "open",
|
|
209
209
|
merged: false,
|
|
210
|
+
assignees: pullRequest.assignees.map((login) => ({ login })),
|
|
211
|
+
requested_reviewers: pullRequest.requestedReviewers.map((login) => ({ login })),
|
|
210
212
|
head: { ref: pullRequest.headBranch },
|
|
211
213
|
base: { ref: pullRequest.baseBranch }
|
|
212
214
|
}
|
|
@@ -287,10 +289,12 @@ function buildGithubRoutes(options) {
|
|
|
287
289
|
const runBoardIssueTriage = runIssueTriage ? async (input) => {
|
|
288
290
|
if (!input.resourceId || !input.projectPath) throw new Error("GitHub issue triage requires an explicit Factory project repository");
|
|
289
291
|
await github.addIssueLabels(input.installationId, input.repository, input.issueNumber, ["auto-triaged"]);
|
|
292
|
+
if (input.labels.includes("status: needs triage")) await github.removeIssueLabel(input.installationId, input.repository, input.issueNumber, "status: needs triage");
|
|
293
|
+
const labels = input.labels.filter((label) => label !== "status: needs triage");
|
|
290
294
|
return runIssueTriage({
|
|
291
295
|
...input,
|
|
292
296
|
defaultModelId: input.defaultModelId ?? await resolveFactoryDefaultModelId(options.projects, input.resourceId),
|
|
293
|
-
labels:
|
|
297
|
+
labels: labels.includes("auto-triaged") ? labels : [...labels, "auto-triaged"]
|
|
294
298
|
});
|
|
295
299
|
} : void 0;
|
|
296
300
|
routes.push(registerApiRoute("/web/github/subscriptions", {
|
|
@@ -538,6 +542,8 @@ function buildGithubRoutes(options) {
|
|
|
538
542
|
title: issue.title,
|
|
539
543
|
url: issue.url,
|
|
540
544
|
author: issue.author,
|
|
545
|
+
assignee: issue.assignee,
|
|
546
|
+
assignees: issue.assignees,
|
|
541
547
|
labels: issue.labels,
|
|
542
548
|
comments: issue.commentCount ?? 0,
|
|
543
549
|
createdAt: issue.createdAt,
|
|
@@ -645,6 +651,8 @@ function buildGithubRoutes(options) {
|
|
|
645
651
|
title: pr.title,
|
|
646
652
|
url: pr.url,
|
|
647
653
|
author: pr.author,
|
|
654
|
+
assignees: pr.assignees ?? [],
|
|
655
|
+
requestedReviewers: pr.requestedReviewers ?? [],
|
|
648
656
|
baseBranch: pr.baseBranch,
|
|
649
657
|
headBranch: pr.headBranch,
|
|
650
658
|
createdAt: pr.createdAt,
|
|
@@ -999,17 +1007,44 @@ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
|
|
|
999
1007
|
} catch {
|
|
1000
1008
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1001
1009
|
}
|
|
1002
|
-
if (!
|
|
1003
|
-
const
|
|
1010
|
+
if (!isJsonObject(body)) return c.json({ error: "Invalid JSON body" }, 400);
|
|
1011
|
+
const requestedBaseBranch = body.baseBranch;
|
|
1012
|
+
if (requestedBaseBranch !== void 0 && typeof requestedBaseBranch !== "string") return c.json({ error: "Invalid baseBranch" }, 400);
|
|
1013
|
+
const baseBranch = requestedBaseBranch ?? project.defaultBranch;
|
|
1004
1014
|
if (!isValidGitRef$1(baseBranch)) return c.json({ error: "Invalid baseBranch" }, 400);
|
|
1015
|
+
const requestedSessionId = body.sessionId;
|
|
1016
|
+
if (requestedSessionId !== void 0 && (typeof requestedSessionId !== "string" || !UUID_PATTERN.test(requestedSessionId))) return c.json({ error: "Invalid sessionId" }, 400);
|
|
1017
|
+
const sessionId = requestedSessionId ?? randomUUID();
|
|
1018
|
+
const requestedTitle = body.title;
|
|
1019
|
+
if (requestedTitle !== void 0 && typeof requestedTitle !== "string") return c.json({ error: "Invalid title" }, 400);
|
|
1020
|
+
const normalizedTitle = requestedTitle === void 0 ? null : normalizeSessionTitle(requestedTitle);
|
|
1021
|
+
const requestedBranch = body.branch;
|
|
1022
|
+
let branch;
|
|
1023
|
+
if (requestedBranch === void 0) branch = `${USER_SESSION_BRANCH_PREFIX}${sessionId}`;
|
|
1024
|
+
else if (typeof requestedBranch === "string" && isValidGitRef$1(requestedBranch)) branch = requestedBranch;
|
|
1025
|
+
else return c.json({ error: "Invalid branch" }, 400);
|
|
1026
|
+
if (requestedSessionId !== void 0) {
|
|
1027
|
+
const existing = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
|
|
1028
|
+
if (existing) {
|
|
1029
|
+
if (existing.projectRepositoryId !== project.id || existing.orgId !== orgId || existing.userId !== userId || existing.branch !== branch) return c.json({ error: "Session ID conflict" }, 409);
|
|
1030
|
+
return c.json({ session: existing });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1005
1033
|
const session = await github.sourceControlStorage.sessions.create({
|
|
1006
|
-
sessionId
|
|
1034
|
+
sessionId,
|
|
1007
1035
|
projectRepositoryId: project.id,
|
|
1008
1036
|
orgId,
|
|
1009
1037
|
userId,
|
|
1010
|
-
branch
|
|
1011
|
-
baseBranch
|
|
1038
|
+
branch,
|
|
1039
|
+
baseBranch,
|
|
1040
|
+
title: normalizedTitle
|
|
1041
|
+
}).catch(async (error) => {
|
|
1042
|
+
if (!(error instanceof UniqueViolationError) || requestedSessionId === void 0) throw error;
|
|
1043
|
+
const conflict = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
|
|
1044
|
+
if (!conflict) throw error;
|
|
1045
|
+
return conflict;
|
|
1012
1046
|
});
|
|
1047
|
+
if (requestedSessionId !== void 0 && (session.sessionId !== sessionId || session.projectRepositoryId !== project.id || session.orgId !== orgId || session.userId !== userId || session.branch !== branch)) return c.json({ error: "Session ID conflict" }, 409);
|
|
1013
1048
|
return c.json({ session });
|
|
1014
1049
|
}
|
|
1015
1050
|
}),
|
|
@@ -1032,40 +1067,18 @@ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
|
|
|
1032
1067
|
if ("response" in resolved) return resolved.response;
|
|
1033
1068
|
const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
|
|
1034
1069
|
if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) return c.json({ error: "Session not found" }, 404);
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
});
|
|
1044
|
-
await github.sourceControlStorage.sandboxPool.release({
|
|
1045
|
-
orgId: session.orgId,
|
|
1046
|
-
projectRepositoryId: session.projectRepositoryId,
|
|
1047
|
-
userId: session.userId,
|
|
1070
|
+
await github.sourceControlStorage.sessions.delete(session.id);
|
|
1071
|
+
reclaimDeletedSessionSandbox({
|
|
1072
|
+
fleet,
|
|
1073
|
+
sourceControl: github.sourceControlStorage,
|
|
1074
|
+
session
|
|
1075
|
+
}).catch((error) => {
|
|
1076
|
+
console.error("[GitHub Sessions] Failed to reclaim sandbox for deleted session", {
|
|
1077
|
+
sessionId: session.sessionId,
|
|
1048
1078
|
sandboxId: session.sandboxId,
|
|
1049
|
-
|
|
1079
|
+
error
|
|
1050
1080
|
});
|
|
1051
|
-
}
|
|
1052
|
-
let sandbox;
|
|
1053
|
-
try {
|
|
1054
|
-
sandbox = await fleet.reattachSandbox(session.sandboxId);
|
|
1055
|
-
} catch {}
|
|
1056
|
-
await fleet.teardownSandbox({
|
|
1057
|
-
sandboxId: session.sandboxId,
|
|
1058
|
-
setSandboxId: async () => {},
|
|
1059
|
-
clear: async () => {
|
|
1060
|
-
await github.sourceControlStorage.sessions.setSandbox({
|
|
1061
|
-
id: session.id,
|
|
1062
|
-
sandboxId: null,
|
|
1063
|
-
sandboxWorkdir: session.sandboxWorkdir ?? ""
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
}, sandbox);
|
|
1067
|
-
}
|
|
1068
|
-
await github.sourceControlStorage.sessions.delete(session.id);
|
|
1081
|
+
});
|
|
1069
1082
|
return c.json({ removed: true });
|
|
1070
1083
|
}
|
|
1071
1084
|
}),
|