@mastra/factory 0.21.1-alpha.3 → 0.22.0-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/dist/capabilities/version-control.d.ts +13 -8
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/environment/build-runner.d.ts +72 -0
- package/dist/environment/build-runner.d.ts.map +1 -0
- package/dist/environment/build-runner.js +131 -0
- package/dist/environment/build-runner.js.map +1 -0
- package/dist/environment/build-schedule.d.ts +40 -0
- package/dist/environment/build-schedule.d.ts.map +1 -0
- package/dist/environment/build-schedule.js +94 -0
- package/dist/environment/build-schedule.js.map +1 -0
- package/dist/environment/build-workflow.d.ts +62 -0
- package/dist/environment/build-workflow.d.ts.map +1 -0
- package/dist/environment/build-workflow.js +149 -0
- package/dist/environment/build-workflow.js.map +1 -0
- package/dist/environment/build.d.ts +111 -0
- package/dist/environment/build.d.ts.map +1 -0
- package/dist/environment/build.js +344 -0
- package/dist/environment/build.js.map +1 -0
- package/dist/environment/heads.d.ts +36 -0
- package/dist/environment/heads.d.ts.map +1 -0
- package/dist/environment/heads.js +60 -0
- package/dist/environment/heads.js.map +1 -0
- package/dist/factory.d.ts +18 -4
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +89 -15
- package/dist/factory.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/integrations/base.d.ts +17 -2
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/commits.d.ts +11 -0
- package/dist/integrations/github/commits.d.ts.map +1 -1
- package/dist/integrations/github/commits.js +20 -1
- package/dist/integrations/github/commits.js.map +1 -1
- package/dist/integrations/github/config.d.ts +3 -3
- package/dist/integrations/github/config.d.ts.map +1 -1
- package/dist/integrations/github/config.js +1 -1
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +2 -2
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +6 -5
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +3 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +31 -1
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +35 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +203 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +117 -62
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.d.ts +7 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -1
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/agent-tools.js +1 -1
- package/dist/integrations/gitlab/routes.d.ts +2 -2
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +8 -7
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/agent-tools.js +1 -1
- package/dist/integrations/jira/agent-tools.js +1 -1
- package/dist/integrations/linear/agent-tools.js +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +46 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/jira/agent-tools.js +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +15 -16
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.js +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +21 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/contracts.d.ts +386 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +166 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/fs.d.ts +25 -0
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +142 -46
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/projects.d.ts +6 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +344 -1
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +2 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +36 -1
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/supervisor.js +1 -1
- package/dist/routes/surface.d.ts +6 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +21 -34
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/binding-context.d.ts +1 -1
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +1 -1
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/review-source-tool.js +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +7 -2
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +1 -1
- package/dist/sandbox/callback-factory-sandbox.d.ts +15 -0
- package/dist/sandbox/callback-factory-sandbox.d.ts.map +1 -0
- package/dist/sandbox/callback-factory-sandbox.js +22 -0
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -0
- package/dist/sandbox/local-factory-sandbox.d.ts +20 -0
- package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -0
- package/dist/sandbox/local-factory-sandbox.js +27 -0
- package/dist/sandbox/local-factory-sandbox.js.map +1 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +23 -3
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +82 -40
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +130 -13
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/sandbox/workdir.d.ts +10 -0
- package/dist/sandbox/workdir.d.ts.map +1 -1
- package/dist/sandbox/workdir.js +10 -1
- package/dist/sandbox/workdir.js.map +1 -1
- package/dist/session/environment-repositories.d.ts +31 -0
- package/dist/session/environment-repositories.d.ts.map +1 -0
- package/dist/session/environment-repositories.js +89 -0
- package/dist/session/environment-repositories.js.map +1 -0
- package/dist/session/environment-state-processor.d.ts +59 -0
- package/dist/session/environment-state-processor.d.ts.map +1 -0
- package/dist/session/environment-state-processor.js +105 -0
- package/dist/session/environment-state-processor.js.map +1 -0
- package/dist/session/factory-session.d.ts +26 -35
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +34 -71
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +13 -0
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +73 -14
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/source-control-tools.d.ts +1 -1
- package/dist/session/source-control-tools.d.ts.map +1 -1
- package/dist/session/source-control-tools.js +144 -71
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/spa-static.js +2 -2
- package/dist/storage/domains/audit/agent-audit.d.ts +6 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +31 -13
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/projects/base.d.ts +115 -2
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +219 -12
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +123 -7
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +406 -33
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +19 -6
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +90 -2
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +12 -5
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/read-tools.js +1 -1
- package/dist/supervisor/write-tools.js +1 -1
- package/dist/workspace.d.ts +50 -7
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +344 -24
- package/dist/workspace.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ENVIRONMENT_BUILD_TRIGGERS, environmentBuildContext, errorReason, redactCredentials, runEnvironmentBuild } from "./build.js";
|
|
2
|
+
import { resolveProjectEnvironment } from "../workspace.js";
|
|
3
|
+
import { createStep, createWorkflow } from "@mastra/core/workflows";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/environment/build-workflow.ts
|
|
6
|
+
/**
|
|
7
|
+
* The `factory-environment-build` workflow: step one decides and starts a
|
|
8
|
+
* build (`runEnvironmentBuild`), step two polls the provider until the build
|
|
9
|
+
* settles and pins the active template on the project row. Registered by
|
|
10
|
+
* `MastraFactory.prepare()` only when the sandbox has `builds`, and driven by
|
|
11
|
+
* the Build now route, the per-project cron schedule and the GitHub push
|
|
12
|
+
* hand-off. Saving settings never starts it.
|
|
13
|
+
*/
|
|
14
|
+
const ENVIRONMENT_BUILD_WORKFLOW_ID = "factory-environment-build";
|
|
15
|
+
const DEFAULT_BUILD_POLL_MS = 15e3;
|
|
16
|
+
const MAX_BUILD_WAIT_MS = 30 * 6e4;
|
|
17
|
+
const environmentBuildInputSchema = z.object({
|
|
18
|
+
projectId: z.string().min(1),
|
|
19
|
+
trigger: z.enum(ENVIRONMENT_BUILD_TRIGGERS)
|
|
20
|
+
});
|
|
21
|
+
const startOutputSchema = z.object({
|
|
22
|
+
projectId: z.string(),
|
|
23
|
+
outcome: z.enum([
|
|
24
|
+
"started",
|
|
25
|
+
"skipped",
|
|
26
|
+
"unavailable",
|
|
27
|
+
"failed"
|
|
28
|
+
]),
|
|
29
|
+
buildId: z.string().optional(),
|
|
30
|
+
templateId: z.string().optional(),
|
|
31
|
+
reason: z.string().optional(),
|
|
32
|
+
heads: z.record(z.string(), z.string()).optional()
|
|
33
|
+
});
|
|
34
|
+
const environmentBuildOutputSchema = z.object({
|
|
35
|
+
outcome: z.enum([
|
|
36
|
+
"started",
|
|
37
|
+
"skipped",
|
|
38
|
+
"unavailable",
|
|
39
|
+
"failed"
|
|
40
|
+
]),
|
|
41
|
+
buildId: z.string().optional(),
|
|
42
|
+
templateId: z.string().optional(),
|
|
43
|
+
reason: z.string().optional(),
|
|
44
|
+
/** Where the started build ended: `ready`, `failed`, or `unknown` when the provider lost it or the wait ran out. */
|
|
45
|
+
status: z.enum([
|
|
46
|
+
"ready",
|
|
47
|
+
"failed",
|
|
48
|
+
"unknown"
|
|
49
|
+
]).optional()
|
|
50
|
+
});
|
|
51
|
+
function createEnvironmentBuildWorkflow(deps, options = {}) {
|
|
52
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
53
|
+
const pollMs = options.pollMs ?? 15e3;
|
|
54
|
+
const maxWaitMs = options.maxWaitMs ?? 18e5;
|
|
55
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
56
|
+
const start = createStep({
|
|
57
|
+
id: "start",
|
|
58
|
+
inputSchema: environmentBuildInputSchema,
|
|
59
|
+
outputSchema: startOutputSchema,
|
|
60
|
+
execute: async ({ inputData, runId }) => {
|
|
61
|
+
const outcome = await runEnvironmentBuild(deps, inputData);
|
|
62
|
+
options.onStart?.(runId, outcome);
|
|
63
|
+
return {
|
|
64
|
+
projectId: inputData.projectId,
|
|
65
|
+
...outcome
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const awaitBuild = createStep({
|
|
70
|
+
id: "await-build",
|
|
71
|
+
inputSchema: startOutputSchema,
|
|
72
|
+
outputSchema: environmentBuildOutputSchema,
|
|
73
|
+
execute: async ({ inputData }) => {
|
|
74
|
+
const { projectId, heads, ...result } = inputData;
|
|
75
|
+
if (result.outcome !== "started" || !result.buildId || !heads) return result;
|
|
76
|
+
const builds = deps.sandbox?.builds;
|
|
77
|
+
const project = await deps.projects.getById({ id: projectId });
|
|
78
|
+
if (!builds || !deps.sourceControl || !project) return {
|
|
79
|
+
...result,
|
|
80
|
+
status: "unknown"
|
|
81
|
+
};
|
|
82
|
+
const environment = await resolveProjectEnvironment(deps.sourceControl.storage, project);
|
|
83
|
+
const ctx = environmentBuildContext(project, environment, heads, deps.sourceControl.versionControl);
|
|
84
|
+
const startedAt = now().getTime();
|
|
85
|
+
try {
|
|
86
|
+
for (;;) {
|
|
87
|
+
const build = await builds.get(ctx, environment.settings, result.buildId);
|
|
88
|
+
if (build.status === "ready") {
|
|
89
|
+
const templateId = build.templateId ?? result.templateId ?? result.buildId;
|
|
90
|
+
const pinned = await deps.projects.pinActiveTemplate({
|
|
91
|
+
id: project.id,
|
|
92
|
+
buildId: result.buildId,
|
|
93
|
+
templateId,
|
|
94
|
+
heads
|
|
95
|
+
});
|
|
96
|
+
deps.logger?.info(pinned ? "environment build ready" : "environment build ready but superseded", {
|
|
97
|
+
factoryProjectId: project.id,
|
|
98
|
+
templateId
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
...result,
|
|
102
|
+
templateId,
|
|
103
|
+
status: "ready"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (build.status === "failed" || build.status === "unknown") {
|
|
107
|
+
deps.logger?.info("environment build did not complete", {
|
|
108
|
+
factoryProjectId: project.id,
|
|
109
|
+
buildId: result.buildId,
|
|
110
|
+
status: build.status,
|
|
111
|
+
...build.error ? { reason: redactCredentials(build.error) } : {}
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
...result,
|
|
115
|
+
status: build.status,
|
|
116
|
+
...build.error ? { reason: redactCredentials(build.error) } : {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (now().getTime() - startedAt >= maxWaitMs) return {
|
|
120
|
+
...result,
|
|
121
|
+
status: "unknown",
|
|
122
|
+
reason: "Build wait timed out."
|
|
123
|
+
};
|
|
124
|
+
await sleep(pollMs);
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const reason = errorReason(error);
|
|
128
|
+
deps.logger?.info("environment build status read failed", {
|
|
129
|
+
factoryProjectId: project.id,
|
|
130
|
+
reason
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
...result,
|
|
134
|
+
status: "unknown",
|
|
135
|
+
reason
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return createWorkflow({
|
|
141
|
+
id: ENVIRONMENT_BUILD_WORKFLOW_ID,
|
|
142
|
+
inputSchema: environmentBuildInputSchema,
|
|
143
|
+
outputSchema: environmentBuildOutputSchema
|
|
144
|
+
}).then(start).then(awaitBuild).commit();
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { DEFAULT_BUILD_POLL_MS, ENVIRONMENT_BUILD_WORKFLOW_ID, MAX_BUILD_WAIT_MS, createEnvironmentBuildWorkflow, environmentBuildInputSchema, environmentBuildOutputSchema };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=build-workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-workflow.js","names":[],"sources":["../../src/environment/build-workflow.ts"],"sourcesContent":["/**\n * The `factory-environment-build` workflow: step one decides and starts a\n * build (`runEnvironmentBuild`), step two polls the provider until the build\n * settles and pins the active template on the project row. Registered by\n * `MastraFactory.prepare()` only when the sandbox has `builds`, and driven by\n * the Build now route, the per-project cron schedule and the GitHub push\n * hand-off. Saving settings never starts it.\n */\n\nimport { createStep, createWorkflow } from '@mastra/core/workflows';\nimport { z } from 'zod';\nimport { resolveProjectEnvironment } from '../workspace.js';\nimport {\n ENVIRONMENT_BUILD_TRIGGERS,\n environmentBuildContext,\n errorReason,\n redactCredentials,\n runEnvironmentBuild,\n} from './build.js';\nimport type { EnvironmentBuildDeps, EnvironmentBuildOutcome } from './build.js';\n\nexport const ENVIRONMENT_BUILD_WORKFLOW_ID = 'factory-environment-build';\nexport const DEFAULT_BUILD_POLL_MS = 15_000;\nexport const MAX_BUILD_WAIT_MS = 30 * 60_000;\n\nexport const environmentBuildInputSchema = z.object({\n projectId: z.string().min(1),\n trigger: z.enum(ENVIRONMENT_BUILD_TRIGGERS),\n});\n\nconst startOutputSchema = z.object({\n projectId: z.string(),\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n heads: z.record(z.string(), z.string()).optional(),\n});\n\nexport const environmentBuildOutputSchema = z.object({\n outcome: z.enum(['started', 'skipped', 'unavailable', 'failed']),\n buildId: z.string().optional(),\n templateId: z.string().optional(),\n reason: z.string().optional(),\n /** Where the started build ended: `ready`, `failed`, or `unknown` when the provider lost it or the wait ran out. */\n status: z.enum(['ready', 'failed', 'unknown']).optional(),\n});\n\nexport type EnvironmentBuildWorkflowResult = z.infer<typeof environmentBuildOutputSchema>;\n\nexport interface EnvironmentBuildWorkflowOptions {\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n maxWaitMs?: number;\n /** Called with the first step's answer so a caller can return it without waiting for the poll. */\n onStart?: (runId: string, outcome: EnvironmentBuildOutcome) => void;\n}\n\nexport function createEnvironmentBuildWorkflow(\n deps: EnvironmentBuildDeps,\n options: EnvironmentBuildWorkflowOptions = {},\n) {\n const sleep = options.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)));\n const pollMs = options.pollMs ?? DEFAULT_BUILD_POLL_MS;\n const maxWaitMs = options.maxWaitMs ?? MAX_BUILD_WAIT_MS;\n const now = deps.now ?? (() => new Date());\n\n const start = createStep({\n id: 'start',\n inputSchema: environmentBuildInputSchema,\n outputSchema: startOutputSchema,\n execute: async ({ inputData, runId }) => {\n const outcome = await runEnvironmentBuild(deps, inputData);\n options.onStart?.(runId, outcome);\n return { projectId: inputData.projectId, ...outcome };\n },\n });\n\n const awaitBuild = createStep({\n id: 'await-build',\n inputSchema: startOutputSchema,\n outputSchema: environmentBuildOutputSchema,\n execute: async ({ inputData }): Promise<EnvironmentBuildWorkflowResult> => {\n const { projectId, heads, ...result } = inputData;\n if (result.outcome !== 'started' || !result.buildId || !heads) return result;\n const builds = deps.sandbox?.builds;\n const project = await deps.projects.getById({ id: projectId });\n if (!builds || !deps.sourceControl || !project) return { ...result, status: 'unknown' };\n const environment = await resolveProjectEnvironment(deps.sourceControl.storage, project);\n const ctx = environmentBuildContext(project, environment, heads, deps.sourceControl.versionControl);\n const startedAt = now().getTime();\n try {\n for (;;) {\n const build = await builds.get(ctx, environment.settings, result.buildId);\n if (build.status === 'ready') {\n const templateId = build.templateId ?? result.templateId ?? result.buildId;\n // A newer run may have replaced `last_build_id` while this one\n // polled; the pin is skipped then so the stale image never wins.\n const pinned = await deps.projects.pinActiveTemplate({\n id: project.id,\n buildId: result.buildId,\n templateId,\n heads,\n });\n deps.logger?.info(pinned ? 'environment build ready' : 'environment build ready but superseded', {\n factoryProjectId: project.id,\n templateId,\n });\n return { ...result, templateId, status: 'ready' };\n }\n if (build.status === 'failed' || build.status === 'unknown') {\n deps.logger?.info('environment build did not complete', {\n factoryProjectId: project.id,\n buildId: result.buildId,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n });\n return {\n ...result,\n status: build.status,\n ...(build.error ? { reason: redactCredentials(build.error) } : {}),\n };\n }\n if (now().getTime() - startedAt >= maxWaitMs) {\n return { ...result, status: 'unknown', reason: 'Build wait timed out.' };\n }\n await sleep(pollMs);\n }\n } catch (error) {\n const reason = errorReason(error);\n deps.logger?.info('environment build status read failed', { factoryProjectId: project.id, reason });\n return { ...result, status: 'unknown', reason };\n }\n },\n });\n\n return createWorkflow({\n id: ENVIRONMENT_BUILD_WORKFLOW_ID,\n inputSchema: environmentBuildInputSchema,\n outputSchema: environmentBuildOutputSchema,\n })\n .then(start)\n .then(awaitBuild)\n .commit();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One proactive environment build: decide whether a trigger should build,
|
|
3
|
+
* hand the sandbox's `builds` capability a context identical to a session's,
|
|
4
|
+
* and record the started build on the project row. Polling the build to its
|
|
5
|
+
* end and pinning the active template is the workflow's second step
|
|
6
|
+
* (`build-workflow.ts`), so a request that only needs the start answer never
|
|
7
|
+
* waits on the provider.
|
|
8
|
+
*/
|
|
9
|
+
import type { FactorySandbox, FactorySandboxContext } from '@mastra/core/workspace';
|
|
10
|
+
import type { VersionControl } from '../capabilities/version-control.js';
|
|
11
|
+
import type { FactoryProject, FactoryProjectsStorage } from '../storage/domains/projects/base.js';
|
|
12
|
+
import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
|
|
13
|
+
import type { SessionEnvironment } from '../workspace.js';
|
|
14
|
+
import type { EnvironmentHeads } from './heads.js';
|
|
15
|
+
export declare const ENVIRONMENT_BUILD_TRIGGERS: readonly ["manual", "schedule", "push"];
|
|
16
|
+
export type EnvironmentBuildTrigger = (typeof ENVIRONMENT_BUILD_TRIGGERS)[number];
|
|
17
|
+
export interface EnvironmentBuildSourceControl {
|
|
18
|
+
storage: Pick<SourceControlStorageHandle, 'projectRepositories' | 'repositories'>;
|
|
19
|
+
versionControl: Pick<VersionControl, 'getRepositoryAccess'>;
|
|
20
|
+
}
|
|
21
|
+
export interface EnvironmentBuildDeps {
|
|
22
|
+
/** The factory's normalized sandbox; builds are unavailable without its `builds`. */
|
|
23
|
+
sandbox: FactorySandbox | undefined;
|
|
24
|
+
projects: Pick<FactoryProjectsStorage, 'getById' | 'claimBuildAttempt' | 'claimBuildStart' | 'renewBuildStart' | 'releaseBuildStart' | 'pinActiveTemplate'>;
|
|
25
|
+
/** Absent when the factory has no source-control integration to resolve heads with. */
|
|
26
|
+
sourceControl: EnvironmentBuildSourceControl | undefined;
|
|
27
|
+
logger?: {
|
|
28
|
+
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
29
|
+
};
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
/** How often a run renews its build-start lease; a third of `BUILD_START_LEASE_MS` by default. */
|
|
32
|
+
leaseRenewMs?: number;
|
|
33
|
+
/** Waits between tries at recording a started build. */
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export interface EnvironmentBuildOutcome {
|
|
37
|
+
outcome: 'started' | 'skipped' | 'unavailable' | 'failed';
|
|
38
|
+
buildId?: string;
|
|
39
|
+
templateId?: string;
|
|
40
|
+
/** Why nothing started: `debounced`, `starting` (another run is starting a build), `in_progress`, `unchanged`, `unresolved` (a lapsed run may have started a build the provider cannot list), `no_environment`, `no_builds`, `no_source_control`; why a run failed: `not_recorded`, `superseded`, or an error. */
|
|
41
|
+
reason?: string;
|
|
42
|
+
/** The heads the started build pins; the workflow stores them once the build is ready. */
|
|
43
|
+
heads?: EnvironmentHeads;
|
|
44
|
+
}
|
|
45
|
+
/** The synthetic session id a project's build runs under; never a session. */
|
|
46
|
+
export declare function environmentBuildSessionId(projectId: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* A build or setup error may echo a clone URL or a token; strip URL userinfo
|
|
49
|
+
* and GitHub token shapes before the message is stored, logged or shown.
|
|
50
|
+
*/
|
|
51
|
+
export declare function redactCredentials(message: string): string;
|
|
52
|
+
/** An error's message with credentials redacted, safe to log, store or return. */
|
|
53
|
+
export declare function errorReason(error: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* The context an environment build (or a read of its status) hands the
|
|
56
|
+
* provider: the same list form a session boots with, under the build's own
|
|
57
|
+
* session id, with heads served from `heads` so the provider never resolves
|
|
58
|
+
* them itself.
|
|
59
|
+
*/
|
|
60
|
+
export declare function environmentBuildContext(project: Pick<FactoryProject, 'id' | 'orgId'>, environment: SessionEnvironment, heads: EnvironmentHeads, versionControl: Pick<VersionControl, 'getRepositoryAccess'>): FactorySandboxContext;
|
|
61
|
+
/**
|
|
62
|
+
* Fingerprint of everything a build bakes into the template besides the
|
|
63
|
+
* repository heads: provider settings, the repositories and their order,
|
|
64
|
+
* their setup commands, the workspace setup command and the working
|
|
65
|
+
* directory. A schedule or push skips a ready build only while this matches.
|
|
66
|
+
*/
|
|
67
|
+
export declare function environmentBuildInputs(environment: SessionEnvironment): string;
|
|
68
|
+
/**
|
|
69
|
+
* The current head of every environment repository's template branch, keyed
|
|
70
|
+
* by slug. The template clones the repository default branch (a link's own
|
|
71
|
+
* branch is the session's base, checked out after boot), so that is the
|
|
72
|
+
* branch a build pins; sessions let the provider resolve the same head.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveEnvironmentHeads(sourceControl: EnvironmentBuildSourceControl, project: Pick<FactoryProject, 'orgId'>, environment: SessionEnvironment): Promise<EnvironmentHeads>;
|
|
75
|
+
/**
|
|
76
|
+
* How long a build-start lease lasts without renewal. The run holding it
|
|
77
|
+
* renews it every third of that while it works, so a lease lapses only when
|
|
78
|
+
* its run died or stalled.
|
|
79
|
+
*/
|
|
80
|
+
export declare const BUILD_START_LEASE_MS: number;
|
|
81
|
+
/**
|
|
82
|
+
* Start a build for `projectId` when `trigger` calls for one. Every trigger
|
|
83
|
+
* takes the project's build-start lease before reading build state and renews
|
|
84
|
+
* it until the started build is recorded, so overlapping manual, scheduled and
|
|
85
|
+
* push runs never both start a build, however long the provider takes; a run
|
|
86
|
+
* that finds the lease held skips as `starting`. Every trigger skips while the
|
|
87
|
+
* last build is still running; `schedule` and `push` also skip when the last
|
|
88
|
+
* build is ready and neither a head nor any other template input changed;
|
|
89
|
+
* `manual` otherwise always builds. A `push` that would build then takes the
|
|
90
|
+
* leading-edge debounce claim, so only a push that starts a build opens the
|
|
91
|
+
* window. Saving settings never builds.
|
|
92
|
+
*
|
|
93
|
+
* Taking over a lapsed lease leaves the start uncertain: the lapsed run may
|
|
94
|
+
* have started a build it never recorded. The mark persists until resolved.
|
|
95
|
+
* While it stands, a provider that lists builds is asked for a running one
|
|
96
|
+
* (and every trigger skips as `in_progress` on it); with a provider that
|
|
97
|
+
* cannot list, every trigger skips as `unresolved`.
|
|
98
|
+
*
|
|
99
|
+
* The result is written as the lease is released, and only while this run
|
|
100
|
+
* still holds it. A failed `builds.start` clears the last build id, so a later
|
|
101
|
+
* `schedule` or `push` cannot mistake the previous ready build for the current
|
|
102
|
+
* template; any other error keeps a running build's id. A started build whose
|
|
103
|
+
* recording keeps failing yields `failed` (`not_recorded`) with the lease left
|
|
104
|
+
* to lapse rather than released, and a run that lost its lease while the
|
|
105
|
+
* provider started yields `failed` (`superseded`); neither is polled or pinned.
|
|
106
|
+
*/
|
|
107
|
+
export declare function runEnvironmentBuild(deps: EnvironmentBuildDeps, input: {
|
|
108
|
+
projectId: string;
|
|
109
|
+
trigger: EnvironmentBuildTrigger;
|
|
110
|
+
}): Promise<EnvironmentBuildOutcome>;
|
|
111
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/environment/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { headsChanged, recordedHeadResolver, resolveCurrentHeads } from "./heads.js";
|
|
2
|
+
import { environmentSandboxContext, resolveProjectEnvironment } from "../workspace.js";
|
|
3
|
+
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
//#region src/environment/build.ts
|
|
5
|
+
/**
|
|
6
|
+
* One proactive environment build: decide whether a trigger should build,
|
|
7
|
+
* hand the sandbox's `builds` capability a context identical to a session's,
|
|
8
|
+
* and record the started build on the project row. Polling the build to its
|
|
9
|
+
* end and pinning the active template is the workflow's second step
|
|
10
|
+
* (`build-workflow.ts`), so a request that only needs the start answer never
|
|
11
|
+
* waits on the provider.
|
|
12
|
+
*/
|
|
13
|
+
const ENVIRONMENT_BUILD_TRIGGERS = [
|
|
14
|
+
"manual",
|
|
15
|
+
"schedule",
|
|
16
|
+
"push"
|
|
17
|
+
];
|
|
18
|
+
/** The synthetic session id a project's build runs under; never a session. */
|
|
19
|
+
function environmentBuildSessionId(projectId) {
|
|
20
|
+
return `environment-build:${projectId}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A build or setup error may echo a clone URL or a token; strip URL userinfo
|
|
24
|
+
* and GitHub token shapes before the message is stored, logged or shown.
|
|
25
|
+
*/
|
|
26
|
+
function redactCredentials(message) {
|
|
27
|
+
return message.replace(/\/\/[^/\s@]+@/g, "//***@").replace(/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]+/g, "***");
|
|
28
|
+
}
|
|
29
|
+
/** An error's message with credentials redacted, safe to log, store or return. */
|
|
30
|
+
function errorReason(error) {
|
|
31
|
+
return redactCredentials(error instanceof Error ? error.message : String(error));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The context an environment build (or a read of its status) hands the
|
|
35
|
+
* provider: the same list form a session boots with, under the build's own
|
|
36
|
+
* session id, with heads served from `heads` so the provider never resolves
|
|
37
|
+
* them itself.
|
|
38
|
+
*/
|
|
39
|
+
function environmentBuildContext(project, environment, heads, versionControl) {
|
|
40
|
+
return {
|
|
41
|
+
sessionId: environmentBuildSessionId(project.id),
|
|
42
|
+
...environmentSandboxContext(environment, {
|
|
43
|
+
orgId: project.orgId,
|
|
44
|
+
getRepositoryAccess: (args) => versionControl.getRepositoryAccess(args),
|
|
45
|
+
resolveHead: recordedHeadResolver(heads)
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Fingerprint of everything a build bakes into the template besides the
|
|
51
|
+
* repository heads: provider settings, the repositories and their order,
|
|
52
|
+
* their setup commands, the workspace setup command and the working
|
|
53
|
+
* directory. A schedule or push skips a ready build only while this matches.
|
|
54
|
+
*/
|
|
55
|
+
function environmentBuildInputs(environment) {
|
|
56
|
+
const inputs = {
|
|
57
|
+
settings: environment.settings,
|
|
58
|
+
workspaceSetupCommand: environment.workspaceSetupCommand ?? null,
|
|
59
|
+
workingDirectory: environment.workingDirectory ?? null,
|
|
60
|
+
repos: environment.repos.map((repo) => ({
|
|
61
|
+
repositoryId: repo.repositoryId,
|
|
62
|
+
templateBranch: repo.templateBranch,
|
|
63
|
+
setupCommand: repo.setupCommand ?? null
|
|
64
|
+
}))
|
|
65
|
+
};
|
|
66
|
+
return createHash("sha256").update(JSON.stringify(inputs)).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The current head of every environment repository's template branch, keyed
|
|
70
|
+
* by slug. The template clones the repository default branch (a link's own
|
|
71
|
+
* branch is the session's base, checked out after boot), so that is the
|
|
72
|
+
* branch a build pins; sessions let the provider resolve the same head.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveEnvironmentHeads(sourceControl, project, environment) {
|
|
75
|
+
return resolveCurrentHeads(sourceControl, {
|
|
76
|
+
orgId: project.orgId,
|
|
77
|
+
repositories: environment.repos.map((repo) => ({
|
|
78
|
+
id: repo.repositoryId,
|
|
79
|
+
slug: repo.slug,
|
|
80
|
+
branch: repo.templateBranch
|
|
81
|
+
}))
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* How long a build-start lease lasts without renewal. The run holding it
|
|
86
|
+
* renews it every third of that while it works, so a lease lapses only when
|
|
87
|
+
* its run died or stalled.
|
|
88
|
+
*/
|
|
89
|
+
const BUILD_START_LEASE_MS = 10 * 6e4;
|
|
90
|
+
/** Tries at recording a started build before the run leaves its lease to lapse. */
|
|
91
|
+
const RECORD_ATTEMPTS = 5;
|
|
92
|
+
/**
|
|
93
|
+
* Start a build for `projectId` when `trigger` calls for one. Every trigger
|
|
94
|
+
* takes the project's build-start lease before reading build state and renews
|
|
95
|
+
* it until the started build is recorded, so overlapping manual, scheduled and
|
|
96
|
+
* push runs never both start a build, however long the provider takes; a run
|
|
97
|
+
* that finds the lease held skips as `starting`. Every trigger skips while the
|
|
98
|
+
* last build is still running; `schedule` and `push` also skip when the last
|
|
99
|
+
* build is ready and neither a head nor any other template input changed;
|
|
100
|
+
* `manual` otherwise always builds. A `push` that would build then takes the
|
|
101
|
+
* leading-edge debounce claim, so only a push that starts a build opens the
|
|
102
|
+
* window. Saving settings never builds.
|
|
103
|
+
*
|
|
104
|
+
* Taking over a lapsed lease leaves the start uncertain: the lapsed run may
|
|
105
|
+
* have started a build it never recorded. The mark persists until resolved.
|
|
106
|
+
* While it stands, a provider that lists builds is asked for a running one
|
|
107
|
+
* (and every trigger skips as `in_progress` on it); with a provider that
|
|
108
|
+
* cannot list, every trigger skips as `unresolved`.
|
|
109
|
+
*
|
|
110
|
+
* The result is written as the lease is released, and only while this run
|
|
111
|
+
* still holds it. A failed `builds.start` clears the last build id, so a later
|
|
112
|
+
* `schedule` or `push` cannot mistake the previous ready build for the current
|
|
113
|
+
* template; any other error keeps a running build's id. A started build whose
|
|
114
|
+
* recording keeps failing yields `failed` (`not_recorded`) with the lease left
|
|
115
|
+
* to lapse rather than released, and a run that lost its lease while the
|
|
116
|
+
* provider started yields `failed` (`superseded`); neither is polled or pinned.
|
|
117
|
+
*/
|
|
118
|
+
async function runEnvironmentBuild(deps, input) {
|
|
119
|
+
const builds = deps.sandbox?.builds;
|
|
120
|
+
if (!builds) return {
|
|
121
|
+
outcome: "unavailable",
|
|
122
|
+
reason: "no_builds"
|
|
123
|
+
};
|
|
124
|
+
if (!deps.sourceControl) return {
|
|
125
|
+
outcome: "unavailable",
|
|
126
|
+
reason: "no_source_control"
|
|
127
|
+
};
|
|
128
|
+
const sourceControl = deps.sourceControl;
|
|
129
|
+
const initial = await deps.projects.getById({ id: input.projectId });
|
|
130
|
+
if (!initial) return {
|
|
131
|
+
outcome: "skipped",
|
|
132
|
+
reason: "no_environment"
|
|
133
|
+
};
|
|
134
|
+
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
135
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
136
|
+
const pushDebounced = (project, at) => project.lastPushBuildAt !== null && at.getTime() - project.lastPushBuildAt.getTime() < project.buildPushDebounceMinutes * 6e4;
|
|
137
|
+
if (input.trigger === "push" && pushDebounced(initial, now())) return {
|
|
138
|
+
outcome: "skipped",
|
|
139
|
+
reason: "debounced"
|
|
140
|
+
};
|
|
141
|
+
const token = randomUUID();
|
|
142
|
+
const lease = {
|
|
143
|
+
id: initial.id,
|
|
144
|
+
token,
|
|
145
|
+
leaseMs: BUILD_START_LEASE_MS
|
|
146
|
+
};
|
|
147
|
+
const claim = await deps.projects.claimBuildStart({
|
|
148
|
+
...lease,
|
|
149
|
+
now: now()
|
|
150
|
+
});
|
|
151
|
+
if (!claim.claimed) return {
|
|
152
|
+
outcome: "skipped",
|
|
153
|
+
reason: "starting"
|
|
154
|
+
};
|
|
155
|
+
let held = true;
|
|
156
|
+
const renewal = setInterval(() => {
|
|
157
|
+
deps.projects.renewBuildStart({
|
|
158
|
+
...lease,
|
|
159
|
+
now: now()
|
|
160
|
+
}).then((renewed) => {
|
|
161
|
+
if (!renewed) held = false;
|
|
162
|
+
}, () => {});
|
|
163
|
+
}, deps.leaseRenewMs ?? 6e5 / 3);
|
|
164
|
+
renewal.unref?.();
|
|
165
|
+
const attempted = { lastBuildAttemptedAt: now() };
|
|
166
|
+
let resolved = false;
|
|
167
|
+
const release = (changes = {}) => deps.projects.releaseBuildStart({
|
|
168
|
+
id: initial.id,
|
|
169
|
+
token,
|
|
170
|
+
changes,
|
|
171
|
+
resolved,
|
|
172
|
+
now: now()
|
|
173
|
+
});
|
|
174
|
+
const failed = async (error, changes) => {
|
|
175
|
+
const reason = errorReason(error);
|
|
176
|
+
await release(changes);
|
|
177
|
+
deps.logger?.info("environment build failed", {
|
|
178
|
+
factoryProjectId: initial.id,
|
|
179
|
+
trigger: input.trigger,
|
|
180
|
+
reason
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
outcome: "failed",
|
|
184
|
+
reason
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
let project;
|
|
189
|
+
let environment;
|
|
190
|
+
let heads;
|
|
191
|
+
let ctx;
|
|
192
|
+
let buildInputs;
|
|
193
|
+
try {
|
|
194
|
+
project = await deps.projects.getById({ id: initial.id }) ?? initial;
|
|
195
|
+
environment = await resolveProjectEnvironment(sourceControl.storage, project);
|
|
196
|
+
if (environment.repos.length === 0) {
|
|
197
|
+
await release();
|
|
198
|
+
return {
|
|
199
|
+
outcome: "skipped",
|
|
200
|
+
reason: "no_environment"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
heads = await resolveEnvironmentHeads(sourceControl, project, environment);
|
|
204
|
+
ctx = environmentBuildContext(project, environment, heads, sourceControl.versionControl);
|
|
205
|
+
buildInputs = environmentBuildInputs(environment);
|
|
206
|
+
if (claim.uncertain) if (builds.list) {
|
|
207
|
+
const running = (await builds.list(ctx, environment.settings)).find((build) => build.status === "pending" || build.status === "building");
|
|
208
|
+
if (running) {
|
|
209
|
+
await release();
|
|
210
|
+
return {
|
|
211
|
+
outcome: "skipped",
|
|
212
|
+
reason: "in_progress",
|
|
213
|
+
buildId: running.buildId
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
resolved = true;
|
|
217
|
+
} else {
|
|
218
|
+
await release();
|
|
219
|
+
return {
|
|
220
|
+
outcome: "skipped",
|
|
221
|
+
reason: "unresolved"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (project.lastBuildId) {
|
|
225
|
+
const last = await builds.get(ctx, environment.settings, project.lastBuildId);
|
|
226
|
+
if (last.status === "pending" || last.status === "building") {
|
|
227
|
+
await release();
|
|
228
|
+
return {
|
|
229
|
+
outcome: "skipped",
|
|
230
|
+
reason: "in_progress",
|
|
231
|
+
buildId: project.lastBuildId
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
let pinnedHeads = project.activeTemplateHeads;
|
|
235
|
+
const lastTemplateId = last.templateId ?? project.lastBuildId;
|
|
236
|
+
if (last.status === "ready" && project.lastBuildHeads && project.activeTemplateId !== lastTemplateId) {
|
|
237
|
+
if (await deps.projects.pinActiveTemplate({
|
|
238
|
+
id: project.id,
|
|
239
|
+
buildId: project.lastBuildId,
|
|
240
|
+
templateId: lastTemplateId,
|
|
241
|
+
heads: project.lastBuildHeads
|
|
242
|
+
})) pinnedHeads = project.lastBuildHeads;
|
|
243
|
+
}
|
|
244
|
+
if ((input.trigger === "schedule" || input.trigger === "push") && last.status === "ready" && !headsChanged(pinnedHeads, heads) && project.lastBuildInputs === buildInputs) {
|
|
245
|
+
await release();
|
|
246
|
+
return {
|
|
247
|
+
outcome: "skipped",
|
|
248
|
+
reason: "unchanged"
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return await failed(error, attempted);
|
|
254
|
+
}
|
|
255
|
+
if (!held) return {
|
|
256
|
+
outcome: "skipped",
|
|
257
|
+
reason: "starting"
|
|
258
|
+
};
|
|
259
|
+
if (input.trigger === "push") {
|
|
260
|
+
let claimed;
|
|
261
|
+
try {
|
|
262
|
+
claimed = await deps.projects.claimBuildAttempt({
|
|
263
|
+
id: project.id,
|
|
264
|
+
debounceMinutes: project.buildPushDebounceMinutes,
|
|
265
|
+
now: now()
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return await failed(error, {});
|
|
269
|
+
}
|
|
270
|
+
if (!claimed) {
|
|
271
|
+
await release();
|
|
272
|
+
return {
|
|
273
|
+
outcome: "skipped",
|
|
274
|
+
reason: "debounced"
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let started;
|
|
279
|
+
try {
|
|
280
|
+
started = await builds.start(ctx, environment.settings);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return await failed(error, {
|
|
283
|
+
lastBuildId: null,
|
|
284
|
+
lastBuildHeads: null,
|
|
285
|
+
lastBuildInputs: null,
|
|
286
|
+
...attempted
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const log = {
|
|
290
|
+
factoryProjectId: project.id,
|
|
291
|
+
trigger: input.trigger,
|
|
292
|
+
buildId: started.buildId
|
|
293
|
+
};
|
|
294
|
+
const changes = {
|
|
295
|
+
lastBuildId: started.buildId,
|
|
296
|
+
lastBuildHeads: heads,
|
|
297
|
+
lastBuildInputs: buildInputs,
|
|
298
|
+
...attempted
|
|
299
|
+
};
|
|
300
|
+
let recorded;
|
|
301
|
+
for (let attempt = 1; recorded === void 0; attempt++) try {
|
|
302
|
+
recorded = await release(changes);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (attempt >= RECORD_ATTEMPTS) {
|
|
305
|
+
deps.logger?.info("environment build started but not recorded", {
|
|
306
|
+
...log,
|
|
307
|
+
reason: errorReason(error)
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
outcome: "failed",
|
|
311
|
+
reason: "not_recorded",
|
|
312
|
+
buildId: started.buildId
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
await sleep(1e3 * 2 ** (attempt - 1));
|
|
316
|
+
}
|
|
317
|
+
if (!recorded) {
|
|
318
|
+
deps.logger?.info("environment build superseded", log);
|
|
319
|
+
return {
|
|
320
|
+
outcome: "failed",
|
|
321
|
+
reason: "superseded",
|
|
322
|
+
buildId: started.buildId
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
deps.logger?.info("environment build started", {
|
|
327
|
+
...log,
|
|
328
|
+
templateId: started.templateId
|
|
329
|
+
});
|
|
330
|
+
} catch {}
|
|
331
|
+
return {
|
|
332
|
+
outcome: "started",
|
|
333
|
+
buildId: started.buildId,
|
|
334
|
+
...started.templateId ? { templateId: started.templateId } : {},
|
|
335
|
+
heads
|
|
336
|
+
};
|
|
337
|
+
} finally {
|
|
338
|
+
clearInterval(renewal);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
342
|
+
export { BUILD_START_LEASE_MS, ENVIRONMENT_BUILD_TRIGGERS, environmentBuildContext, environmentBuildInputs, environmentBuildSessionId, errorReason, redactCredentials, resolveEnvironmentHeads, runEnvironmentBuild };
|
|
343
|
+
|
|
344
|
+
//# sourceMappingURL=build.js.map
|