@defrex/autobuild 0.8.0 → 0.9.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/README.md +1 -1
- package/SPEC.md +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +248 -222
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/status.ts +149 -27
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +208 -19
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +169 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single shared capability object for the `vercel-sandbox` workspace
|
|
3
|
+
* provider (AUT-517). During the transitional state in which the builtin
|
|
4
|
+
* hosts the provider implementation, both the builtin registration (in
|
|
5
|
+
* `builtin-capabilities.ts`) and the `@defrex/autobuild-vercel-sandbox`
|
|
6
|
+
* plugin reference THIS object, so the two declarations cannot drift: any
|
|
7
|
+
* future divergence is a compile-time split, not a silently duplicated
|
|
8
|
+
* closure.
|
|
9
|
+
*
|
|
10
|
+
* The parse-time subset comes from `BUILTIN_WORKSPACE_PROVIDER_CONFIG` in
|
|
11
|
+
* `config/schema.ts`; the registry-seam behaviors and the closures
|
|
12
|
+
* (`validateOrigin`, `guestEnvNames`, `describeEnvironment`,
|
|
13
|
+
* `validateReadiness`) are assembled here once.
|
|
14
|
+
*
|
|
15
|
+
* Import graph discipline: this module imports `config/schema.ts`,
|
|
16
|
+
* `github-origin.ts`, and `init-readiness-remote.ts`; nothing imports it
|
|
17
|
+
* from `schema.ts`, so no cycle exists.
|
|
18
|
+
*/
|
|
19
|
+
import { BUILTIN_WORKSPACE_PROVIDER_CONFIG, type VercelSandboxConfig } from '../../config/schema'
|
|
20
|
+
import { validateRemoteReadiness } from '../../cli/init-readiness-remote'
|
|
21
|
+
import type {
|
|
22
|
+
WorkspaceProviderCapabilities,
|
|
23
|
+
WorkspaceReadinessContext,
|
|
24
|
+
} from './provider-capabilities'
|
|
25
|
+
import { validateVercelGithubOrigin } from './github-origin'
|
|
26
|
+
|
|
27
|
+
/** The parse-time declaration subset from the config table, without the
|
|
28
|
+
* parse-site refusal text (which never belongs on the capability). */
|
|
29
|
+
function pickConfigDeclaration(name: string): WorkspaceProviderCapabilities {
|
|
30
|
+
const declaration = BUILTIN_WORKSPACE_PROVIDER_CONFIG.get(name)
|
|
31
|
+
if (declaration === undefined) return {}
|
|
32
|
+
return {
|
|
33
|
+
...(declaration.configSchema !== undefined ? { configSchema: declaration.configSchema } : {}),
|
|
34
|
+
...(declaration.requireRuntimeProvisioning === true
|
|
35
|
+
? { requireRuntimeProvisioning: true }
|
|
36
|
+
: {}),
|
|
37
|
+
...(declaration.sandboxForbiddenEnv !== undefined
|
|
38
|
+
? { sandboxForbiddenEnv: declaration.sandboxForbiddenEnv }
|
|
39
|
+
: {}),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const VERCEL_SANDBOX_CAPABILITIES: WorkspaceProviderCapabilities = {
|
|
44
|
+
// Parse-time subset comes from the config table; the capability carries
|
|
45
|
+
// no configRefusal because the provider accepts [workspace.config]
|
|
46
|
+
// through its schema.
|
|
47
|
+
...pickConfigDeclaration('vercel-sandbox'),
|
|
48
|
+
supportedForges: ['github'],
|
|
49
|
+
forgeDispatchMessage: 'vercel-sandbox requires the builtin github forge',
|
|
50
|
+
forgeValidationMessage:
|
|
51
|
+
'vercel-sandbox supports forge = "github" only; configure GitHub publication before validating',
|
|
52
|
+
requiredEnv: [
|
|
53
|
+
{
|
|
54
|
+
alternatives: [['GITHUB_TOKEN'], ['GH_TOKEN']],
|
|
55
|
+
dispatchMessage:
|
|
56
|
+
'vercel-sandbox publication requires GITHUB_TOKEN or GH_TOKEN in the dispatcher environment',
|
|
57
|
+
validationMessage:
|
|
58
|
+
'vercel-sandbox publication requires push-capable GITHUB_TOKEN or GH_TOKEN',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
alternatives: [
|
|
62
|
+
['VERCEL_OIDC_TOKEN'],
|
|
63
|
+
['VERCEL_TOKEN', 'VERCEL_TEAM_ID', 'VERCEL_PROJECT_ID'],
|
|
64
|
+
],
|
|
65
|
+
validationMessage:
|
|
66
|
+
'Vercel authentication requires VERCEL_OIDC_TOKEN or the durable VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID set',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
processEnvOnly: [
|
|
70
|
+
{
|
|
71
|
+
name: 'VERCEL_OIDC_TOKEN',
|
|
72
|
+
message:
|
|
73
|
+
'VERCEL_OIDC_TOKEN loaded only from the target .env is unavailable to the Vercel SDK; export it in the launcher environment or configure VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
storeRequirements: {
|
|
77
|
+
constructionMessage:
|
|
78
|
+
'vercel-sandbox requires an HTTPS BuildStore and scoped AB_TOKEN authority',
|
|
79
|
+
storeRefMessage: 'vercel-sandbox requires AB_STORE to be an HTTPS URL reachable from Vercel',
|
|
80
|
+
storeTokenMessage: 'vercel-sandbox requires nonempty AB_TOKEN for the hosted Store',
|
|
81
|
+
},
|
|
82
|
+
validateOrigin: validateVercelGithubOrigin,
|
|
83
|
+
originReadFailureMessage: 'vercel-sandbox requires a readable Git origin',
|
|
84
|
+
guestEnvNames: (raw) => {
|
|
85
|
+
const config = raw as VercelSandboxConfig
|
|
86
|
+
return [
|
|
87
|
+
...config.environmentVariables,
|
|
88
|
+
...(config.gitUsernameEnv === undefined ? [] : [config.gitUsernameEnv]),
|
|
89
|
+
...(config.gitPasswordEnv === undefined ? [] : [config.gitPasswordEnv]),
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
describeEnvironment: (raw, env) => {
|
|
93
|
+
const config = raw as VercelSandboxConfig
|
|
94
|
+
return [
|
|
95
|
+
`Vercel auth: ${env.VERCEL_OIDC_TOKEN ? 'OIDC' : 'access token'}; team=${env.VERCEL_TEAM_ID ?? '(linked)'}; project=${env.VERCEL_PROJECT_ID ?? '(linked)'}`,
|
|
96
|
+
`Private clone variables: ${
|
|
97
|
+
config.gitUsernameEnv === undefined
|
|
98
|
+
? '(public repository)'
|
|
99
|
+
: `${config.gitUsernameEnv}, ${config.gitPasswordEnv}`
|
|
100
|
+
}`,
|
|
101
|
+
`Guest environment variable names: ${config.environmentVariables.join(', ') || '(none)'}`,
|
|
102
|
+
`Runtime provisioning names: ${
|
|
103
|
+
Object.keys(config.runtimeProvisioning ?? {})
|
|
104
|
+
.sort()
|
|
105
|
+
.join(', ') || '(none)'
|
|
106
|
+
}`,
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
validateReadiness: (ctx: WorkspaceReadinessContext) => validateRemoteReadiness(ctx),
|
|
110
|
+
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { basename, join } from 'node:path'
|
|
5
2
|
import { Sandbox, Snapshot, type NetworkPolicy, type SandboxRegion } from '@vercel/sandbox'
|
|
6
3
|
import { displayName, tomlKey, type RuntimeReferenceGroup } from '../../config/roles'
|
|
7
4
|
import {
|
|
@@ -9,12 +6,12 @@ import {
|
|
|
9
6
|
type VercelSandboxConfig,
|
|
10
7
|
vercelSandboxConfigSchema,
|
|
11
8
|
} from '../../config/schema'
|
|
12
|
-
import {
|
|
13
|
-
import { defaultDistributionArchive, readDistributionIdentity } from './distribution-archive'
|
|
9
|
+
import { distributionStampFor, resolveDistributionArchive } from './distribution-archive'
|
|
14
10
|
import type {
|
|
15
11
|
WorkspaceHandle,
|
|
16
12
|
WorkspaceProvider,
|
|
17
13
|
WorkspaceProvisionResult,
|
|
14
|
+
WorkspacePublication,
|
|
18
15
|
WorkspaceReapOutcome,
|
|
19
16
|
} from '../types'
|
|
20
17
|
import {
|
|
@@ -42,15 +39,23 @@ import {
|
|
|
42
39
|
import { BUILD_RUNNER_OPTIONS_ENV } from './local-build-execution'
|
|
43
40
|
import type { Exec } from './git-worktree'
|
|
44
41
|
import { spawnExec } from './git-worktree'
|
|
42
|
+
import { validateVercelGithubOrigin } from './github-origin'
|
|
43
|
+
import { currentRuntimeReferences, type RuntimeReferencesSource } from './provider-capabilities'
|
|
44
|
+
|
|
45
|
+
export { validateVercelGithubOrigin }
|
|
45
46
|
|
|
46
47
|
export const VERCEL_WORKSPACE_PATH = '/vercel/sandbox/workspace'
|
|
47
48
|
export const VERCEL_AUTOBUILD_PATH = '/opt/autobuild'
|
|
48
49
|
export const VERCEL_PROVISIONED_MARKER = `${VERCEL_AUTOBUILD_PATH}/.provisioned`
|
|
49
|
-
/** Records the installed distribution's
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
/** Records the installed distribution's identity stamp — everything the
|
|
51
|
+
* hosted store's skew check compares (package version and remote-store
|
|
52
|
+
* protocol version) plus the sha256 digest of the exact packed archive the
|
|
53
|
+
* dispatcher installs (see `readDistributionIdentityStamp`). A reused sandbox
|
|
54
|
+
* whose marker disagrees with the stamp the current system would deliver is
|
|
55
|
+
* refreshed by reinstalling the archive, so an upgraded dispatcher retrofits
|
|
56
|
+
* its persistent guests (including legacy guests predating this marker)
|
|
57
|
+
* instead of resuming a stale distribution that the hosted store would
|
|
58
|
+
* reject. */
|
|
54
59
|
export const VERCEL_DISTRIBUTION_VERSION_MARKER = `${VERCEL_AUTOBUILD_PATH}/.distribution-version`
|
|
55
60
|
export const VERCEL_BUN_VERSION = '1.4.0'
|
|
56
61
|
export const VERCEL_BUN_PREFIX = '/opt/autobuild-runtime'
|
|
@@ -447,35 +452,6 @@ export async function purgeEnvironmentSnapshots(
|
|
|
447
452
|
}
|
|
448
453
|
}
|
|
449
454
|
|
|
450
|
-
export function validateVercelGithubOrigin(raw: string): {
|
|
451
|
-
url: string
|
|
452
|
-
host: string
|
|
453
|
-
path: string
|
|
454
|
-
directory: string
|
|
455
|
-
} {
|
|
456
|
-
let url: URL
|
|
457
|
-
try {
|
|
458
|
-
url = new URL(raw)
|
|
459
|
-
} catch {
|
|
460
|
-
throw new Error('vercel-sandbox requires an HTTPS GitHub origin')
|
|
461
|
-
}
|
|
462
|
-
if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com') {
|
|
463
|
-
throw new Error('vercel-sandbox requires an HTTPS github.com origin')
|
|
464
|
-
}
|
|
465
|
-
url.username = ''
|
|
466
|
-
url.password = ''
|
|
467
|
-
url.search = ''
|
|
468
|
-
url.hash = ''
|
|
469
|
-
const path = url.pathname.replace(/\.git$/, '').replace(/^\//, '')
|
|
470
|
-
if (!/^[^/]+\/[^/]+$/.test(path)) throw new Error('GitHub origin must name owner/repository')
|
|
471
|
-
return {
|
|
472
|
-
url: `https://github.com/${path}.git`,
|
|
473
|
-
host: 'github.com',
|
|
474
|
-
path: `/${path}.git`,
|
|
475
|
-
directory: basename(path),
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
455
|
const cleanGithubOrigin = validateVercelGithubOrigin
|
|
480
456
|
|
|
481
457
|
function sandboxName(origin: string, branch: string, generation = 0): string {
|
|
@@ -759,23 +735,6 @@ async function preflightBun(sandbox: VercelSandboxHandle, image: string): Promis
|
|
|
759
735
|
}
|
|
760
736
|
}
|
|
761
737
|
|
|
762
|
-
export async function packageAutobuildDistribution(): Promise<Uint8Array> {
|
|
763
|
-
const destination = await mkdtemp(join(tmpdir(), 'autobuild-pack-'))
|
|
764
|
-
try {
|
|
765
|
-
await execOrThrow(
|
|
766
|
-
spawnExec,
|
|
767
|
-
['bun', 'pm', 'pack', '--ignore-scripts', '--destination', destination],
|
|
768
|
-
distributionRoot(),
|
|
769
|
-
)
|
|
770
|
-
const archives = (await readdir(destination)).filter((name) => name.endsWith('.tgz'))
|
|
771
|
-
if (archives.length !== 1)
|
|
772
|
-
throw new Error('Autobuild packaging did not produce exactly one archive')
|
|
773
|
-
return new Uint8Array(await readFile(join(destination, archives[0]!)))
|
|
774
|
-
} finally {
|
|
775
|
-
await rm(destination, { recursive: true, force: true })
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
738
|
export interface VercelReadinessOptions {
|
|
780
739
|
config: VercelSandboxConfig
|
|
781
740
|
env: Record<string, string | undefined>
|
|
@@ -930,7 +889,7 @@ export async function validateVercelSandbox(
|
|
|
930
889
|
}
|
|
931
890
|
await provisionBun(sandbox, config.image, options.signal)
|
|
932
891
|
const provisioning = await runSystemProvisioning(sandbox, config.provisioning, options.signal)
|
|
933
|
-
const archive = await (options.packageArchive
|
|
892
|
+
const archive = await effectiveArchive(options.packageArchive)
|
|
934
893
|
checkCancellation()
|
|
935
894
|
await sandbox.writeFiles([{ path: '/tmp/autobuild.tgz', content: archive }], {
|
|
936
895
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
@@ -1052,24 +1011,16 @@ export async function validateVercelSandbox(
|
|
|
1052
1011
|
return { ...readiness!, snapshotsDeleted }
|
|
1053
1012
|
}
|
|
1054
1013
|
|
|
1055
|
-
export type RuntimeReferencesSource
|
|
1056
|
-
| readonly RuntimeReferenceGroup[]
|
|
1057
|
-
| (() => readonly RuntimeReferenceGroup[])
|
|
1058
|
-
|
|
1059
|
-
function currentRuntimeReferences(
|
|
1060
|
-
source: RuntimeReferencesSource | undefined,
|
|
1061
|
-
): readonly RuntimeReferenceGroup[] {
|
|
1062
|
-
if (source === undefined) return []
|
|
1063
|
-
return typeof source === 'function' ? source() : source
|
|
1064
|
-
}
|
|
1014
|
+
export type { RuntimeReferencesSource } from './provider-capabilities'
|
|
1065
1015
|
|
|
1066
1016
|
/** Install the distribution archive into `/opt/autobuild` and record its
|
|
1067
|
-
*
|
|
1068
|
-
* readback comparison guards a silently failed write or
|
|
1017
|
+
* identity marker. Shared by fresh provisioning, reuse-path refreshes, and
|
|
1018
|
+
* the restart path; the readback comparison guards a silently failed write or
|
|
1019
|
+
* extraction. */
|
|
1069
1020
|
async function installDistribution(
|
|
1070
1021
|
sandbox: VercelSandboxHandle,
|
|
1071
1022
|
archive: Uint8Array,
|
|
1072
|
-
|
|
1023
|
+
identity: string,
|
|
1073
1024
|
signal?: AbortSignal,
|
|
1074
1025
|
): Promise<void> {
|
|
1075
1026
|
await sandbox.writeFiles([{ path: '/tmp/autobuild.tgz', content: archive }], {
|
|
@@ -1091,12 +1042,12 @@ async function installDistribution(
|
|
|
1091
1042
|
cwd: VERCEL_AUTOBUILD_PATH,
|
|
1092
1043
|
...(signal === undefined ? {} : { signal }),
|
|
1093
1044
|
})
|
|
1094
|
-
await writeDistributionVersionMarker(sandbox,
|
|
1045
|
+
await writeDistributionVersionMarker(sandbox, identity, signal)
|
|
1095
1046
|
}
|
|
1096
1047
|
|
|
1097
1048
|
async function writeDistributionVersionMarker(
|
|
1098
1049
|
sandbox: VercelSandboxHandle,
|
|
1099
|
-
|
|
1050
|
+
identity: string,
|
|
1100
1051
|
signal?: AbortSignal,
|
|
1101
1052
|
): Promise<void> {
|
|
1102
1053
|
await commandOrThrow(sandbox, {
|
|
@@ -1105,7 +1056,7 @@ async function writeDistributionVersionMarker(
|
|
|
1105
1056
|
'-c',
|
|
1106
1057
|
'printf %s "$1" > "$2"',
|
|
1107
1058
|
'distribution-version',
|
|
1108
|
-
|
|
1059
|
+
identity,
|
|
1109
1060
|
VERCEL_DISTRIBUTION_VERSION_MARKER,
|
|
1110
1061
|
],
|
|
1111
1062
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -1115,17 +1066,17 @@ async function writeDistributionVersionMarker(
|
|
|
1115
1066
|
args: [VERCEL_DISTRIBUTION_VERSION_MARKER],
|
|
1116
1067
|
...(signal === undefined ? {} : { signal }),
|
|
1117
1068
|
})
|
|
1118
|
-
if (readback.trim() !==
|
|
1069
|
+
if (readback.trim() !== identity) {
|
|
1119
1070
|
throw new Error(
|
|
1120
|
-
`distribution
|
|
1121
|
-
`wrote ${JSON.stringify(
|
|
1071
|
+
`distribution identity marker readback mismatch in ${VERCEL_DISTRIBUTION_VERSION_MARKER}: ` +
|
|
1072
|
+
`wrote ${JSON.stringify(identity)} but read ${JSON.stringify(readback.trim())}`,
|
|
1122
1073
|
)
|
|
1123
1074
|
}
|
|
1124
1075
|
}
|
|
1125
1076
|
|
|
1126
|
-
/** The installed distribution's
|
|
1127
|
-
* unreadable — which includes every guest provisioned before the marker
|
|
1128
|
-
* existed and is treated as a
|
|
1077
|
+
/** The installed distribution's identity stamp, or `undefined` when the marker
|
|
1078
|
+
* is unreadable — which includes every guest provisioned before the marker
|
|
1079
|
+
* existed and is treated as a mismatch by the reuse and restart paths. */
|
|
1129
1080
|
async function readDistributionVersionMarker(
|
|
1130
1081
|
sandbox: VercelSandboxHandle,
|
|
1131
1082
|
signal?: AbortSignal,
|
|
@@ -1143,6 +1094,17 @@ async function readDistributionVersionMarker(
|
|
|
1143
1094
|
}
|
|
1144
1095
|
}
|
|
1145
1096
|
|
|
1097
|
+
/** The archive bytes a provider installs: the injected test archive when
|
|
1098
|
+
* present, else the per-process memoized running distribution archive
|
|
1099
|
+
* (`resolveDistributionArchive`), so a deployment without an injected archive
|
|
1100
|
+
* packs or fetches once per process rather than once per install site. */
|
|
1101
|
+
async function effectiveArchive(
|
|
1102
|
+
packageArchive: (() => Promise<Uint8Array>) | undefined,
|
|
1103
|
+
): Promise<Uint8Array> {
|
|
1104
|
+
if (packageArchive) return packageArchive()
|
|
1105
|
+
return (await resolveDistributionArchive()).archive
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1146
1108
|
export interface VercelSandboxProviderOptions {
|
|
1147
1109
|
config: VercelSandboxConfig
|
|
1148
1110
|
env: Record<string, string | undefined>
|
|
@@ -1163,11 +1125,14 @@ export interface VercelSandboxProviderOptions {
|
|
|
1163
1125
|
* the branch does not exist. Default runs `git ls-remote` from `repo` on
|
|
1164
1126
|
* the host. */
|
|
1165
1127
|
remoteBranchHead?: (branch: string) => Promise<string | undefined>
|
|
1166
|
-
/** Test seam: the distribution
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
|
|
1128
|
+
/** Test seam: the distribution identity stamp the current archive source
|
|
1129
|
+
* delivers (package version plus remote-store protocol version plus the
|
|
1130
|
+
* sha256 digest of the archive bytes, see `readDistributionIdentityStamp`).
|
|
1131
|
+
* Default computes the stamp over the effective archive source (origin mode
|
|
1132
|
+
* keys the guest archive to the running version; source mode packs the same
|
|
1133
|
+
* tree `readDistributionIdentity` reads), so the stamp and the installed
|
|
1134
|
+
* bytes agree by construction. */
|
|
1135
|
+
distributionIdentity?: () => Promise<string>
|
|
1171
1136
|
/** Test seam: the bound on the observation path's wait on a command whose
|
|
1172
1137
|
* lookup shows no exit. Defaults to `VERCEL_OBSERVE_WAIT_MS`. */
|
|
1173
1138
|
observeWaitMs?: number
|
|
@@ -1193,6 +1158,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1193
1158
|
readonly harvestExecution: HarvestExecution
|
|
1194
1159
|
readonly orchestratorSandbox: OperatorSandboxExecution
|
|
1195
1160
|
readonly publication
|
|
1161
|
+
readonly sandboxPublication: WorkspacePublication
|
|
1196
1162
|
readonly recovery
|
|
1197
1163
|
private readonly facade: VercelSandboxFacade
|
|
1198
1164
|
private readonly exec: Exec
|
|
@@ -1203,6 +1169,24 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1203
1169
|
private readonly sessions = new Map<string, VercelSandboxHandle>()
|
|
1204
1170
|
private readonly observeWaitMs: number
|
|
1205
1171
|
|
|
1172
|
+
/** The archive bytes this provider installs: the injected `packageArchive`
|
|
1173
|
+
* seam when set, else the per-process memoized running distribution
|
|
1174
|
+
* archive. Every install site routes through here, so the marker digest
|
|
1175
|
+
* always describes the bytes actually installed. */
|
|
1176
|
+
private archiveBytes(): Promise<Uint8Array> {
|
|
1177
|
+
return effectiveArchive(this.options.packageArchive)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/** The distribution identity stamp to write and compare: the injected
|
|
1181
|
+
* `distributionIdentity` seam when set (which pins the stamp independent of
|
|
1182
|
+
* the archive), else the default stamp computed over `archiveBytes()` — so
|
|
1183
|
+
* a provider that injects only `packageArchive` stamps the bytes it
|
|
1184
|
+
* actually installs and the seam divergence class is unrepresentable. */
|
|
1185
|
+
private async identityStamp(): Promise<string> {
|
|
1186
|
+
if (this.options.distributionIdentity) return this.options.distributionIdentity()
|
|
1187
|
+
return distributionStampFor(await this.archiveBytes())
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1206
1190
|
constructor(private readonly options: VercelSandboxProviderOptions) {
|
|
1207
1191
|
if (!/^https:\/\//i.test(options.storeRef) || options.storeToken === '') {
|
|
1208
1192
|
throw new Error('vercel-sandbox requires an HTTPS BuildStore and scoped AB_TOKEN authority')
|
|
@@ -1243,6 +1227,14 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1243
1227
|
isPublished: (input: { sha: string; branch: string }) => this.isPublished(input),
|
|
1244
1228
|
publish: (input: { ref: string; sha: string; branch: string }) => this.publish(input),
|
|
1245
1229
|
}
|
|
1230
|
+
// The operator-sandbox publication path reuses the SAME capability: its
|
|
1231
|
+
// publish pushes an exact sha from any sandbox ref, including an operator
|
|
1232
|
+
// environment, and its isPublished ignores the optional `ref` field.
|
|
1233
|
+
// Constructor-body assignment, never a field initializer — the class
|
|
1234
|
+
// assigns `publication` in the constructor too, and under the repo's
|
|
1235
|
+
// `useDefineForClassFields` semantics a field initializer would run
|
|
1236
|
+
// before that assignment and capture `undefined`.
|
|
1237
|
+
this.sandboxPublication = this.publication
|
|
1246
1238
|
}
|
|
1247
1239
|
|
|
1248
1240
|
async provision(opts: {
|
|
@@ -1255,12 +1247,10 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1255
1247
|
const rawOrigin = await this.origin()
|
|
1256
1248
|
const origin = cleanGithubOrigin(rawOrigin)
|
|
1257
1249
|
const name = sandboxName(origin.url, opts.branch, opts.generation)
|
|
1258
|
-
// Resolved once, before the create/reuse branch: the
|
|
1259
|
-
// archive source would deliver, against which both a fresh
|
|
1260
|
-
// marker and a reused sandbox's marker are compared.
|
|
1261
|
-
const
|
|
1262
|
-
this.options.distributionVersion ?? readDistributionIdentity
|
|
1263
|
-
)()
|
|
1250
|
+
// Resolved once, before the create/reuse branch: the identity stamp the
|
|
1251
|
+
// current archive source would deliver, against which both a fresh
|
|
1252
|
+
// install's marker and a reused sandbox's marker are compared.
|
|
1253
|
+
const distributionIdentity = await this.identityStamp()
|
|
1264
1254
|
let sandbox = await this.facade.get(name, this.operationSignal())
|
|
1265
1255
|
const existing = await this.remoteBranchHead(opts.branch, `remote branch ${opts.branch}`)
|
|
1266
1256
|
const base =
|
|
@@ -1279,7 +1269,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1279
1269
|
})
|
|
1280
1270
|
if (marker.exitCode === 0) {
|
|
1281
1271
|
const installed = await readDistributionVersionMarker(sandbox, this.operationSignal())
|
|
1282
|
-
if (installed ===
|
|
1272
|
+
if (installed === distributionIdentity) {
|
|
1283
1273
|
await sandbox.stop({ signal: this.operationSignal() })
|
|
1284
1274
|
} else {
|
|
1285
1275
|
// A completed sandbox whose installed distribution disagrees with
|
|
@@ -1287,8 +1277,13 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1287
1277
|
// without a marker — is refreshed in place. Any refresh failure
|
|
1288
1278
|
// deletes the sandbox so the next pass rematerializes cleanly.
|
|
1289
1279
|
try {
|
|
1290
|
-
const archive = await
|
|
1291
|
-
await installDistribution(
|
|
1280
|
+
const archive = await this.archiveBytes()
|
|
1281
|
+
await installDistribution(
|
|
1282
|
+
sandbox,
|
|
1283
|
+
archive,
|
|
1284
|
+
distributionIdentity,
|
|
1285
|
+
this.operationSignal(),
|
|
1286
|
+
)
|
|
1292
1287
|
} catch (error) {
|
|
1293
1288
|
try {
|
|
1294
1289
|
this.sessions.set(name, sandbox)
|
|
@@ -1374,8 +1369,8 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1374
1369
|
}
|
|
1375
1370
|
await provisionBun(sandbox, this.options.config.image)
|
|
1376
1371
|
await runSystemProvisioning(sandbox, this.options.config.provisioning ?? [])
|
|
1377
|
-
const archive = await
|
|
1378
|
-
await installDistribution(sandbox, archive,
|
|
1372
|
+
const archive = await this.archiveBytes()
|
|
1373
|
+
await installDistribution(sandbox, archive, distributionIdentity, this.operationSignal())
|
|
1379
1374
|
// Repository dependencies precede branch-owned package plugin loading.
|
|
1380
1375
|
// The fixed bootstrap supports the consuming repository's lockfile; its
|
|
1381
1376
|
// configured setup command still runs at every runner attachment.
|
|
@@ -1541,6 +1536,32 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1541
1536
|
await sandbox.stop({ signal: this.operationSignal() })
|
|
1542
1537
|
this.uncertain.delete(ref)
|
|
1543
1538
|
}
|
|
1539
|
+
// Restart (the lease sweep's re-attach) launches guest code in an existing
|
|
1540
|
+
// sandbox, so it must compare the distribution identity exactly as the
|
|
1541
|
+
// reuse paths do: a guest installed by an older dispatcher — including one
|
|
1542
|
+
// stranded by a protocol-only bump or a missing/unreadable marker — is
|
|
1543
|
+
// refreshed before the runner starts, or the hosted store would 409 its
|
|
1544
|
+
// heartbeat and the build would strand. Refresh failure deletes the
|
|
1545
|
+
// sandbox so the next pass rematerializes it cleanly.
|
|
1546
|
+
const distributionIdentity = await this.identityStamp()
|
|
1547
|
+
const installed = await readDistributionVersionMarker(sandbox, this.operationSignal())
|
|
1548
|
+
if (installed !== distributionIdentity) {
|
|
1549
|
+
try {
|
|
1550
|
+
const archive = await this.archiveBytes()
|
|
1551
|
+
await installDistribution(sandbox, archive, distributionIdentity, this.operationSignal())
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
try {
|
|
1554
|
+
this.sessions.set(ref, sandbox)
|
|
1555
|
+
await this.reap(ref)
|
|
1556
|
+
} catch (deleteError) {
|
|
1557
|
+
throw new AggregateError(
|
|
1558
|
+
[error, deleteError],
|
|
1559
|
+
`sandbox ${ref} distribution refresh failed and its incomplete environment could not be confirmed deleted`,
|
|
1560
|
+
)
|
|
1561
|
+
}
|
|
1562
|
+
throw error
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1544
1565
|
await preflightBun(sandbox, this.options.config.image)
|
|
1545
1566
|
await bootstrapRuntimes(
|
|
1546
1567
|
sandbox,
|
|
@@ -1679,12 +1700,10 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1679
1700
|
const origin = cleanGithubOrigin(await this.origin())
|
|
1680
1701
|
const name = harvestSandboxName(origin.url)
|
|
1681
1702
|
if (this.active.has(name)) throw new Error(`sandbox ${name} already has a live execution`)
|
|
1682
|
-
// Resolved once, before the create/reuse branch: the
|
|
1683
|
-
// archive source would deliver, against which both a fresh
|
|
1684
|
-
// marker and a reused environment's marker are compared.
|
|
1685
|
-
const
|
|
1686
|
-
this.options.distributionVersion ?? readDistributionIdentity
|
|
1687
|
-
)()
|
|
1703
|
+
// Resolved once, before the create/reuse branch: the identity stamp the
|
|
1704
|
+
// current archive source would deliver, against which both a fresh
|
|
1705
|
+
// install's marker and a reused environment's marker are compared.
|
|
1706
|
+
const distributionIdentity = await this.identityStamp()
|
|
1688
1707
|
let sandbox = await this.facade.get(name, this.operationSignal())
|
|
1689
1708
|
if (sandbox !== null) {
|
|
1690
1709
|
const marker = await sandbox.runCommand({
|
|
@@ -1703,10 +1722,15 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1703
1722
|
// disagrees with what the current system would deliver is refreshed
|
|
1704
1723
|
// in place, mirroring the build reuse path.
|
|
1705
1724
|
const installed = await readDistributionVersionMarker(sandbox, this.operationSignal())
|
|
1706
|
-
if (installed !==
|
|
1725
|
+
if (installed !== distributionIdentity) {
|
|
1707
1726
|
try {
|
|
1708
|
-
const archive = await
|
|
1709
|
-
await installDistribution(
|
|
1727
|
+
const archive = await this.archiveBytes()
|
|
1728
|
+
await installDistribution(
|
|
1729
|
+
sandbox,
|
|
1730
|
+
archive,
|
|
1731
|
+
distributionIdentity,
|
|
1732
|
+
this.operationSignal(),
|
|
1733
|
+
)
|
|
1710
1734
|
} catch (error) {
|
|
1711
1735
|
try {
|
|
1712
1736
|
this.sessions.set(name, sandbox)
|
|
@@ -1785,8 +1809,8 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1785
1809
|
}
|
|
1786
1810
|
await provisionBun(sandbox, this.options.config.image)
|
|
1787
1811
|
await runSystemProvisioning(sandbox, this.options.config.provisioning ?? [])
|
|
1788
|
-
const archive = await
|
|
1789
|
-
await installDistribution(sandbox, archive,
|
|
1812
|
+
const archive = await this.archiveBytes()
|
|
1813
|
+
await installDistribution(sandbox, archive, distributionIdentity, this.operationSignal())
|
|
1790
1814
|
// Repository dependencies precede runtime preflight; the fixed
|
|
1791
1815
|
// bootstrap is the same one build provisioning runs.
|
|
1792
1816
|
await commandOrThrow(sandbox, {
|
|
@@ -2006,9 +2030,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
2006
2030
|
}): Promise<SandboxEnvironmentIdentity> {
|
|
2007
2031
|
const origin = cleanGithubOrigin(await this.origin())
|
|
2008
2032
|
const name = sandboxEnvironmentName(origin.url, input.operator)
|
|
2009
|
-
const
|
|
2010
|
-
this.options.distributionVersion ?? readDistributionIdentity
|
|
2011
|
-
)()
|
|
2033
|
+
const distributionIdentity = await this.identityStamp()
|
|
2012
2034
|
let sandbox = await this.facade.get(name, this.operationSignal())
|
|
2013
2035
|
if (sandbox !== null) {
|
|
2014
2036
|
const marker = await sandbox.runCommand({
|
|
@@ -2022,14 +2044,19 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
2022
2044
|
sandbox = null
|
|
2023
2045
|
} else {
|
|
2024
2046
|
const installed = await readDistributionVersionMarker(sandbox, this.operationSignal())
|
|
2025
|
-
if (installed !==
|
|
2047
|
+
if (installed !== distributionIdentity) {
|
|
2026
2048
|
// A reused environment whose installed distribution disagrees with
|
|
2027
2049
|
// what the current system would deliver is refreshed in place,
|
|
2028
2050
|
// exactly as build guests are. Refresh failure deletes the
|
|
2029
2051
|
// environment so the next call rematerializes it cleanly.
|
|
2030
2052
|
try {
|
|
2031
|
-
const archive = await
|
|
2032
|
-
await installDistribution(
|
|
2053
|
+
const archive = await this.archiveBytes()
|
|
2054
|
+
await installDistribution(
|
|
2055
|
+
sandbox,
|
|
2056
|
+
archive,
|
|
2057
|
+
distributionIdentity,
|
|
2058
|
+
this.operationSignal(),
|
|
2059
|
+
)
|
|
2033
2060
|
} catch (error) {
|
|
2034
2061
|
try {
|
|
2035
2062
|
this.sessions.set(name, sandbox)
|
|
@@ -2114,8 +2141,8 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
2114
2141
|
}
|
|
2115
2142
|
await provisionBun(sandbox, this.options.config.image)
|
|
2116
2143
|
await runSystemProvisioning(sandbox, this.options.config.provisioning ?? [])
|
|
2117
|
-
const archive = await
|
|
2118
|
-
await installDistribution(sandbox, archive,
|
|
2144
|
+
const archive = await this.archiveBytes()
|
|
2145
|
+
await installDistribution(sandbox, archive, distributionIdentity, this.operationSignal())
|
|
2119
2146
|
// Repository dependencies precede the setup command; the fixed
|
|
2120
2147
|
// bootstrap is the same one build provisioning runs.
|
|
2121
2148
|
await commandOrThrow(sandbox, {
|
|
@@ -2182,6 +2209,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
2182
2209
|
environmentId: name,
|
|
2183
2210
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
2184
2211
|
workspacePath: VERCEL_WORKSPACE_PATH,
|
|
2212
|
+
baseSha: head,
|
|
2185
2213
|
}
|
|
2186
2214
|
}
|
|
2187
2215
|
|
|
@@ -10,6 +10,7 @@ import { openProductionStore } from '../cli/store-opening'
|
|
|
10
10
|
import { BuildRunner, LeaseHeldError, SetupFailureError } from './build-runner'
|
|
11
11
|
import {
|
|
12
12
|
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
13
|
+
buildOwnedSectionsDiffer,
|
|
13
14
|
diagnosticArtifact,
|
|
14
15
|
parseEffectiveBuildConfig,
|
|
15
16
|
selectOpenWorkspace,
|
|
@@ -22,11 +23,17 @@ function message(error: unknown): string {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** Child-side composition. The launch envelope supplies identity only; all
|
|
25
|
-
* execution location/configuration is discovered through the scoped Store.
|
|
26
|
+
* execution location/configuration is discovered through the scoped Store.
|
|
27
|
+
* `opts.guest` marks a structurally guest process (environment-supervised
|
|
28
|
+
* launch): plugin loading then tolerates a configured package specifier the
|
|
29
|
+
* guest cannot resolve — guests never construct workspace providers, so a
|
|
30
|
+
* provider plugin configured in autobuild.toml is skipped with a notice
|
|
31
|
+
* instead of failing the build (AUT-517). */
|
|
26
32
|
export async function runBuildChild(
|
|
27
33
|
input: BuildExecutionStart,
|
|
28
34
|
env: Record<string, string | undefined> = process.env,
|
|
29
35
|
openStore: (ref: string, token?: string) => BuildStore = openProductionStore,
|
|
36
|
+
opts: { guest?: boolean } = {},
|
|
30
37
|
): Promise<void> {
|
|
31
38
|
const fullStore = openStore(input.storeRef, env.AB_TOKEN)
|
|
32
39
|
const store = fullStore.scopeBuild(input.slug)
|
|
@@ -50,6 +57,7 @@ export async function runBuildChild(
|
|
|
50
57
|
let currentConfig = parseEffectiveBuildConfig(configArtifact)
|
|
51
58
|
const plugins = await loadPlugins(currentConfig.plugins, workspace.path, {
|
|
52
59
|
packageRoot: record.repo,
|
|
60
|
+
...(opts.guest === true ? { guest: true } : {}),
|
|
53
61
|
})
|
|
54
62
|
const { runtimes: builtins } = createProductionRuntimes()
|
|
55
63
|
const runtimes = await materializePluginRuntimes(builtins, plugins, {
|
|
@@ -66,7 +74,14 @@ export async function runBuildChild(
|
|
|
66
74
|
const latest = await store.getArtifact(input.slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
67
75
|
if (latest === null) return currentConfig
|
|
68
76
|
try {
|
|
69
|
-
|
|
77
|
+
const parsed = parseEffectiveBuildConfig(latest)
|
|
78
|
+
// Defensive guard (SPEC §16.1): after pinning, a boundary deposit may
|
|
79
|
+
// move deployment-owned sections only. If build-owned sections change
|
|
80
|
+
// without the build's own branch advancing, the artifact was written
|
|
81
|
+
// by something other than a launch — keep the last valid boundary.
|
|
82
|
+
if (!buildOwnedSectionsDiffer(currentConfig, parsed)) {
|
|
83
|
+
currentConfig = parsed
|
|
84
|
+
}
|
|
70
85
|
} catch {
|
|
71
86
|
// The dispatcher publishes only validated snapshots. If storage is
|
|
72
87
|
// externally corrupted, preserve the child's last valid boundary.
|