@mastra/factory 0.6.0-alpha.1 → 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 +117 -0
- package/dist/capabilities/version-control.d.ts +1 -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 +13 -1
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/integration.d.ts +13 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +50 -2
- 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 +45 -6
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +33 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +89 -11
- package/dist/integrations/github/rules.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 +2 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +14 -1
- 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/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 +35 -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/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 +2 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +2 -1
- 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 +37 -2
- package/dist/rules/defaults.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 +7 -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/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/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 +17 -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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-reconciler.js","names":[],"sources":["../../../src/integrations/github/issue-reconciler.ts"],"sourcesContent":["import type { IntegrationContext } from '../base.js';\nimport {\n githubRulesOptions,\n reconcilableIssueNumber,\n reconciledIssueClosedEvent,\n RECONCILE_ERROR_SAMPLE_LIMIT,\n sameStrings,GithubRules\n} from './rules.js';\nimport type { GithubIssueFetcher, GithubRulesIntegration, GithubRulesOptions, ReconcileRepository } from './rules.js';\n\nexport interface GithubIssueReconcileSummary {\n /** Repositories included in the sweep. */\n repositories: number;\n /** Issue cards whose live state was fetched. */\n checked: number;\n /** Open issues with metadata patches. */\n updated: number;\n /** Closed issues replayed through the rules ingress. */\n closed: number;\n /** Errors encountered during the sweep. */\n failed: number;\n /** Error samples with context. */\n errors: Array<{ repository: string; issueNumber?: number; error: string }>;\n}\n\nexport type GithubIssueReconciler = (repositories: ReconcileRepository[]) => Promise<GithubIssueReconcileSummary>;\n\nexport function createGithubIssueReconciler(\n options: GithubRulesOptions,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler {\n const rules = new GithubRules(options);\n return async repositories => {\n const summary: GithubIssueReconcileSummary = {\n repositories: 0,\n checked: 0,\n updated: 0,\n closed: 0,\n failed: 0,\n errors: [],\n };\n\n for (const repository of repositories) {\n summary.repositories += 1;\n\n try {\n const projects = await options.sourceControl.projectRepositories.listByExternalRepository({\n installationExternalId: String(repository.installationId),\n repositoryExternalId: String(repository.id),\n });\n if (projects.length === 0) continue;\n\n // Collect issue cards: number -> items (skip terminal stages)\n const itemsByNumber = new Map<number, import('../../storage/domains/work-items/base.js').WorkItemRow[]>();\n\n for (const project of projects) {\n const items = await options.storage.list({\n orgId: project.orgId,\n factoryProjectId: project.factoryProjectId,\n });\n for (const item of items) {\n const issueNumber = reconcilableIssueNumber(item, repository);\n if (!issueNumber) continue;\n const stage = item.stages[0];\n if (stage === 'done' || stage === 'canceled') continue; // terminal, skip\n let list = itemsByNumber.get(issueNumber);\n if (!list) {\n list = [];\n itemsByNumber.set(issueNumber, list);\n }\n list.push(item);\n }\n }\n\n // Fetch and reconcile each unique issue\n for (const [issueNumber, items] of itemsByNumber) {\n try {\n const state = await fetchIssue({\n installationId: repository.installationId,\n repository: repository.fullName,\n number: issueNumber,\n });\n if (!state) continue; // missing or PR-backed\n summary.checked += 1;\n\n // Close detection: replay through rules ingress\n if (state.state === 'closed') {\n await rules.ingest(reconciledIssueClosedEvent(repository, issueNumber, state));\n summary.closed += 1;\n continue; // card transitions, skip metadata patch\n }\n\n // Metadata sync for open issues\n const desiredMetadata = {\n githubRepositoryId: repository.id,\n githubIssueNumber: issueNumber,\n state: 'open' as const,\n author: state.author,\n assignees: state.assignees ?? [],\n labels: state.labels ?? [],\n };\n\n for (const item of items) {\n const current = item.metadata ?? {};\n const metadataChanged =\n current.githubRepositoryId !== desiredMetadata.githubRepositoryId ||\n current.githubIssueNumber !== desiredMetadata.githubIssueNumber ||\n current.state !== desiredMetadata.state ||\n current.author !== desiredMetadata.author ||\n !sameStrings(current.assignees as string[] | undefined, desiredMetadata.assignees) ||\n !sameStrings(current.labels as string[] | undefined, desiredMetadata.labels);\n\n if (!metadataChanged) continue;\n\n await options.storage.update({\n orgId: item.orgId,\n id: item.id,\n userId: 'factory-rule-dispatcher',\n patch: { metadata: { ...current, ...desiredMetadata } },\n });\n summary.updated += 1;\n }\n } catch (error) {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n issueNumber,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n } catch (error) {\n summary.failed += 1;\n if (summary.errors.length < RECONCILE_ERROR_SAMPLE_LIMIT) {\n summary.errors.push({\n repository: repository.fullName,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n }\n\n return summary;\n };\n}\n\nexport function attachGithubIssueReconciler(\n github: GithubRulesIntegration,\n context: IntegrationContext,\n fetchIssue: GithubIssueFetcher,\n): GithubIssueReconciler | undefined {\n if (!context.rules) return undefined;\n const options = githubRulesOptions(github, context);\n if (!options) return undefined;\n return createGithubIssueReconciler(options, fetchIssue);\n}"],"mappings":";;AA2BA,SAAgB,4BACd,SACA,YACuB;CACvB,MAAM,QAAQ,IAAI,YAAY,OAAO;CACrC,OAAO,OAAM,iBAAgB;EAC3B,MAAM,UAAuC;GAC3C,cAAc;GACd,SAAS;GACT,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,QAAQ,CAAC;EACX;EAEA,KAAK,MAAM,cAAc,cAAc;GACrC,QAAQ,gBAAgB;GAExB,IAAI;IACF,MAAM,WAAW,MAAM,QAAQ,cAAc,oBAAoB,yBAAyB;KACxF,wBAAwB,OAAO,WAAW,cAAc;KACxD,sBAAsB,OAAO,WAAW,EAAE;IAC5C,CAAC;IACD,IAAI,SAAS,WAAW,GAAG;IAG3B,MAAM,gCAAgB,IAAI,IAA8E;IAExG,KAAK,MAAM,WAAW,UAAU;KAC9B,MAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK;MACvC,OAAO,QAAQ;MACf,kBAAkB,QAAQ;KAC5B,CAAC;KACD,KAAK,MAAM,QAAQ,OAAO;MACxB,MAAM,cAAc,wBAAwB,MAAM,UAAU;MAC5D,IAAI,CAAC,aAAa;MAClB,MAAM,QAAQ,KAAK,OAAO;MAC1B,IAAI,UAAU,UAAU,UAAU,YAAY;MAC9C,IAAI,OAAO,cAAc,IAAI,WAAW;MACxC,IAAI,CAAC,MAAM;OACT,OAAO,CAAC;OACR,cAAc,IAAI,aAAa,IAAI;MACrC;MACA,KAAK,KAAK,IAAI;KAChB;IACF;IAGA,KAAK,MAAM,CAAC,aAAa,UAAU,eACjC,IAAI;KACF,MAAM,QAAQ,MAAM,WAAW;MAC7B,gBAAgB,WAAW;MAC3B,YAAY,WAAW;MACvB,QAAQ;KACV,CAAC;KACD,IAAI,CAAC,OAAO;KACZ,QAAQ,WAAW;KAGnB,IAAI,MAAM,UAAU,UAAU;MAC5B,MAAM,MAAM,OAAO,2BAA2B,YAAY,aAAa,KAAK,CAAC;MAC7E,QAAQ,UAAU;MAClB;KACF;KAGA,MAAM,kBAAkB;MACtB,oBAAoB,WAAW;MAC/B,mBAAmB;MACnB,OAAO;MACP,QAAQ,MAAM;MACd,WAAW,MAAM,aAAa,CAAC;MAC/B,QAAQ,MAAM,UAAU,CAAC;KAC3B;KAEA,KAAK,MAAM,QAAQ,OAAO;MACxB,MAAM,UAAU,KAAK,YAAY,CAAC;MASlC,IAAI,EAPF,QAAQ,uBAAuB,gBAAgB,sBAC/C,QAAQ,sBAAsB,gBAAgB,qBAC9C,QAAQ,UAAU,gBAAgB,SAClC,QAAQ,WAAW,gBAAgB,UACnC,CAAC,YAAY,QAAQ,WAAmC,gBAAgB,SAAS,KACjF,CAAC,YAAY,QAAQ,QAAgC,gBAAgB,MAAM,IAEvD;MAEtB,MAAM,QAAQ,QAAQ,OAAO;OAC3B,OAAO,KAAK;OACZ,IAAI,KAAK;OACT,QAAQ;OACR,OAAO,EAAE,UAAU;QAAE,GAAG;QAAS,GAAG;OAAgB,EAAE;MACxD,CAAC;MACD,QAAQ,WAAW;KACrB;IACF,SAAS,OAAO;KACd,QAAQ,UAAU;KAClB,IAAI,QAAQ,OAAO,SAAA,GACjB,QAAQ,OAAO,KAAK;MAClB,YAAY,WAAW;MACvB;MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KAC9D,CAAC;IAEL;GAEJ,SAAS,OAAO;IACd,QAAQ,UAAU;IAClB,IAAI,QAAQ,OAAO,SAAA,GACjB,QAAQ,OAAO,KAAK;KAClB,YAAY,WAAW;KACvB,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IAC9D,CAAC;GAEL;EACF;EAEA,OAAO;CACT;AACF;AAEA,SAAgB,4BACd,QACA,SACA,YACmC;CACnC,IAAI,CAAC,QAAQ,OAAO,OAAO,KAAA;CAC3B,MAAM,UAAU,mBAAmB,QAAQ,OAAO;CAClD,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,OAAO,4BAA4B,SAAS,UAAU;AACxD"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MastraWorker } from '@mastra/core/worker';
|
|
2
2
|
import type { WorkerDeps } from '@mastra/core/worker';
|
|
3
3
|
import type { ConfiguredExternalRepositoryKey, ExternalRepositoryProjectTarget, SourceControlRepository } from '../../storage/domains/source-control/base.js';
|
|
4
|
+
import type { GithubIssueReconciler } from './issue-reconciler.js';
|
|
4
5
|
import type { GithubPullRequestReconciler } from './rules.js';
|
|
5
6
|
export declare const DEFAULT_GITHUB_RECONCILE_INTERVAL_MS: number;
|
|
6
7
|
/** Storage slice the sweep needs to turn configured repositories into targets. */
|
|
@@ -21,6 +22,13 @@ export interface GithubReconcileRepositorySource {
|
|
|
21
22
|
}
|
|
22
23
|
export interface GithubReconcileWorkerConfig {
|
|
23
24
|
reconcile: GithubPullRequestReconciler;
|
|
25
|
+
/**
|
|
26
|
+
* Optional issue-metadata sweep. When provided, the worker folds it into the
|
|
27
|
+
* same tick as the PR reconciler, sharing the lease and the same
|
|
28
|
+
* `ReconcileRepository` target discovery. Mirrors how webhook-driven ingress
|
|
29
|
+
* flows both issues and PRs through one path.
|
|
30
|
+
*/
|
|
31
|
+
reconcileIssues?: GithubIssueReconciler;
|
|
24
32
|
sourceControl: GithubReconcileRepositorySource;
|
|
25
33
|
intervalMs?: number;
|
|
26
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-worker.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/reconcile-worker.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,uBAAuB,EACxB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,2BAA2B,EAAuB,MAAM,YAAY,CAAC;AAEnF,eAAO,MAAM,oCAAoC,QAAa,CAAC;AAI/D,kFAAkF;AAClF,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE;QACnB,0BAA0B,IAAI,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC;QACzE,wBAAwB,CAAC,IAAI,EAAE;YAC7B,sBAAsB,EAAE,MAAM,CAAC;YAC/B,oBAAoB,EAAE,MAAM,CAAC;SAC9B,GAAG,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC;KAChD,CAAC;IACF,YAAY,EAAE;QACZ,gBAAgB,CAAC,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;KACxG,CAAC;CACH;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,2BAA2B,CAAC;IACvC,aAAa,EAAE,+BAA+B,CAAC;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;;IACrD,QAAQ,CAAC,IAAI,mCAAmC;
|
|
1
|
+
{"version":3,"file":"reconcile-worker.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/reconcile-worker.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,uBAAuB,EACxB,MAAM,8CAA8C,CAAC;AACtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,KAAK,EAAE,2BAA2B,EAAuB,MAAM,YAAY,CAAC;AAEnF,eAAO,MAAM,oCAAoC,QAAa,CAAC;AAI/D,kFAAkF;AAClF,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE;QACnB,0BAA0B,IAAI,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC;QACzE,wBAAwB,CAAC,IAAI,EAAE;YAC7B,sBAAsB,EAAE,MAAM,CAAC;YAC/B,oBAAoB,EAAE,MAAM,CAAC;SAC9B,GAAG,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC;KAChD,CAAC;IACF,YAAY,EAAE;QACZ,gBAAgB,CAAC,IAAI,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAC;KACxG,CAAC;CACH;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,2BAA2B,CAAC;IACvC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,aAAa,EAAE,+BAA+B,CAAC;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,YAAY;;IACrD,QAAQ,CAAC,IAAI,mCAAmC;gBAcpC,MAAM,EAAE,2BAA2B;IAYzC,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,IAAI,SAAS,IAAI,OAAO,CAEvB;CAsIF"}
|
|
@@ -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":"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"}
|
|
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"}
|
|
@@ -8,6 +8,7 @@ import { getGithubFeatureDiagnostics, isGithubFeatureEnabled } from "./config.js
|
|
|
8
8
|
import { handleGithubWebhook } from "./webhook.js";
|
|
9
9
|
import { registerApiRoute } from "@mastra/core/server";
|
|
10
10
|
import { randomUUID } from "crypto";
|
|
11
|
+
import { UniqueViolationError } from "@mastra/core/storage";
|
|
11
12
|
import { streamSSE } from "hono/streaming";
|
|
12
13
|
//#region src/integrations/github/routes.ts
|
|
13
14
|
/**
|
|
@@ -23,6 +24,9 @@ import { streamSSE } from "hono/streaming";
|
|
|
23
24
|
* so the SPA can cleanly hide all GitHub UI.
|
|
24
25
|
*/
|
|
25
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}$/;
|
|
26
30
|
/**
|
|
27
31
|
* Serialize same-session mutations within one Factory process. Factory sessions
|
|
28
32
|
* normally issue these operations sequentially, so this lock is probably not
|
|
@@ -71,6 +75,12 @@ function isCanonicalGithubIssueUrl(value, repoFullName, issueNumber) {
|
|
|
71
75
|
function isValidGitRef(value) {
|
|
72
76
|
return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9_./-]+$/.test(value);
|
|
73
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
|
+
}
|
|
74
84
|
/**
|
|
75
85
|
* Resolve the org-scoped tenant for a GitHub request. GitHub project features
|
|
76
86
|
* are org-owned, so they require both a signed-in user and a WorkOS
|
|
@@ -279,10 +289,12 @@ function buildGithubRoutes(options) {
|
|
|
279
289
|
const runBoardIssueTriage = runIssueTriage ? async (input) => {
|
|
280
290
|
if (!input.resourceId || !input.projectPath) throw new Error("GitHub issue triage requires an explicit Factory project repository");
|
|
281
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");
|
|
282
294
|
return runIssueTriage({
|
|
283
295
|
...input,
|
|
284
296
|
defaultModelId: input.defaultModelId ?? await resolveFactoryDefaultModelId(options.projects, input.resourceId),
|
|
285
|
-
labels:
|
|
297
|
+
labels: labels.includes("auto-triaged") ? labels : [...labels, "auto-triaged"]
|
|
286
298
|
});
|
|
287
299
|
} : void 0;
|
|
288
300
|
routes.push(registerApiRoute("/web/github/subscriptions", {
|
|
@@ -995,17 +1007,44 @@ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
|
|
|
995
1007
|
} catch {
|
|
996
1008
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
997
1009
|
}
|
|
998
|
-
if (!
|
|
999
|
-
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;
|
|
1000
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
|
+
}
|
|
1001
1033
|
const session = await github.sourceControlStorage.sessions.create({
|
|
1002
|
-
sessionId
|
|
1034
|
+
sessionId,
|
|
1003
1035
|
projectRepositoryId: project.id,
|
|
1004
1036
|
orgId,
|
|
1005
1037
|
userId,
|
|
1006
|
-
branch
|
|
1007
|
-
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;
|
|
1008
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);
|
|
1009
1048
|
return c.json({ session });
|
|
1010
1049
|
}
|
|
1011
1050
|
}),
|