@defrex/autobuild 0.8.0
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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { RuntimeRegistry, RuntimeUsabilityResult } from '../ports/runner/runtime'
|
|
2
|
+
|
|
3
|
+
export const SETUP_RUNTIME_PREFERENCE = ['claude', 'codex', 'pi'] as const
|
|
4
|
+
|
|
5
|
+
export interface RuntimeProbeReport {
|
|
6
|
+
runtime: string
|
|
7
|
+
usable: boolean
|
|
8
|
+
reason: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SetupAgentInvocation {
|
|
12
|
+
runtime: string
|
|
13
|
+
prompt: string
|
|
14
|
+
cwd: string
|
|
15
|
+
env: Readonly<Record<string, string | undefined>>
|
|
16
|
+
signal?: AbortSignal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type SetupAgentLauncher = (invocation: SetupAgentInvocation) => Promise<number>
|
|
20
|
+
|
|
21
|
+
function message(error: unknown): string {
|
|
22
|
+
return error instanceof Error ? error.message : String(error)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeProbeResult(result: RuntimeUsabilityResult): { usable: boolean; reason: string } {
|
|
26
|
+
if (typeof result === 'boolean') {
|
|
27
|
+
return {
|
|
28
|
+
usable: result,
|
|
29
|
+
reason: result ? 'usable' : 'runtime reported unavailable',
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
usable: result.usable,
|
|
34
|
+
reason: result.reason.trim() || (result.usable ? 'usable' : 'runtime reported unavailable'),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Probe every registration in deterministic diagnostic order. */
|
|
39
|
+
export async function probeInitRuntimes(
|
|
40
|
+
runtimes: RuntimeRegistry,
|
|
41
|
+
cwd: string,
|
|
42
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
43
|
+
): Promise<RuntimeProbeReport[]> {
|
|
44
|
+
const preferred = SETUP_RUNTIME_PREFERENCE.filter((name) => Object.hasOwn(runtimes, name))
|
|
45
|
+
const remaining = Object.keys(runtimes)
|
|
46
|
+
.filter((name) => !preferred.includes(name as (typeof SETUP_RUNTIME_PREFERENCE)[number]))
|
|
47
|
+
.sort()
|
|
48
|
+
const names = [...preferred, ...remaining]
|
|
49
|
+
return Promise.all(
|
|
50
|
+
names.map(async (runtime): Promise<RuntimeProbeReport> => {
|
|
51
|
+
const registration = runtimes[runtime]!
|
|
52
|
+
if (registration.initUsable === undefined) {
|
|
53
|
+
return { runtime, usable: false, reason: 'no init usability probe is registered' }
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const result = await registration.initUsable({
|
|
57
|
+
cwd,
|
|
58
|
+
env,
|
|
59
|
+
models: registration.defaultModel === undefined ? [] : [registration.defaultModel],
|
|
60
|
+
})
|
|
61
|
+
return { runtime, ...normalizeProbeResult(result) }
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { runtime, usable: false, reason: `probe failed: ${message(error)}` }
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Setup launch order is product-owned and never depends on repository contents. */
|
|
70
|
+
export function selectSetupRuntime(reports: readonly RuntimeProbeReport[]): string | undefined {
|
|
71
|
+
for (const preferred of SETUP_RUNTIME_PREFERENCE) {
|
|
72
|
+
if (reports.some((report) => report.runtime === preferred && report.usable)) return preferred
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Remove ambient Autobuild identities before starting a non-phase setup agent. */
|
|
78
|
+
export function sanitizeSetupEnvironment(
|
|
79
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
80
|
+
): Record<string, string> {
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
Object.entries(env).filter(
|
|
83
|
+
(entry): entry is [string, string] => entry[1] !== undefined && !entry[0].startsWith('AB_'),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function setupAgentCommand(runtime: string, prompt: string): string[] {
|
|
89
|
+
if (!SETUP_RUNTIME_PREFERENCE.some((supported) => supported === runtime)) {
|
|
90
|
+
throw new Error(`runtime "${runtime}" has no interactive setup launcher`)
|
|
91
|
+
}
|
|
92
|
+
return [runtime, prompt]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Launch a shipped coding-agent CLI with inherited terminal streams. */
|
|
96
|
+
export const launchSetupAgent: SetupAgentLauncher = async (invocation) => {
|
|
97
|
+
const proc = Bun.spawn(setupAgentCommand(invocation.runtime, invocation.prompt), {
|
|
98
|
+
cwd: invocation.cwd,
|
|
99
|
+
env: sanitizeSetupEnvironment(invocation.env),
|
|
100
|
+
stdin: 'inherit',
|
|
101
|
+
stdout: 'inherit',
|
|
102
|
+
stderr: 'inherit',
|
|
103
|
+
...(invocation.signal === undefined ? {} : { signal: invocation.signal }),
|
|
104
|
+
})
|
|
105
|
+
return proc.exited
|
|
106
|
+
}
|
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { effectiveRuntimeReferences } from '../config/roles'
|
|
5
|
+
import type { Config } from '../config/schema'
|
|
6
|
+
import { vercelSandboxConfigSchema } from '../config/schema'
|
|
7
|
+
import { loadConfig } from '../config/load'
|
|
8
|
+
import { createProductionRuntimes } from '../ports/runner/production'
|
|
9
|
+
import { createRuntimeResolver } from '../ports/runner/routing'
|
|
10
|
+
import type { RuntimeRegistry } from '../ports/runner/runtime'
|
|
11
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
12
|
+
import { spawnExec } from '../ports/workspace/git-worktree'
|
|
13
|
+
import { type VercelSandboxFacade, validateVercelSandbox } from '../ports/workspace/vercel-sandbox'
|
|
14
|
+
import { loadPlugins } from '../plugins/load'
|
|
15
|
+
import { materializePluginRuntimes } from '../plugins/runtimes'
|
|
16
|
+
import { createTicketSource } from '../ports/tickets/create'
|
|
17
|
+
import { inspectLocalStoreSnapshot } from '../store/local/store'
|
|
18
|
+
import type { StoreOpener } from './store-opening'
|
|
19
|
+
import { openProductionStore } from './store-opening'
|
|
20
|
+
import { isRemoteStoreRef, resolveMainRepo, resolveRepoStatePaths } from './repo-state'
|
|
21
|
+
|
|
22
|
+
export const INIT_PROBE_MARKER = 'AB_INIT_READINESS_V1='
|
|
23
|
+
|
|
24
|
+
export interface ReadinessCheck {
|
|
25
|
+
name: string
|
|
26
|
+
status: 'pass' | 'fail' | 'absent'
|
|
27
|
+
detail: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InitValidationReport {
|
|
31
|
+
provider: string
|
|
32
|
+
context: 'local worktree' | 'Vercel Sandbox'
|
|
33
|
+
workspace?: string
|
|
34
|
+
revision?: string
|
|
35
|
+
checks: ReadinessCheck[]
|
|
36
|
+
exitCode: number
|
|
37
|
+
/** Vercel only: automatic snapshots deleted while releasing the disposable
|
|
38
|
+
* environment; zero proves no snapshot storage was left behind. */
|
|
39
|
+
snapshotsDeleted?: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GuestProbeReport {
|
|
43
|
+
checks: ReadinessCheck[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function message(error: unknown): string {
|
|
47
|
+
if (error instanceof AggregateError) {
|
|
48
|
+
return [error.message, ...error.errors.map(message)].filter(Boolean).join('; ')
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof Error)
|
|
51
|
+
return `${error.message}${error.cause === undefined ? '' : `; ${message(error.cause)}`}`
|
|
52
|
+
return String(error)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Replace every supplied nonempty value, longest first, in all diagnostics. */
|
|
56
|
+
export function createReadinessRedactor(
|
|
57
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
58
|
+
explicitSecretNames: readonly string[] = [],
|
|
59
|
+
): (value: unknown) => string {
|
|
60
|
+
const namedSecrets = new Set(explicitSecretNames)
|
|
61
|
+
const secrets = [
|
|
62
|
+
...new Set(
|
|
63
|
+
Object.entries(env)
|
|
64
|
+
.filter(
|
|
65
|
+
([name, value]) =>
|
|
66
|
+
value !== undefined &&
|
|
67
|
+
value !== '' &&
|
|
68
|
+
(namedSecrets.has(name) || /(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH)/i.test(name)),
|
|
69
|
+
)
|
|
70
|
+
.map(([, value]) => value as string),
|
|
71
|
+
),
|
|
72
|
+
].sort((left, right) => right.length - left.length)
|
|
73
|
+
return (value) => {
|
|
74
|
+
let text = message(value)
|
|
75
|
+
for (const secret of secrets) text = text.split(secret).join('[REDACTED]')
|
|
76
|
+
return text
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function shell(
|
|
81
|
+
exec: Exec,
|
|
82
|
+
cwd: string,
|
|
83
|
+
command: string,
|
|
84
|
+
signal?: AbortSignal,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const result = await exec(['sh', '-c', command], {
|
|
87
|
+
cwd,
|
|
88
|
+
...(signal === undefined ? {} : { signal }),
|
|
89
|
+
})
|
|
90
|
+
if (result.exitCode !== 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`setup command exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim() || '(no output)'}`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function effectiveTargets(
|
|
98
|
+
config: Config,
|
|
99
|
+
runtimes: RuntimeRegistry,
|
|
100
|
+
): Array<{ runtime: string; models: string[] }> {
|
|
101
|
+
// Preserve eager registry/model/argument validation, but probe only routes
|
|
102
|
+
// the pipeline can consume rather than every declared role table.
|
|
103
|
+
createRuntimeResolver(runtimes, config.roles, config.policy.sessionBudgetSeconds)
|
|
104
|
+
return effectiveRuntimeReferences(config).map((group) => {
|
|
105
|
+
const defaultModel = runtimes[group.runtime]?.defaultModel
|
|
106
|
+
return {
|
|
107
|
+
runtime: group.runtime,
|
|
108
|
+
models: [
|
|
109
|
+
...new Set([
|
|
110
|
+
...group.models,
|
|
111
|
+
...(group.usesRuntimeDefaultModel && defaultModel !== undefined ? [defaultModel] : []),
|
|
112
|
+
]),
|
|
113
|
+
].sort(),
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Probe code shared by the detached local worktree and the private sandbox entry. */
|
|
119
|
+
export async function runGuestReadinessProbe(opts: {
|
|
120
|
+
repo: string
|
|
121
|
+
env: Record<string, string | undefined>
|
|
122
|
+
runtimes?: RuntimeRegistry
|
|
123
|
+
openStore?: StoreOpener
|
|
124
|
+
exec?: Exec
|
|
125
|
+
signal?: AbortSignal
|
|
126
|
+
}): Promise<GuestProbeReport> {
|
|
127
|
+
const checks: ReadinessCheck[] = []
|
|
128
|
+
const config = await loadConfig(join(opts.repo, 'autobuild.toml'))
|
|
129
|
+
const vercelConfig =
|
|
130
|
+
config.workspace.provider === 'vercel-sandbox'
|
|
131
|
+
? vercelSandboxConfigSchema.parse(config.workspace.config)
|
|
132
|
+
: undefined
|
|
133
|
+
const redact = createReadinessRedactor(opts.env, vercelConfig?.environmentVariables)
|
|
134
|
+
try {
|
|
135
|
+
const setup = config.commands.setup
|
|
136
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
137
|
+
if (setup !== undefined) await shell(opts.exec ?? spawnExec, opts.repo, setup, opts.signal)
|
|
138
|
+
checks.push({
|
|
139
|
+
name: 'repository setup',
|
|
140
|
+
status: 'pass',
|
|
141
|
+
detail: setup === undefined ? 'no commands.setup configured' : 'commands.setup completed',
|
|
142
|
+
})
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? error
|
|
145
|
+
return {
|
|
146
|
+
checks: [
|
|
147
|
+
{
|
|
148
|
+
name: 'repository setup',
|
|
149
|
+
status: 'fail',
|
|
150
|
+
detail: `${redact(error)}; make commands.setup idempotent and runnable in a fresh environment`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let runtimes: RuntimeRegistry
|
|
157
|
+
try {
|
|
158
|
+
const packageRoot = await resolveMainRepo(opts.repo, opts.exec ?? spawnExec)
|
|
159
|
+
const plugins = await loadPlugins(config.plugins, opts.repo, { packageRoot })
|
|
160
|
+
runtimes = await materializePluginRuntimes(
|
|
161
|
+
opts.runtimes ?? createProductionRuntimes().runtimes,
|
|
162
|
+
plugins,
|
|
163
|
+
{ repoRoot: opts.repo, env: opts.env },
|
|
164
|
+
)
|
|
165
|
+
checks.push({
|
|
166
|
+
name: 'configuration and plugins',
|
|
167
|
+
status: 'pass',
|
|
168
|
+
detail: `${config.plugins.length} configured plugin(s) loaded`,
|
|
169
|
+
})
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
checks: [
|
|
173
|
+
...checks,
|
|
174
|
+
{
|
|
175
|
+
name: 'configuration and plugins',
|
|
176
|
+
status: 'fail',
|
|
177
|
+
detail: `${redact(error)}; install or correct the configured plugin in this environment`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const runtimeRemediation = (runtime: string, preflightOnly: boolean): string =>
|
|
184
|
+
vercelConfig === undefined
|
|
185
|
+
? 'install/authenticate this runtime in the local validation environment'
|
|
186
|
+
: `fix workspace.config.runtimeProvisioning.${runtime}${preflightOnly ? '.preflight' : ''} and expose API credential names in workspace.config.environmentVariables`
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
for (const target of effectiveTargets(config, runtimes)) {
|
|
190
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
191
|
+
const probe = runtimes[target.runtime]?.initUsable
|
|
192
|
+
if (probe === undefined) {
|
|
193
|
+
checks.push({
|
|
194
|
+
name: `runtime ${target.runtime}`,
|
|
195
|
+
status: 'fail',
|
|
196
|
+
detail:
|
|
197
|
+
'selected runtime has no onboarding usability probe; add initUsable or select a supported runtime',
|
|
198
|
+
})
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const outcome = await probe({ cwd: opts.repo, env: opts.env, models: target.models })
|
|
203
|
+
const usable = typeof outcome === 'boolean' ? outcome : outcome.usable
|
|
204
|
+
const reason =
|
|
205
|
+
typeof outcome === 'boolean' ? (outcome ? 'usable' : 'unusable') : outcome.reason
|
|
206
|
+
checks.push({
|
|
207
|
+
name: `runtime ${target.runtime}${target.models.length === 0 ? '' : ` (${target.models.join(', ')})`}`,
|
|
208
|
+
status: usable ? 'pass' : 'fail',
|
|
209
|
+
detail: usable
|
|
210
|
+
? redact(reason)
|
|
211
|
+
: `${redact(reason)}; ${runtimeRemediation(target.runtime, true)}`,
|
|
212
|
+
})
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? error
|
|
215
|
+
checks.push({
|
|
216
|
+
name: `runtime ${target.runtime}`,
|
|
217
|
+
status: 'fail',
|
|
218
|
+
detail: `${redact(error)}; ${runtimeRemediation(target.runtime, false)}`,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? error
|
|
224
|
+
checks.push({ name: 'runtime routing', status: 'fail', detail: redact(error) })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
228
|
+
const storeRef = opts.env.AB_STORE
|
|
229
|
+
if (storeRef === undefined || storeRef === '') {
|
|
230
|
+
checks.push({
|
|
231
|
+
name: 'BuildStore',
|
|
232
|
+
status: 'fail',
|
|
233
|
+
detail: 'AB_STORE is missing; configure the Store used by builds',
|
|
234
|
+
})
|
|
235
|
+
} else if (opts.openStore === undefined && !isRemoteStoreRef(storeRef)) {
|
|
236
|
+
const localRoot = resolve(opts.repo, storeRef)
|
|
237
|
+
try {
|
|
238
|
+
const inspection = await inspectLocalStoreSnapshot(localRoot)
|
|
239
|
+
if (inspection.status === 'absent') {
|
|
240
|
+
checks.push({
|
|
241
|
+
name: 'BuildStore',
|
|
242
|
+
status: 'absent',
|
|
243
|
+
detail: `${inspection.databasePath} does not exist; no repository history was available to inspect and no Store was created`,
|
|
244
|
+
})
|
|
245
|
+
} else {
|
|
246
|
+
checks.push({
|
|
247
|
+
name: 'BuildStore',
|
|
248
|
+
status: 'pass',
|
|
249
|
+
detail: `${inspection.buildCount} readable build record(s) in a disposable snapshot of ${inspection.databasePath}`,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
checks.push({
|
|
254
|
+
name: 'BuildStore',
|
|
255
|
+
status: 'fail',
|
|
256
|
+
detail: `${redact(error)}; verify the local Store database permissions and integrity`,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
let store: ReturnType<StoreOpener> | undefined
|
|
261
|
+
try {
|
|
262
|
+
store = (opts.openStore ?? openProductionStore)(storeRef, opts.env.AB_TOKEN)
|
|
263
|
+
await store.listBuilds()
|
|
264
|
+
checks.push({ name: 'BuildStore', status: 'pass', detail: 'read-only request succeeded' })
|
|
265
|
+
} catch (error) {
|
|
266
|
+
checks.push({
|
|
267
|
+
name: 'BuildStore',
|
|
268
|
+
status: 'fail',
|
|
269
|
+
detail: `${redact(error)}; verify the Store URL, AB_TOKEN scope, and network access`,
|
|
270
|
+
})
|
|
271
|
+
} finally {
|
|
272
|
+
await store?.close().catch((error) => {
|
|
273
|
+
checks.push({ name: 'BuildStore close', status: 'fail', detail: redact(error) })
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { checks }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parseGuestOutput(output: string): GuestProbeReport {
|
|
281
|
+
const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(INIT_PROBE_MARKER))
|
|
282
|
+
if (line === undefined)
|
|
283
|
+
throw new Error('remote readiness probe returned malformed output (result marker absent)')
|
|
284
|
+
const value = JSON.parse(line.slice(INIT_PROBE_MARKER.length)) as GuestProbeReport
|
|
285
|
+
if (!Array.isArray(value.checks))
|
|
286
|
+
throw new Error('remote readiness probe returned malformed checks')
|
|
287
|
+
return value
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function gitText(
|
|
291
|
+
exec: Exec,
|
|
292
|
+
repo: string,
|
|
293
|
+
args: string[],
|
|
294
|
+
signal?: AbortSignal,
|
|
295
|
+
): Promise<string> {
|
|
296
|
+
const result = await exec(['git', ...args], {
|
|
297
|
+
cwd: repo,
|
|
298
|
+
...(signal === undefined ? {} : { signal }),
|
|
299
|
+
})
|
|
300
|
+
if (result.exitCode !== 0)
|
|
301
|
+
throw new Error(
|
|
302
|
+
`git ${args.join(' ')} exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim()}`,
|
|
303
|
+
)
|
|
304
|
+
return result.stdout.trim()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function hostPreflight(config: Config, env: Record<string, string | undefined>): void {
|
|
308
|
+
if (config.workspace.provider !== 'vercel-sandbox') return
|
|
309
|
+
if (config.forge !== 'github')
|
|
310
|
+
throw new Error(
|
|
311
|
+
'vercel-sandbox supports forge = "github" only; configure GitHub publication before validating',
|
|
312
|
+
)
|
|
313
|
+
if (!env.GITHUB_TOKEN && !env.GH_TOKEN)
|
|
314
|
+
throw new Error('vercel-sandbox publication requires push-capable GITHUB_TOKEN or GH_TOKEN')
|
|
315
|
+
if (
|
|
316
|
+
!env.VERCEL_OIDC_TOKEN &&
|
|
317
|
+
(!env.VERCEL_TOKEN || !env.VERCEL_TEAM_ID || !env.VERCEL_PROJECT_ID)
|
|
318
|
+
) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
'Vercel authentication requires VERCEL_OIDC_TOKEN or the durable VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID set',
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
if (config.tickets.source === 'linear' && !env.LINEAR_API_KEY)
|
|
324
|
+
throw new Error('ticket source "linear" requires LINEAR_API_KEY')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function validateInitReadiness(opts: {
|
|
328
|
+
targetRepo: string
|
|
329
|
+
env: Record<string, string | undefined>
|
|
330
|
+
stdout?: (line: string) => void
|
|
331
|
+
exec?: Exec
|
|
332
|
+
openStore?: StoreOpener
|
|
333
|
+
runtimes?: RuntimeRegistry
|
|
334
|
+
vercelFacade?: VercelSandboxFacade
|
|
335
|
+
packageArchive?: () => Promise<Uint8Array>
|
|
336
|
+
signal?: AbortSignal
|
|
337
|
+
}): Promise<InitValidationReport> {
|
|
338
|
+
const exec = opts.exec ?? spawnExec
|
|
339
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
340
|
+
const repo = await resolveMainRepo(opts.targetRepo, exec)
|
|
341
|
+
const configPath = join(repo, 'autobuild.toml')
|
|
342
|
+
const configBytes = await readFile(configPath, 'utf8')
|
|
343
|
+
const config = await loadConfig(configPath)
|
|
344
|
+
const state = resolveRepoStatePaths({ repo, envStore: opts.env.AB_STORE })
|
|
345
|
+
const vercelConfig =
|
|
346
|
+
config.workspace.provider === 'vercel-sandbox'
|
|
347
|
+
? vercelSandboxConfigSchema.parse(config.workspace.config)
|
|
348
|
+
: undefined
|
|
349
|
+
const redact = createReadinessRedactor(opts.env, [
|
|
350
|
+
...(vercelConfig?.environmentVariables ?? []),
|
|
351
|
+
...(vercelConfig?.gitUsernameEnv === undefined ? [] : [vercelConfig.gitUsernameEnv]),
|
|
352
|
+
...(vercelConfig?.gitPasswordEnv === undefined ? [] : [vercelConfig.gitPasswordEnv]),
|
|
353
|
+
])
|
|
354
|
+
try {
|
|
355
|
+
hostPreflight(config, opts.env)
|
|
356
|
+
// Ticket acquisition is a host responsibility for both workspace providers.
|
|
357
|
+
// Exercise its read surface before allocating disposable infrastructure.
|
|
358
|
+
const hostPlugins = await loadPlugins(config.plugins, repo, { packageRoot: repo })
|
|
359
|
+
if (config.tickets.source !== 'file') {
|
|
360
|
+
const ticketSource = await createTicketSource(
|
|
361
|
+
config.tickets,
|
|
362
|
+
{ ...opts.env, AB_STORE: state.storeRef },
|
|
363
|
+
repo,
|
|
364
|
+
state.localStateRoot,
|
|
365
|
+
hostPlugins,
|
|
366
|
+
)
|
|
367
|
+
await ticketSource.listReady({
|
|
368
|
+
...(config.tickets.readyLabels !== undefined ? { labels: config.tickets.readyLabels } : {}),
|
|
369
|
+
state: config.tickets.readyState,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new Error(redact(error))
|
|
374
|
+
}
|
|
375
|
+
let report: InitValidationReport | undefined
|
|
376
|
+
|
|
377
|
+
if (config.workspace.provider === 'git-worktree') {
|
|
378
|
+
const root = await mkdtemp(join(tmpdir(), 'ab-init-validation-'))
|
|
379
|
+
const workspace = join(root, 'worktree')
|
|
380
|
+
let failure: unknown
|
|
381
|
+
let cleanupFailure: unknown
|
|
382
|
+
try {
|
|
383
|
+
const revision = await gitText(
|
|
384
|
+
exec,
|
|
385
|
+
repo,
|
|
386
|
+
['rev-parse', '--verify', `${config.baseBranch}^{commit}`],
|
|
387
|
+
opts.signal,
|
|
388
|
+
)
|
|
389
|
+
const add = await exec(['git', 'worktree', 'add', '--detach', workspace, revision], {
|
|
390
|
+
cwd: repo,
|
|
391
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
392
|
+
})
|
|
393
|
+
if (add.exitCode !== 0)
|
|
394
|
+
throw new Error(
|
|
395
|
+
`could not create validation worktree: ${add.stderr.trim() || add.stdout.trim()}`,
|
|
396
|
+
)
|
|
397
|
+
const candidateConfig = await readFile(join(workspace, 'autobuild.toml'), 'utf8')
|
|
398
|
+
if (candidateConfig !== configBytes)
|
|
399
|
+
throw new Error(
|
|
400
|
+
`autobuild.toml differs from committed ${config.baseBranch}; commit setup changes before validating`,
|
|
401
|
+
)
|
|
402
|
+
const guest = await runGuestReadinessProbe({
|
|
403
|
+
repo: workspace,
|
|
404
|
+
env: { ...opts.env, AB_STORE: state.storeRef },
|
|
405
|
+
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
406
|
+
...(opts.runtimes !== undefined ? { runtimes: opts.runtimes } : {}),
|
|
407
|
+
exec,
|
|
408
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
409
|
+
})
|
|
410
|
+
report = {
|
|
411
|
+
provider: 'git-worktree',
|
|
412
|
+
context: 'local worktree',
|
|
413
|
+
workspace,
|
|
414
|
+
revision,
|
|
415
|
+
checks: [
|
|
416
|
+
{
|
|
417
|
+
name: 'repository acquisition',
|
|
418
|
+
status: 'pass',
|
|
419
|
+
detail: `detached committed ${config.baseBranch} at ${revision}`,
|
|
420
|
+
},
|
|
421
|
+
...guest.checks,
|
|
422
|
+
],
|
|
423
|
+
exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
failure = error
|
|
427
|
+
} finally {
|
|
428
|
+
const remove = await exec(['git', 'worktree', 'remove', '--force', workspace], { cwd: repo })
|
|
429
|
+
const prune = await exec(['git', 'worktree', 'prune'], { cwd: repo })
|
|
430
|
+
await rm(root, { recursive: true, force: true })
|
|
431
|
+
if (remove.exitCode !== 0 && !/not a working tree/i.test(remove.stderr)) {
|
|
432
|
+
cleanupFailure = new Error(
|
|
433
|
+
`validation worktree ${workspace} could not be removed: ${remove.stderr.trim()}`,
|
|
434
|
+
)
|
|
435
|
+
} else if (prune.exitCode !== 0) {
|
|
436
|
+
cleanupFailure = new Error(
|
|
437
|
+
`validation worktree registry could not be pruned: ${prune.stderr.trim()}`,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (failure !== undefined && cleanupFailure !== undefined) {
|
|
442
|
+
throw new AggregateError(
|
|
443
|
+
[failure, cleanupFailure],
|
|
444
|
+
`validation failed and disposable worktree ${workspace} cleanup also failed`,
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
if (failure !== undefined) throw failure
|
|
448
|
+
if (cleanupFailure !== undefined) throw cleanupFailure
|
|
449
|
+
} else if (config.workspace.provider === 'vercel-sandbox') {
|
|
450
|
+
const vercel = vercelConfig!
|
|
451
|
+
const storeRef = state.storeRef
|
|
452
|
+
if (!/^https:\/\//i.test(storeRef))
|
|
453
|
+
throw new Error('vercel-sandbox requires AB_STORE to be an HTTPS URL reachable from Vercel')
|
|
454
|
+
const token = opts.env.AB_TOKEN
|
|
455
|
+
if (!token) throw new Error('vercel-sandbox requires nonempty AB_TOKEN for the hosted Store')
|
|
456
|
+
const remoteLine = await gitText(
|
|
457
|
+
exec,
|
|
458
|
+
repo,
|
|
459
|
+
['ls-remote', '--heads', 'origin', `refs/heads/${config.baseBranch}`],
|
|
460
|
+
opts.signal,
|
|
461
|
+
)
|
|
462
|
+
const remoteRevision = remoteLine.split(/\s+/)[0]
|
|
463
|
+
if (!/^[0-9a-f]{40,64}$/i.test(remoteRevision ?? '')) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`remote base ${config.baseBranch} does not exist; commit and push setup changes before validating`,
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
await gitText(
|
|
469
|
+
exec,
|
|
470
|
+
repo,
|
|
471
|
+
[
|
|
472
|
+
'fetch',
|
|
473
|
+
'--no-tags',
|
|
474
|
+
'--no-write-fetch-head',
|
|
475
|
+
'--refmap=',
|
|
476
|
+
'origin',
|
|
477
|
+
`refs/heads/${config.baseBranch}`,
|
|
478
|
+
],
|
|
479
|
+
opts.signal,
|
|
480
|
+
)
|
|
481
|
+
const shownConfig = await exec(['git', 'show', `${remoteRevision}:autobuild.toml`], {
|
|
482
|
+
cwd: repo,
|
|
483
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
484
|
+
})
|
|
485
|
+
if (shownConfig.exitCode !== 0) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`remote ${config.baseBranch} does not contain autobuild.toml; commit and push setup changes before validating`,
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
if (shownConfig.stdout !== configBytes) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`remote ${config.baseBranch} autobuild.toml differs from this checkout; commit and push setup changes before validating`,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
let remote: Awaited<ReturnType<typeof validateVercelSandbox>>
|
|
496
|
+
try {
|
|
497
|
+
remote = await validateVercelSandbox({
|
|
498
|
+
config: vercel,
|
|
499
|
+
env: opts.env,
|
|
500
|
+
storeRef,
|
|
501
|
+
storeToken: token,
|
|
502
|
+
repo,
|
|
503
|
+
baseBranch: config.baseBranch,
|
|
504
|
+
...(opts.vercelFacade !== undefined ? { facade: opts.vercelFacade } : {}),
|
|
505
|
+
exec,
|
|
506
|
+
...(opts.packageArchive !== undefined ? { packageArchive: opts.packageArchive } : {}),
|
|
507
|
+
runtimeReferences: effectiveRuntimeReferences(config),
|
|
508
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
509
|
+
onSandbox: (name) => opts.stdout?.(`Disposable Vercel Sandbox: ${name} (active)`),
|
|
510
|
+
})
|
|
511
|
+
} catch (error) {
|
|
512
|
+
throw new Error(redact(error))
|
|
513
|
+
}
|
|
514
|
+
let guest: GuestProbeReport
|
|
515
|
+
try {
|
|
516
|
+
guest = parseGuestOutput(remote.output)
|
|
517
|
+
} catch (error) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`disposable sandbox ${remote.sandbox} was deleted, but its readiness output was invalid: ${redact(error)}`,
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
report = {
|
|
523
|
+
provider: 'vercel-sandbox',
|
|
524
|
+
context: 'Vercel Sandbox',
|
|
525
|
+
workspace: remote.sandbox,
|
|
526
|
+
revision: remote.revision,
|
|
527
|
+
snapshotsDeleted: remote.snapshotsDeleted,
|
|
528
|
+
checks: [
|
|
529
|
+
{
|
|
530
|
+
name: 'repository acquisition',
|
|
531
|
+
status: 'pass',
|
|
532
|
+
detail: `${remote.origin} ${remote.revision}`,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: 'system provisioning',
|
|
536
|
+
status: 'pass',
|
|
537
|
+
detail:
|
|
538
|
+
remote.provisioning.length === 0
|
|
539
|
+
? 'no workspace.config.provisioning steps declared'
|
|
540
|
+
: `completed: ${remote.provisioning.join(', ')}`,
|
|
541
|
+
},
|
|
542
|
+
...guest.checks,
|
|
543
|
+
],
|
|
544
|
+
exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`workspace provider "${config.workspace.provider}" does not support init readiness validation`,
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (report === undefined) throw new Error('readiness validation produced no report')
|
|
553
|
+
const stdout = opts.stdout ?? (() => {})
|
|
554
|
+
stdout(`Readiness: ${report.context} (${report.provider})`)
|
|
555
|
+
stdout(`Store: ${state.storeRef}`)
|
|
556
|
+
stdout(`Forge: ${config.forge}`)
|
|
557
|
+
if (vercelConfig !== undefined) {
|
|
558
|
+
stdout(
|
|
559
|
+
`Vercel auth: ${opts.env.VERCEL_OIDC_TOKEN ? 'OIDC' : 'access token'}; team=${opts.env.VERCEL_TEAM_ID ?? '(linked)'}; project=${opts.env.VERCEL_PROJECT_ID ?? '(linked)'}`,
|
|
560
|
+
)
|
|
561
|
+
stdout(
|
|
562
|
+
`Private clone variables: ${vercelConfig.gitUsernameEnv === undefined ? '(public repository)' : `${vercelConfig.gitUsernameEnv}, ${vercelConfig.gitPasswordEnv}`}`,
|
|
563
|
+
)
|
|
564
|
+
stdout(
|
|
565
|
+
`Guest environment variable names: ${vercelConfig.environmentVariables.join(', ') || '(none)'}`,
|
|
566
|
+
)
|
|
567
|
+
stdout(
|
|
568
|
+
`Runtime provisioning names: ${Object.keys(vercelConfig.runtimeProvisioning).sort().join(', ') || '(none)'}`,
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
if (report.workspace !== undefined)
|
|
572
|
+
stdout(
|
|
573
|
+
`Disposable environment: ${report.workspace} (released${report.snapshotsDeleted === undefined ? '' : `; ${report.snapshotsDeleted} snapshot(s) deleted`})`,
|
|
574
|
+
)
|
|
575
|
+
for (const check of report.checks) {
|
|
576
|
+
const label = check.status === 'pass' ? 'PASS' : check.status === 'absent' ? 'ABSENT' : 'FAIL'
|
|
577
|
+
stdout(` ${label} ${check.name}: ${redact(check.detail)}`)
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
...report,
|
|
581
|
+
checks: report.checks.map((check) => ({ ...check, detail: redact(check.detail) })),
|
|
582
|
+
}
|
|
583
|
+
}
|