#!/usr/bin/env node /** * `npm run deploy` — build the current source, run wrangler deploy, then print * a next-steps epilogue. Pass `--skip-build` only when a caller has just run * `npm run build` and wants to reuse that exact artifact. * * Workers Builds streams the build and deploy steps into one log that ends, * without this, at wrangler's own output: a raw workers.dev URL and no hint * that /admin is the next stop. Wrangler 4.x has no command that reports the * account's workers.dev subdomain, but `wrangler deploy` prints the deployed * URL on success — so tee its stdout, grep the URL, and append instructions. * * The epilogue is additive: wrangler's output passes through untouched, a * non-zero exit propagates unchanged with no epilogue (never dress up a * failed deploy), and stdout is scanned line-by-line rather than buffered. */ import { spawn, spawnSync } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, } from 'node:fs'; import { homedir, tmpdir } from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import { hasScheduledComposition } from './worker-artifact.mjs'; import { builtWorkerConfigPath } from './lib/built-worker-config.mjs'; import { mergeDeploymentSecrets, OPERATOR_SECRETS_ENV, readOperatorSecretsFile } from './lib/deploy-operator-secrets.mjs'; import { isQaLane } from './lib/qa-lanes.mjs'; import { describeLaneSecrets, ensureLaneSeedToken, LANE_SEED_TOKEN_BINDING, resolveLaneSecrets } from './lib/lane-secrets.mjs'; import { wranglerInspector, deploymentFingerprint } from './lib/inspect-deployment.mjs'; import { AUTH_SCHEMA_QUERY, expectedAuthSchema, normalizeAuthSchemaRows } from './lib/auth-schema.mjs'; import { validateInstallation, validateTarget, assertSameInstallation, overlayInstallation, wranglerProfileArgs } from './lib/upgrade-installation.mjs'; import { readPrivateJson, writePrivateJson, writeDeploymentEvent } from './lib/upgrade-receipt.mjs'; import { verifyRetainedBuildRoot } from './lib/upgrade-source.mjs'; import { preflightCloudflareAccount, assertCloudflareAccountConfig } from './lib/cloudflare-account-preflight.mjs'; import { formatPreflightProblems, prebuildSandboxImage, preflightSandboxDeployment, rerunCommand, sandboxApplicationName, sandboxPartialDeployRecovery, sandboxStoppedBeforeUpload, uploadedBeforeFailure, useSandboxImage, checkpointsOffNotice, withoutCheckpointBucket, verifySandboxContainerApplication, } from './lib/sandbox-deploy-preflight.mjs'; import { classifyCloudflareDeploymentProfile, formatCloudflareDeploymentTargetTuple, readCloudflareDeploymentTargetTuple, resolveCloudflareDeploymentProfile, } from './cloudflare-deployment-profile.mjs'; import { mintSetupCapability, SETUP_CAPABILITY_DIGEST_BINDING, SETUP_CAPABILITY_ISSUED_AT_BINDING, setupCapabilityUrl, } from '../src/auth/setup-capability.mjs'; import { DEPLOYMENT_ACTIVATION_DIGEST_BINDING, DEPLOYMENT_ACTIVATION_ISSUED_AT_BINDING, mintDeploymentActivation, } from '../src/auth/deployment-activation.mjs'; import { parseResidencyHoldMs, RESIDENCY_HOLD_VARIABLE } from '../src/state/residency-hold.ts'; const runnerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // Invoke wrangler's bin with the current node (mirrors flue-build-cf.mjs): // use the current runner's reviewed deployment tooling, including recovery. const wranglerBin = path.join(runnerRoot, 'node_modules', 'wrangler', 'bin', 'wrangler.js'); const cliArgs = process.argv.slice(2); // `--worktree ` deploys another checkout's candidate through that // checkout's own reviewed wrapper, from that checkout, so a verifier that // batches repairs in a sibling worktree runs one plain command instead of // `cd && ...`. The flag never applies here; the target's script // sees the remaining arguments and the same environment. { const index = cliArgs.findIndex((arg) => arg === '--worktree' || arg.startsWith('--worktree=')); if (index >= 0) { const inline = cliArgs[index].startsWith('--worktree='); const requested = inline ? cliArgs[index].slice('--worktree='.length) : cliArgs[index + 1]; const consumed = inline ? 1 : 2; if (!requested || requested.startsWith('--')) { console.error('--worktree needs an absolute checkout path.'); process.exit(2); } if (!path.isAbsolute(requested)) { console.error(`--worktree must be absolute, not ${requested}.`); process.exit(2); } const target = path.resolve(requested); const script = path.join(target, 'scripts', 'deploy-with-epilogue.mjs'); if (!existsSync(script)) { console.error(`${target} is not a Chickpea checkout (no scripts/deploy-with-epilogue.mjs).`); process.exit(2); } cliArgs.splice(index, consumed); if (realpathSync(target) !== realpathSync(runnerRoot)) { const forwarded = spawnSync(process.execPath, [script, ...cliArgs], { cwd: target, stdio: 'inherit', env: process.env }); if (forwarded.error) console.error(`Could not run ${script}: ${forwarded.error.message}`); if (forwarded.signal) process.kill(process.pid, forwarded.signal); process.exit(forwarded.status ?? 1); } } } // `--release-tag vX.Y.Z` admits a published release to a QA lane after main // has moved past it: HEAD must be exactly that tag and the tag must be on main. let releaseTagOption; { const index = cliArgs.findIndex((arg) => arg === '--release-tag' || arg.startsWith('--release-tag=')); if (index >= 0) { const inline = cliArgs[index].startsWith('--release-tag='); releaseTagOption = inline ? cliArgs[index].slice('--release-tag='.length) : cliArgs[index + 1]; if (!releaseTagOption || releaseTagOption.startsWith('--')) { console.error('--release-tag needs a tag such as v0.1.33.'); process.exit(2); } cliArgs.splice(index, inline ? 1 : 2); } } const deployArgs = cliArgs.filter((arg) => !['--skip-build', '--preflight-only'].includes(arg)); const skipBuild = cliArgs.includes('--skip-build'); const preflightOnly = cliArgs.includes('--preflight-only'); const upgradeContextPath = process.env.CHICKPEA_UPGRADE_CONTEXT; let upgradeContext; let projectRoot = runnerRoot; try { if (upgradeContextPath) { upgradeContext = readPrivateJson(upgradeContextPath); if (upgradeContext.sourceRoot !== undefined) { if (!skipBuild) throw new Error('A retained upgrade artifact must already be built.'); projectRoot = verifyRetainedBuildRoot(upgradeContextPath, upgradeContext); } } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } // A QA checkout must never fall through to the ordinary production target. // This is a selection check, not claim authority; the environment preflight // below still validates the actual lease, source, and immutable identities. // // `CHICKPEA_DEPLOY_TARGET=production` names the ordinary wrangler.jsonc Worker // on purpose. It is consumed here so the build profile, which knows only the // claimed lanes, still sees an unset target for that deploy. const PRODUCTION_DEPLOY_TARGET = 'production'; // Mirrors defaultEnvironmentRoot() in scripts/lib/environment-registry.mjs; // inlined so the ordinary production deploy still loads no lane module. const claimedLaneRegistryRoot = process.env.CHICKPEA_ENVIRONMENT_ROOT?.trim() || path.join(homedir(), '.chickpea', 'environments'); const explicitProductionTarget = process.env.CHICKPEA_DEPLOY_TARGET?.trim() === PRODUCTION_DEPLOY_TARGET; if (explicitProductionTarget) delete process.env.CHICKPEA_DEPLOY_TARGET; const requestedDeploymentTarget = process.env.CHICKPEA_DEPLOY_TARGET?.trim(); const installationLane = process.env.CHICKPEA_INSTALLATION_LANE?.trim(); let installationApi; try { if (installationLane) { if (!explicitProductionTarget || requestedDeploymentTarget) throw new Error('A borrowed installation uses the ordinary explicitly selected Worker in its isolated installation checkout.'); installationApi = await import('./lib/environment-installation.mjs'); installationApi.assertInstallationDeployment(installationLane, projectRoot); } const markerPath = path.join(projectRoot, '.chickpea-environment'); let markerStat; try { markerStat = lstatSync(markerPath); } catch (error) { if (error.code !== 'ENOENT') throw error; } if (markerStat) { if (!markerStat.isFile() || markerStat.isSymbolicLink()) { throw new Error('QA claim marker is not a regular file. Refusing deployment.'); } let marker; try { marker = JSON.parse(readFileSync(markerPath, 'utf8')); } catch { throw new Error('QA claim marker is unreadable. Refusing deployment.'); } if (marker?.schemaVersion !== 'chickpea-environment-claim/v1' || !isQaLane(marker.target)) { throw new Error('QA claim marker is invalid. Refusing deployment.'); } if (requestedDeploymentTarget !== marker.target) { throw new Error(`This worktree claims ${marker.target}. Set CHICKPEA_DEPLOY_TARGET=${marker.target}; refusing an unspecified or different target.`); } } if (!requestedDeploymentTarget && (process.env.CHICKPEA_LOCAL_LANE || existsSync(path.join(projectRoot, '.chickpea-local-worker')))) { throw new Error('This checkout owns a local Worker lane. Select an explicitly claimed QA deployment target; the default production deployment is refused.'); } // A machine that operates claimed QA lanes keeps a registry under // ~/.chickpea/environments. On such a machine an unnamed deploy is far more // often a checkout that forgot its lane than a deliberate production push, // so the ordinary Worker must be named explicitly. Self-hosters, Workers // Builds, and non-mutating dry runs have no registry and are unaffected. const mutatingDeploy = !deployArgs.includes('--dry-run') && !preflightOnly; if (!requestedDeploymentTarget && !explicitProductionTarget && mutatingDeploy && process.env.WORKERS_CI !== '1' && existsSync(claimedLaneRegistryRoot)) { throw new Error( 'This machine operates claimed QA lanes, so an unnamed deploy is refused. ' + 'Set CHICKPEA_DEPLOY_TARGET=amber, cobalt, or violet for a claimed lane, or ' + `CHICKPEA_DEPLOY_TARGET=${PRODUCTION_DEPLOY_TARGET} to deploy the ordinary wrangler.jsonc Worker on purpose.`, ); } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } if ( isQaLane(requestedDeploymentTarget) && cliVariable('CHICKPEA_TELEMETRY_ENVIRONMENT') !== undefined ) { console.error( 'Do not override CHICKPEA_TELEMETRY_ENVIRONMENT for a QA deployment target; ' + 'the reviewed artifact must supply the canonical test label.', ); process.exit(1); } // Workers Builds runs its configured build command immediately before its // configured deploy command in the same build workspace. Reuse that exact // artifact, but only when both Workers-specific markers are present; local // deploys and generic CI retain the build-before-deploy safety boundary. const reuseWorkersBuildArtifact = process.env.WORKERS_CI === '1' && typeof process.env.WORKERS_CI_BUILD_UUID === 'string' && process.env.WORKERS_CI_BUILD_UUID.trim().length > 0; let deploymentProfile; try { // Resolve once so build, preflight, D1 setup, and deploy cannot disagree if // a caller mutates process.env later in this process. deploymentProfile = resolveCloudflareDeploymentProfile(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } // An unset profile means core, but only an explicit `core` may replace a live // coding sandbox: a routine update must not silently uninstall it. const explicitCoreProfile = process.env.CHICKPEA_DEPLOY_PROFILE?.trim() === 'core'; // Local sandbox deploys prove Docker, the base image, and Containers access, // then push the image, all before the Worker upload that makes the SANDBOX // binding live. Workers Builds keeps Wrangler's own container build; a // `--containers-rollout=none` deploy deliberately skips the Container step. const guardedSandboxDeploy = deploymentProfile === 'sandbox' && !preflightOnly && !deployArgs.some((arg) => ['--dry-run', '--help', '-h'].includes(arg)) && !deployArgs.some((arg) => arg === '--containers-rollout=none' || arg === '--containers-rollout') && process.env.WORKERS_CI !== '1' && !upgradeContextPath; const BETA_FLUE_CLASSES = Object.freeze([ 'FlueRegistry', 'FlueSlackThreadAgent', 'FlueRoutineIntentAgent', 'FlueRoutineWorkflow', ]); const RETIRED_AUTH_CLASSES = Object.freeze(['AuthGuard']); const EXPECTED_DELETED_CLASSES = Object.freeze([ ...BETA_FLUE_CLASSES, ...RETIRED_AUTH_CLASSES, ]); const V2_AGENT_CLASSES = Object.freeze([ 'FlueChickpeaSlackV2Agent', 'FlueChickpeaRoutineIntentV2Agent', 'FlueChickpeaRoutineExecutionV2Agent', ]); const V2_AGENT_BINDINGS = Object.freeze([ ['FLUE_CHICKPEA_SLACK_V2_AGENT', 'FlueChickpeaSlackV2Agent'], ['FLUE_CHICKPEA_ROUTINE_INTENT_V2_AGENT', 'FlueChickpeaRoutineIntentV2Agent'], ['FLUE_CHICKPEA_ROUTINE_EXECUTION_V2_AGENT', 'FlueChickpeaRoutineExecutionV2Agent'], ]); const PROTECTED_CLASSES = new Set(['TagStateStore', 'Sandbox', 'ContainerProxy']); function hasCustomConfigFlag(args) { return args.some((argument) => argument === '--config' || argument === '-c' || argument.startsWith('--config=') ); } function hasWorkerNameOverride(args) { return args.some((argument) => argument === '--name' || argument.startsWith('--name=')); } if (hasCustomConfigFlag(deployArgs)) { console.error( 'Do not pass a custom Wrangler config. Build the Vite artifact and use its generated deploy redirect.', ); process.exit(1); } if (hasWorkerNameOverride(deployArgs)) { console.error( 'Do not override the Worker name. Chickpea must inspect and mutate the same Worker it deploys.', ); process.exit(1); } function validateAgentViewManifest() { const manifestPath = path.join(projectRoot, 'slack-app-manifest.json'); let manifest; try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Unable to validate the Slack manifest before deployment: ${detail}`); } const hasAgentView = Boolean(manifest?.features?.agent_view); const hasAssistantView = Boolean(manifest?.features?.assistant_view); if (hasAgentView && hasAssistantView) { throw new Error( 'Slack manifest is invalid for deployment: agent_view and assistant_view cannot coexist.', ); } if (!hasAgentView) { throw new Error( 'Slack manifest requires features.agent_view as the permanent app-home contract.', ); } } try { validateAgentViewManifest(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } // A selected Phase 1 lane is a permanent, claimed environment. Resolve that // authority before even building so an expired/rotated claim or stale HEAD // cannot cause a build hook (or anything after it) to run. The ordinary // production deploy does not load this module when no lane was selected and // the checkout has no QA ownership marker, as checked above. const selectedEnvironmentTarget = !deployArgs.includes('--dry-run') && requestedDeploymentTarget ? requestedDeploymentTarget : undefined; if (releaseTagOption !== undefined && !selectedEnvironmentTarget) { console.error('--release-tag applies only to a QA lane deploy: set CHICKPEA_DEPLOY_TARGET= and omit --dry-run.'); process.exit(2); } let environmentPreflightApi; let initialEnvironmentPreflight; let resumedEnvironmentDeployment; let environmentMutationLease; let qaCandidateApi; let qaSourceAdmission; if (selectedEnvironmentTarget) { try { // Admission is deliberately outside the nonce-bound intent/receipt schema. // Every upload, including a resumed upload, needs it. The separate env // reconciliation command can finish old intents without changing source. qaCandidateApi = await import('./lib/qa-candidate.mjs'); qaSourceAdmission = qaCandidateApi.admitQaCandidate(projectRoot, releaseTagOption === undefined ? {} : { releaseTag: releaseTagOption }); process.stdout.write(`QA source includes remote main ${qaSourceAdmission.approvedTip}; tracking ref ${qaSourceAdmission.trackingMatchesRemote ? 'matches' : 'differs (use the admitted tip as --base)'}.\n`); environmentPreflightApi = await import('./lib/environment-preflight.mjs'); resumedEnvironmentDeployment = await environmentPreflightApi.resumeEnvironmentDeployment( selectedEnvironmentTarget, { projectRoot, providerContext: deploymentResourceArgs() }, ); initialEnvironmentPreflight = resumedEnvironmentDeployment?.preflight ?? await environmentPreflightApi.preflightEnvironmentMutation( selectedEnvironmentTarget, { projectRoot, providerContext: deploymentResourceArgs() }, ); // The claimed lane's immutable AUTH_DB and schema generation are already // known from the registry the preflight just validated. Feed them to the // build profile so a claimed deploy does not depend on the operator // exporting CHICKPEA_DEPLOY_AUTH_DB_ID and CHICKPEA_DEPLOY_SCHEMA_GENERATION // by hand; explicit values still win and are still checked below. const metadata = initialEnvironmentPreflight.deploymentMetadata; if (selectedEnvironmentTarget === 'violet') { const registration = initialEnvironmentPreflight.registration; for (const [key, value] of Object.entries({ CHICKPEA_DEPLOY_WORKER_NAME: registration.workerName, CHICKPEA_DEPLOY_AUTH_DB_NAME: registration.authDatabaseName, })) { if (process.env[key]?.trim() && process.env[key].trim() !== value) { throw new Error(`${key} does not match the claimed Violet installation.`); } process.env[key] = value; } } const resolved = []; if (!process.env.CHICKPEA_DEPLOY_AUTH_DB_ID?.trim() && metadata?.authDatabaseId) { process.env.CHICKPEA_DEPLOY_AUTH_DB_ID = metadata.authDatabaseId; resolved.push('AUTH_DB id'); } if (!process.env.CHICKPEA_DEPLOY_SCHEMA_GENERATION?.trim() && metadata?.schemaGeneration) { process.env.CHICKPEA_DEPLOY_SCHEMA_GENERATION = metadata.schemaGeneration; resolved.push('schema generation'); } if (resolved.length > 0) { process.stdout.write( `Resolved ${resolved.join(' and ')} for ${selectedEnvironmentTarget} from the claimed registration.\n`, ); } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } function buildCloudflareArtifact() { process.stdout.write('Building the Cloudflare artifact from current source...\n'); const npmExecPath = process.env.npm_execpath; const buildCommand = npmExecPath ? [process.execPath, [npmExecPath, 'run', 'build']] : [process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build']]; const build = spawnSync(buildCommand[0], buildCommand[1], { cwd: projectRoot, stdio: 'inherit', }); if (build.error) { console.error(`Unable to start the Cloudflare build: ${build.error.message}`); process.exit(1); } if (build.status !== 0) { process.exit(build.status ?? 1); } } const checkAccount = !preflightOnly && !deployArgs.some((arg) => ['--dry-run', '--help', '-h'].includes(arg)); let checkedAccount; function accountCheckOptions(configPath) { return { runnerRoot, projectRoot, configPath, providerContext: deploymentResourceArgs(), ...(upgradeContext ? { expectedAccount: upgradeContext.target?.account } : {}) }; } async function checkDeploymentAccount(configPath) { const result = await preflightCloudflareAccount(accountCheckOptions(configPath)); // Pin an unambiguous whoami selection so later Wrangler calls cannot choose // another account. This affects only the current guarded deploy process. if (result.accountId) process.env.CLOUDFLARE_ACCOUNT_ID = result.accountId; return result; } try { if (checkAccount && !skipBuild && !reuseWorkersBuildArtifact) { checkedAccount = await checkDeploymentAccount(path.join(projectRoot, 'wrangler.jsonc')); } } catch (error) { console.error(error.message); process.exit(1); } function sandboxToolOptions(configPath) { return { wranglerBin, projectRoot, configPath, providerContext: deploymentResourceArgs(), dockerfilePath: path.join(projectRoot, 'Dockerfile'), accountId: checkedAccount?.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID, ...(process.env.DEPLOY_TEST_SANDBOX_RETRY_MS ? { retryDelayMs: Number(process.env.DEPLOY_TEST_SANDBOX_RETRY_MS) } : {}), }; } // An account without R2 cannot hold the checkpoint bucket. Checkpoints are // optional, so the deploy drops that binding instead of failing mid-deploy. let sandboxCheckpointsOff = false; if (guardedSandboxDeploy) { try { const { problems, checkpoints } = preflightSandboxDeployment(sandboxToolOptions(path.join(projectRoot, 'wrangler.jsonc'))); sandboxCheckpointsOff = !checkpoints; if (problems.length) { console.error(formatPreflightProblems(problems)); process.exit(1); } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } // A claimed QA lane gets two cheap checks before the build: dependencies that // match the lockfile (a stale install builds a candidate nobody reviewed), and // a deploy profile that matches the live Worker (a core deploy over a sandbox // lane is refused after the build anyway; refuse it before spending one). if (selectedEnvironmentTarget && !skipBuild && !reuseWorkersBuildArtifact) { try { const { lockfileDrift, staleDependenciesMessage } = await import('./lib/installed-dependencies.mjs'); const drift = lockfileDrift(projectRoot); if (drift.length) throw new Error(staleDependenciesMessage(drift, 'Nothing was built or changed.')); const workerName = initialEnvironmentPreflight.registration.workerName; if (deploymentProfile === 'core' && !explicitCoreProfile) { const { readLiveWorkerProfile } = await import('./lib/environment-capabilities.mjs'); const live = await readLiveWorkerProfile(workerName, { providerContext: deploymentResourceArgs() }); if (live.profile === 'unknown') { process.stdout.write(`! Could not read ${workerName}'s live deploy profile (${live.errors.join(', ') || 'no answer'}); ` + 'the post-build check still refuses a core deploy over a sandbox Worker.\n'); } if (live.profile === 'sandbox' || live.profile === 'mixed') { throw new Error( `Claimed lane ${selectedEnvironmentTarget} serves the sandbox profile, and this command would deploy the core profile. ` + `Run \`CHICKPEA_DEPLOY_TARGET=${selectedEnvironmentTarget} npm run verify:host -- --wait-ms 300000 npm run deploy:sandbox\` instead. ` + 'Nothing was built or changed.', ); } } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } if (!skipBuild && !reuseWorkersBuildArtifact) buildCloudflareArtifact(); function sortedUnique(values) { return [...new Set(values)].sort(); } function sameMembers(actual, expected) { return actual.length === expected.length && JSON.stringify([...actual].sort()) === JSON.stringify([...expected].sort()); } function renamedClassNames(migrations) { const names = []; for (const migration of migrations) { for (const rename of migration.renamed_classes ?? []) { if (typeof rename?.from === 'string') names.push(rename.from); if (typeof rename?.to === 'string') names.push(rename.to); } } return names; } function requireBuiltArtifact() { const configPath = builtWorkerConfigPath(projectRoot); const config = JSON.parse(readFileSync(configPath, 'utf8')); const artifactRoot = path.dirname(configPath); const bundlePath = path.resolve(artifactRoot, config.main ?? 'index.js'); const bundle = existsSync(bundlePath) ? readdirSync(artifactRoot, { recursive: true }) .filter((entry) => typeof entry === 'string' && entry.endsWith('.js')) .sort() .map((entry) => readFileSync(path.join(artifactRoot, entry), 'utf8')) .join('\n') : ''; return { configPath, config, bundle, entry: existsSync(bundlePath) ? readFileSync(bundlePath, 'utf8') : '' }; } function expectedWorkerName(targetTuple) { if (targetTuple) return targetTuple.workerName; const override = process.env.WRANGLER_CI_OVERRIDE_NAME?.trim(); if (override) return override; try { const source = readFileSync(path.join(projectRoot, 'wrangler.jsonc'), 'utf8'); const match = source.match(/"name"\s*:\s*"([^"]+)"/); if (match?.[1]) return match[1]; } catch { /* the generated artifact validation below will report the missing identity */ } return undefined; } function validateArtifactIdentity(artifact, { requireDatabaseId = false } = {}) { const { config, configPath } = artifact; if (installationLane) { const database = config.d1_databases?.find((binding) => binding.binding === 'AUTH_DB'); installationApi.assertInstallationDeployment(installationLane, projectRoot, { workerName: config.name, authDatabaseName: database?.database_name, authDatabaseId: database?.database_id, }); } const failures = []; const targetTuple = readCloudflareDeploymentTargetTuple(config); const expectedName = expectedWorkerName(targetTuple); if (typeof config.name !== 'string' || !config.name.trim()) { failures.push('a generated Worker name'); } else if (expectedName && config.name !== expectedName) { failures.push(`Worker identity ${expectedName} (found ${config.name})`); } if ( typeof config.topLevelName === 'string' && config.topLevelName !== config.name ) { failures.push(`one Worker identity (name=${config.name}, topLevelName=${config.topLevelName})`); } const main = typeof config.main === 'string' ? path.resolve(path.dirname(configPath), config.main) : undefined; if (!main || !existsSync(main)) failures.push('a real generated Worker entry'); const authDatabases = (config.d1_databases ?? []).filter( (binding) => binding?.binding === 'AUTH_DB', ); if (authDatabases.length !== 1) { failures.push('exactly one AUTH_DB binding'); } else { const authDb = authDatabases[0]; if (typeof authDb.database_name !== 'string' || !authDb.database_name.trim()) { failures.push('an AUTH_DB database name'); } if (!String(authDb.migrations_dir ?? '').endsWith('migrations/better-auth')) { failures.push('AUTH_DB with reviewed Better Auth migrations'); } if ( requireDatabaseId && (typeof authDb.database_id !== 'string' || !authDb.database_id.trim()) ) { failures.push('the resolved AUTH_DB database identity'); } } if (failures.length) { throw new Error(`Cloudflare deployment identity preflight failed; missing or unsafe ${failures.join(', ')}.`); } } function validateDeploymentProfile(artifact) { let actualProfile; try { actualProfile = classifyCloudflareDeploymentProfile(artifact.config); } catch (error) { throw new Error( `Cloudflare ${deploymentProfile} profile preflight failed: ` + (error instanceof Error ? error.message : String(error)), ); } if (actualProfile !== deploymentProfile) { throw new Error( `Cloudflare deployment profile mismatch: requested ${deploymentProfile}, generated ${actualProfile}.`, ); } } function validateFlue2CutoverArtifact(artifact) { const { config, bundle } = artifact; const failures = []; const migrations = config.migrations ?? []; const deleted = migrations.flatMap((migration) => migration.deleted_classes ?? []); const renamed = renamedClassNames(migrations); const destructive = sortedUnique([...deleted, ...renamed]); const unexpected = destructive.filter((name) => !EXPECTED_DELETED_CLASSES.includes(name)); if (unexpected.length) failures.push(`unexpected deleted/renamed classes: ${unexpected.join(', ')}`); const protectedDestruction = destructive.filter((name) => PROTECTED_CLASSES.has(name)); if (protectedDestruction.length) { failures.push(`protected classes marked deleted/renamed: ${protectedDestruction.join(', ')}`); } if (!sameMembers(deleted, EXPECTED_DELETED_CLASSES) || renamed.length > 0) { failures.push('the exact beta and retired AuthGuard deletion set with no class renames'); } const reset = migrations.find((migration) => migration.tag === 'v6'); if (!reset || !sameMembers(reset.new_sqlite_classes ?? [], V2_AGENT_CLASSES)) { failures.push('v6 fresh Flue 2 SQLite agent classes'); } const sandboxMigration = migrations.find((migration) => migration.tag === 'v3'); if (!sandboxMigration || !sameMembers(sandboxMigration.new_sqlite_classes ?? [], ['Sandbox'])) { failures.push('v3 Sandbox SQLite class'); } const authGuardMigration = migrations.find((migration) => migration.tag === 'v7'); if (!authGuardMigration || !sameMembers(authGuardMigration.new_sqlite_classes ?? [], RETIRED_AUTH_CLASSES)) { failures.push('v7 AuthGuard SQLite class history'); } const authGuardRetirement = migrations.find((migration) => migration.tag === 'v8'); if (!authGuardRetirement || !sameMembers(authGuardRetirement.deleted_classes ?? [], RETIRED_AUTH_CLASSES)) { failures.push('v8 AuthGuard retirement'); } const gatewaySessionMigration = migrations.find((migration) => migration.tag === 'v9'); if (!gatewaySessionMigration || !sameMembers(gatewaySessionMigration.new_sqlite_classes ?? [], ['SlackGatewaySession'])) { failures.push('v9 SlackGatewaySession SQLite class'); } const threadRunnerMigration = migrations.find((migration) => migration.tag === 'v11'); if (!threadRunnerMigration || !sameMembers(threadRunnerMigration.new_sqlite_classes ?? [], ['SlackThreadRunner'])) { failures.push('v11 SlackThreadRunner SQLite class'); } const bindings = config.durable_objects?.bindings ?? []; const hasBinding = (name, className) => bindings.some( (binding) => binding.name === name && binding.class_name === className, ); if (!hasBinding('TAG_STATE', 'TagStateStore')) failures.push('TAG_STATE/TagStateStore binding'); if (!hasBinding('SLACK_THREAD_RUNNER', 'SlackThreadRunner')) { failures.push('SLACK_THREAD_RUNNER/SlackThreadRunner binding'); } for (const [name, className] of V2_AGENT_BINDINGS) { if (!hasBinding(name, className)) failures.push(`${name}/${className} binding`); } const betaBindings = bindings.filter((binding) => BETA_FLUE_CLASSES.includes(binding.class_name)); if (betaBindings.length) failures.push('no beta Flue Durable Object bindings'); if ((config.workflows ?? []).length !== 0) failures.push('no Flue workflow bindings'); const authDb = (config.d1_databases ?? []).find((binding) => binding.binding === 'AUTH_DB'); if (!authDb || !String(authDb.migrations_dir ?? '').endsWith('migrations/better-auth')) { failures.push('AUTH_DB with reviewed Better Auth migrations'); } if (config.observability?.traces?.enabled !== true) { failures.push('enabled Workers Traces for metadata-only Flue spans'); } if (!bundle.includes('@flue/runtime/cloudflare-tracing')) { failures.push('explicit content-free Cloudflare tracing instrumentation'); } if (!bundle.includes('FLUE_PRIVATE_SANDBOX_COMMAND_V1')) { failures.push('content-free Cloudflare Sandbox exec logging'); } if (!bundle.includes('chickpea.response-metadata')) { failures.push('bounded metadata-only Chickpea instrumentation'); } if ( typeof config.compatibility_date !== 'string' || config.compatibility_date < '2026-04-01' ) { failures.push('compatibility_date at or above 2026-04-01'); } if (!(config.compatibility_flags ?? []).includes('global_fetch_strictly_public')) { failures.push('global_fetch_strictly_public for the shared Slack gateway'); } if (config.version_metadata?.binding !== 'CF_VERSION_METADATA') { failures.push('CF_VERSION_METADATA Worker version binding'); } if (failures.length) { throw new Error(`Flue 2 cutover preflight failed; missing or unsafe ${failures.join(', ')}.`); } } function cliVariable(name) { for (let index = 0; index < deployArgs.length; index += 1) { const argument = deployArgs[index]; const raw = argument === '--var' ? deployArgs[index + 1] : argument.startsWith('--var=') ? argument.slice(6) : undefined; if (typeof raw !== 'string') continue; const separator = raw.search(/[:=]/); if (separator < 1 || raw.slice(0, separator) !== name) continue; return raw.slice(separator + 1); } return undefined; } function validateRoutineArtifact(artifact) { const { config, bundle } = artifact; const failures = []; const crons = config.triggers?.crons ?? []; if (crons.length !== 1 || crons[0] !== '* * * * *') failures.push('one * * * * * heartbeat Cron Trigger'); const bindings = config.durable_objects?.bindings ?? []; if (!bindings.some((binding) => binding.name === 'TAG_STATE' && binding.class_name === 'TagStateStore')) { failures.push('TAG_STATE/TagStateStore binding'); } for (const [name, className] of V2_AGENT_BINDINGS.slice(1)) { if (!bindings.some((binding) => binding.name === name && binding.class_name === className)) { failures.push(`${name}/${className} binding`); } } if (!hasScheduledComposition(artifact.entry)) { failures.push('composed heartbeat and maintenance handlers'); } if ( !bundle.includes('chickpea-routine-intent-v2') || !bundle.includes('chickpea-routine-execution-v2') ) { failures.push('fresh Flue 2 routine agent registrations'); } if (failures.length) { throw new Error( 'Routine scheduling artifact is unsafe; missing ' + failures.join(', ') + '. ' + 'Repair the artifact and verify the heartbeat before deployment.', ); } } function validateLedgerCanaryArtifact(artifact) { const { config, bundle } = artifact; const cliSelector = cliVariable('SLACK_TAG_LEDGER_CANARY_CHANNELS'); const selector = cliSelector ?? config.vars?.SLACK_TAG_LEDGER_CANARY_CHANNELS ?? ''; if (selector === '') return; if (typeof selector !== 'string') { throw new Error('SLACK_TAG_LEDGER_CANARY_CHANNELS must be a string.'); } const entries = selector.split(',').map((entry) => entry.trim()); const exactPair = /^[A-Za-z][A-Za-z0-9_-]{1,63}\/[A-Za-z][A-Za-z0-9_-]{1,63}$/; if (entries.length > 20 || entries.some((entry) => !exactPair.test(entry))) { throw new Error( 'SLACK_TAG_LEDGER_CANARY_CHANNELS is unsafe: use 1-20 exact workspace/channel pairs ' + '(for example T123/C456), comma-separated with no wildcard or empty entry.', ); } const requiredSeams = [ 'SLACK_TAG_LEDGER_CANARY_CHANNELS', 'delivery_receipt_persist_unknown', 'slack_agent_bindings', ]; const missing = requiredSeams.filter((seam) => !bundle.includes(seam)); if (missing.length) { throw new Error( 'SLACK_TAG_LEDGER_CANARY_CHANNELS is unsafe for this artifact; missing durable driver seams: ' + missing.join(', ') + '. Deploy with the selector empty and repair the artifact.', ); } } // Cloudflare turns run in per-thread SlackThreadRunner objects by default; // SLACK_TAG_TURN_EXECUTOR=alarm is the emergency fallback to the state store's // alarm. Accept only the two executors (or unset), and an explicit `runner` // only for an artifact that carries the runner execution path. Unset needs no // check: a Worker without the runner binding keeps turns on the alarm. function validateTurnExecutorArtifact(artifact) { const { config, bundle } = artifact; const value = cliVariable('SLACK_TAG_TURN_EXECUTOR') ?? config.vars?.SLACK_TAG_TURN_EXECUTOR ?? ''; if (value === '' || value === 'alarm') return; if (value !== 'runner') { throw new Error( 'SLACK_TAG_TURN_EXECUTOR must be runner or alarm (the emergency fallback), or unset (runner).', ); } const bindings = config.durable_objects?.bindings ?? []; const missing = ['SLACK_TAG_TURN_EXECUTOR', 'threadRunnerTurn', 'thread_runner_alarm'] .filter((seam) => !bundle.includes(seam)); if (!bindings.some((binding) => binding.name === 'SLACK_THREAD_RUNNER' && binding.class_name === 'SlackThreadRunner')) { missing.push('SLACK_THREAD_RUNNER/SlackThreadRunner binding'); } if (missing.length) { throw new Error( 'SLACK_TAG_TURN_EXECUTOR=runner is unsafe for this artifact; missing thread runner seams: ' + missing.join(', ') + '. Deploy with the variable unset and repair the artifact.', ); } } // A Worker holds nothing for a residency hold window it cannot read, so refuse // one here, where the operator sees why. function validateResidencyHold(artifact) { parseResidencyHoldMs(cliVariable(RESIDENCY_HOLD_VARIABLE) ?? artifact.config.vars?.[RESIDENCY_HOLD_VARIABLE]); } function validateAgentViewArtifact(artifact) { if (!artifact.bundle.includes('agent_view') || !artifact.bundle.includes('agent_description')) { throw new Error( 'Generated Cloudflare artifact is missing the permanent Agent View contract.', ); } } function validateDeploymentArtifact(artifact, options = {}) { validateArtifactIdentity(artifact, options); validateDeploymentProfile(artifact); validateAgentViewArtifact(artifact); validateFlue2CutoverArtifact(artifact); validateRoutineArtifact(artifact); validateLedgerCanaryArtifact(artifact); validateTurnExecutorArtifact(artifact); validateResidencyHold(artifact); return artifact; } let builtArtifact; let deploymentTargetTuple; try { const artifact = requireBuiltArtifact(); if (upgradeContextPath) { const target = validateTarget(upgradeContext.target); if (upgradeContext.schema !== 1 || !target.url || !explicitProductionTarget || selectedEnvironmentTarget || target.account !== process.env.CLOUDFLARE_ACCOUNT_ID || target.worker !== process.env.WRANGLER_CI_OVERRIDE_NAME || target.profile !== resolveCloudflareDeploymentProfile() || JSON.stringify(deployArgs) !== JSON.stringify(wranglerProfileArgs(target)) || artifact.config.vars?.CHICKPEA_APP_VERSION !== upgradeContext.source?.version || artifact.config.vars?.CHICKPEA_SOURCE_COMMIT !== upgradeContext.source?.commit) { throw new Error('Upgrade context does not match the selected target, source, or guarded command.'); } artifact.config = overlayInstallation(artifact.config, upgradeContext.installation, target); writeFileSync(artifact.configPath, `${JSON.stringify(artifact.config, null, 2)}\n`); } builtArtifact = validateDeploymentArtifact(artifact); deploymentTargetTuple = readCloudflareDeploymentTargetTuple(builtArtifact.config); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } if (deploymentTargetTuple) { process.stdout.write(`${formatCloudflareDeploymentTargetTuple(deploymentTargetTuple)}\n`); } if (selectedEnvironmentTarget && deploymentTargetTuple?.target !== selectedEnvironmentTarget) { console.error( `Selected environment ${selectedEnvironmentTarget} does not match the generated target tuple.`, ); process.exit(1); } if (selectedEnvironmentTarget && deploymentTargetTuple?.stateMode !== 'permanent') { console.error( `Claimed environment ${selectedEnvironmentTarget} requires its registered immutable AUTH_DB and schema generation; disposable target mutation is refused.`, ); process.exit(1); } if (preflightOnly) { process.stdout.write('Permanent Cloudflare capability preflight passed. No deployment was attempted.\n'); process.exit(0); } try { if (checkAccount) { if (checkedAccount) assertCloudflareAccountConfig(accountCheckOptions(builtArtifact.configPath), checkedAccount); else checkedAccount = await checkDeploymentAccount(builtArtifact.configPath); } } catch (error) { console.error(error.message); process.exit(1); } let prebuiltSandboxImage; if (guardedSandboxDeploy) { try { prebuiltSandboxImage = await prebuildSandboxImage({ ...sandboxToolOptions(builtArtifact.configPath), applicationName: sandboxApplicationName(builtArtifact.config), }); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } const AUTH_SECRET = 'CHICKPEA_AUTH_SECRET'; const CREDENTIAL_CURRENT_KEY = 'CHICKPEA_CREDENTIAL_KEY_CURRENT_ID'; const CREDENTIAL_KEY_PREFIX = 'CHICKPEA_CREDENTIAL_KEY_'; const INITIAL_CREDENTIAL_KEY_ID = 'key_v1'; const INITIAL_CREDENTIAL_KEY_SLOT = `${CREDENTIAL_KEY_PREFIX}KEY_V1`; const DEFAULT_ACTIVE_WORKER_INSPECTION_TIMEOUT_MS = 30_000; const configuredInspectionTimeout = Number( process.env.CHICKPEA_DEPLOY_INSPECTION_TIMEOUT_MS, ); const activeWorkerInspectionTimeoutMs = Number.isFinite(configuredInspectionTimeout) && configuredInspectionTimeout > 0 ? Math.min(configuredInspectionTimeout, DEFAULT_ACTIVE_WORKER_INSPECTION_TIMEOUT_MS) : DEFAULT_ACTIVE_WORKER_INSPECTION_TIMEOUT_MS; function inspector(artifact) { return wranglerInspector({ root: runnerRoot, configPath: artifact.configPath, args: deploymentResourceArgs(), timeout: activeWorkerInspectionTimeoutMs }); } function inspectRemoteWorker(artifact) { return inspector(artifact).worker(); } function activeDeployment(artifact) { try { return inspector(artifact).active(); } catch { throw new Error('Unable to inspect the active Worker deployment. Refusing an update without its current AUTH_DB binding.'); } } function assertActiveDeploymentUnchanged(artifact, expectedFingerprint) { const currentFingerprint = deploymentFingerprint(activeDeployment(artifact)); if (currentFingerprint === expectedFingerprint) return; throw new Error( 'The active Worker deployment changed while this deploy was preparing. ' + 'Another task is using the same deployment target; refusing to replace its canary. ' + 'Coordinate the live acceptance run, then rebuild and deploy again.', ); } function deployedAuthDatabase(artifact) { const versions = activeDeployment(artifact); const databaseIds = []; const setupAuthorities = []; let versionsWithoutAuthDatabase = 0; let sandboxBound = false; for (const version of versions) { let details; try { details = inspector(artifact).version(version.version_id); } catch { throw new Error(`Unable to inspect active Worker version ${version.version_id}. Refusing an update without its current AUTH_DB binding.`); } const allBindings = details?.resources?.bindings ?? []; if (allBindings.some((binding) => binding?.type === 'durable_object_namespace' && (binding.name === 'SANDBOX' || binding.class_name === 'Sandbox'))) { sandboxBound = true; } const bindings = allBindings.filter( (binding) => binding?.name === 'AUTH_DB' && binding?.type === 'd1', ); const setupDigestBindings = allBindings.filter((binding) => binding?.name === SETUP_CAPABILITY_DIGEST_BINDING && typeof (binding.text ?? binding.value) === 'string' ); const setupIssuedBindings = allBindings.filter((binding) => binding?.name === SETUP_CAPABILITY_ISSUED_AT_BINDING && typeof (binding.text ?? binding.value) === 'string' ); if (selectedEnvironmentTarget || upgradeContext) { if (setupDigestBindings.length !== 1 || setupIssuedBindings.length !== 1) { throw new Error('Claimed environment active versions must expose one preserved setup authority.'); } const digest = setupDigestBindings[0].text ?? setupDigestBindings[0].value; const issuedAt = setupIssuedBindings[0].text ?? setupIssuedBindings[0].value; if (!/^[A-Za-z0-9_-]{43}$/u.test(digest) || !/^\d{13}$/u.test(issuedAt)) { throw new Error('Claimed environment setup authority is unreadable; refusing rotation.'); } setupAuthorities.push(`${digest}:${issuedAt}`); } if (bindings.length > 1) { throw new Error(`Active Worker version ${version.version_id} has duplicate AUTH_DB bindings.`); } if (bindings.length === 0) { versionsWithoutAuthDatabase += 1; continue; } const databaseId = bindings[0].database_id ?? bindings[0].id; if (typeof databaseId !== 'string' || !databaseId.trim()) { throw new Error(`Active Worker version ${version.version_id} has an unreadable AUTH_DB binding.`); } databaseIds.push(databaseId.trim()); } if (versionsWithoutAuthDatabase > 0 && databaseIds.length > 0) { throw new Error('Active Worker versions disagree about whether AUTH_DB is bound; refusing to guess.'); } const uniqueIds = [...new Set(databaseIds)]; if (uniqueIds.length > 1) { throw new Error('Active Worker versions use different AUTH_DB databases; refusing to update either one.'); } const uniqueSetupAuthorities = [...new Set(setupAuthorities)]; if ((selectedEnvironmentTarget || upgradeContext) && uniqueSetupAuthorities.length !== 1) { throw new Error('Active Worker versions disagree about setup authority; refusing rotation.'); } const [setupDigest, setupIssuedAt] = uniqueSetupAuthorities[0]?.split(':') ?? []; return { databaseId: uniqueIds[0], fingerprint: deploymentFingerprint(versions), sandboxBound, servingVersionId: versions.length === 1 ? versions[0].version_id : undefined, ...((selectedEnvironmentTarget || upgradeContext) ? { setupAuthority: { digest: setupDigest, issuedAt: Number(setupIssuedAt) }, } : {}), }; } async function prepareDeploymentAuthority(artifact, secretNames, preservedSetupAuthority) { const generatedSecrets = {}; if (!secretNames.has(AUTH_SECRET)) { if (selectedEnvironmentTarget || upgradeContext) { throw new Error('Existing installation is missing its permanent auth authority; refusing rotation.'); } generatedSecrets[AUTH_SECRET] = (await mintSetupCapability()).capability; } const hasCredentialCurrent = secretNames.has(CREDENTIAL_CURRENT_KEY); const credentialSlots = [...secretNames].filter((name) => name.startsWith(CREDENTIAL_KEY_PREFIX) && name !== CREDENTIAL_CURRENT_KEY ); if (!hasCredentialCurrent && credentialSlots.length === 0) { if (selectedEnvironmentTarget || upgradeContext) { throw new Error( 'Existing installation credential encryption authority is missing; refusing to rotate encrypted connections.', ); } generatedSecrets[CREDENTIAL_CURRENT_KEY] = INITIAL_CREDENTIAL_KEY_ID; generatedSecrets[INITIAL_CREDENTIAL_KEY_SLOT] = randomBytes(32).toString('base64url'); } else if (!hasCredentialCurrent || credentialSlots.length === 0) { throw new Error( 'Cloudflare Slack credential encryption is only partially provisioned. ' + 'Restore CHICKPEA_CREDENTIAL_KEY_CURRENT_ID and its versioned ' + 'CHICKPEA_CREDENTIAL_KEY_ slot before deploying.', ); } const setup = preservedSetupAuthority ?? await mintSetupCapability(); artifact.config.vars = { ...(artifact.config.vars ?? {}), [SETUP_CAPABILITY_DIGEST_BINDING]: setup.digest, [SETUP_CAPABILITY_ISSUED_AT_BINDING]: String(setup.issuedAt), }; writeFileSync(artifact.configPath, `${JSON.stringify(artifact.config, null, 2)}\n`); return { generatedSecrets, setup }; } async function bindDeploymentActivation(artifact) { const activation = await mintDeploymentActivation(); artifact.config.vars = { ...(artifact.config.vars ?? {}), [DEPLOYMENT_ACTIVATION_DIGEST_BINDING]: activation.digest, [DEPLOYMENT_ACTIVATION_ISSUED_AT_BINDING]: String(activation.issuedAt), }; writeFileSync(artifact.configPath, `${JSON.stringify(artifact.config, null, 2)}\n`); return activation; } function createSecretsFile(secrets) { if (Object.keys(secrets).length === 0) return undefined; const directory = mkdtempSync(path.join(tmpdir(), 'chickpea-deploy-secrets-')); const file = path.join(directory, 'secrets.json'); try { writeFileSync(file, `${JSON.stringify(secrets)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' }); } catch (error) { rmSync(directory, { recursive: true, force: true }); throw error; } return { directory, file }; } function removeSecretsFile(prepared) { if (prepared) rmSync(prepared.directory, { recursive: true, force: true }); } function deploymentResourceArgs() { const resolved = new Map(); for (let index = 0; index < deployArgs.length; index += 1) { const argument = deployArgs[index]; if (['--env', '-e', '--profile'].includes(argument)) { const flag = argument === '-e' ? '--env' : argument; const value = deployArgs[index + 1]; if (!value || value.startsWith('--') || resolved.has(flag)) { throw new Error(`Claimed deployment has ambiguous ${flag} provider context.`); } resolved.set(flag, value); index += 1; } else if (argument.startsWith('--env=') || argument.startsWith('--profile=')) { const separator = argument.indexOf('='); const flag = argument.slice(0, separator); const value = argument.slice(separator + 1); if (!value || resolved.has(flag)) { throw new Error(`Claimed deployment has ambiguous ${flag} provider context.`); } resolved.set(flag, value); } } return ['--profile', '--env'].flatMap((flag) => resolved.has(flag) ? [flag, resolved.get(flag)] : [] ); } function ensureAuthDatabase(artifact, deployedDatabaseId) { const authDb = (artifact.config.d1_databases ?? []).find( (binding) => binding.binding === 'AUTH_DB', ); const targetTuple = readCloudflareDeploymentTargetTuple(artifact.config); if (deployedDatabaseId) { const generatedId = typeof authDb?.database_id === 'string' ? authDb.database_id.trim() : ''; if (generatedId && generatedId !== deployedDatabaseId) { throw new Error( `The generated AUTH_DB database (${generatedId}) differs from the deployed database ` + `(${deployedDatabaseId}); refusing to replace customer auth data.`, ); } process.stdout.write('Preserving the deployed AUTH_DB database...\n'); authDb.database_id = deployedDatabaseId; writeFileSync(artifact.configPath, `${JSON.stringify(artifact.config, null, 2)}\n`); return validateDeploymentArtifact(requireBuiltArtifact(), { requireDatabaseId: true }); } if (typeof authDb?.database_id === 'string' && authDb.database_id.trim()) return artifact; const existingId = existingAuthDatabaseId(authDb?.database_name || 'chickpea-auth-db'); if (existingId) { if (targetTuple?.stateMode === 'disposable') { throw new Error( `Cloudflare disposable target ${targetTuple.target} already has an existing AUTH_DB. ` + 'Remove that disposable state or register its immutable ID and schema generation before deploying.', ); } process.stdout.write('Reusing the customer-owned AUTH_DB database...\n'); authDb.database_id = existingId; writeFileSync(artifact.configPath, `${JSON.stringify(artifact.config, null, 2)}\n`); return validateDeploymentArtifact(requireBuiltArtifact(), { requireDatabaseId: true }); } const rootConfig = path.join(projectRoot, 'wrangler.jsonc'); const provisionConfig = targetTuple ? artifact.configPath : rootConfig; process.stdout.write('Provisioning the customer-owned AUTH_DB database...\n'); const provision = spawnSync( process.execPath, [ wranglerBin, 'd1', 'create', authDb?.database_name || 'chickpea-auth-db', '--binding', 'AUTH_DB', '--update-config', '--config', provisionConfig, ...deploymentResourceArgs(), ], { cwd: projectRoot, stdio: 'inherit' }, ); if (provision.error) { console.error(`Unable to start AUTH_DB provisioning: ${provision.error.message}`); process.exit(1); } if (provision.status !== 0) { console.error( 'AUTH_DB provisioning failed. If the database already exists, copy its ID into ' + 'wrangler.jsonc and rerun npm run deploy.', ); process.exit(provision.status ?? 1); } if (targetTuple) { return validateDeploymentArtifact(requireBuiltArtifact(), { requireDatabaseId: true }); } buildCloudflareArtifact(); const rebuilt = requireBuiltArtifact(); return validateDeploymentArtifact(rebuilt, { requireDatabaseId: true }); } function existingAuthDatabaseId(databaseName) { const list = spawnSync( process.execPath, [wranglerBin, 'd1', 'list', '--json', ...deploymentResourceArgs()], { cwd: projectRoot, encoding: 'utf8', stdio: ['inherit', 'pipe', 'inherit'] }, ); if (list.error) { console.error(`Unable to inspect AUTH_DB resources: ${list.error.message}`); process.exit(1); } if (list.status !== 0) process.exit(list.status ?? 1); let databases; try { databases = JSON.parse(list.stdout); } catch { console.error('AUTH_DB discovery returned an unreadable response.'); process.exit(1); } if (!Array.isArray(databases)) { console.error('AUTH_DB discovery returned an unexpected response.'); process.exit(1); } const matches = databases.filter((database) => database?.name === databaseName); if (matches.length > 1) { console.error(`Multiple D1 databases are named ${databaseName}; refusing to guess.`); process.exit(1); } const id = matches[0]?.uuid ?? matches[0]?.id; return typeof id === 'string' && id.trim() ? id.trim() : undefined; } function verifyRemoteAuthSchema(artifact) { const expected = expectedAuthSchema(artifact); const inspection = spawnSync( process.execPath, [ wranglerBin, 'd1', 'execute', 'AUTH_DB', '--remote', '--json', '--command', AUTH_SCHEMA_QUERY, '--config', artifact.configPath, ...deploymentResourceArgs(), ], { cwd: projectRoot, encoding: 'utf8' }, ); if (inspection.error || inspection.status !== 0) { throw new Error( 'Unable to inspect the migrated AUTH_DB schema. Refusing to upload the Worker.', ); } let payload; try { payload = JSON.parse(inspection.stdout); } catch { throw new Error('AUTH_DB schema inspection returned unreadable JSON.'); } const statements = Array.isArray(payload) ? payload : []; const resultSets = statements.filter((statement) => Array.isArray(statement?.results)); if (resultSets.length !== 1 || resultSets[0]?.success === false) { throw new Error('AUTH_DB schema inspection returned an unexpected result.'); } const actual = normalizeAuthSchemaRows(resultSets[0].results); if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error( 'AUTH_DB contains an incompatible reviewed Better Auth migration-chain schema. ' + 'Its schema structure differs from the reviewed migration chain. Preserve the database and investigate the mismatch before deploying.', ); } process.stdout.write('Verified the reviewed Better Auth schema in AUTH_DB...\n'); } let deploymentAuthority; let remoteWorker; let expectedActiveDeploymentFingerprint; let previousServingVersionId; let finalEnvironmentPreflight; if (!deployArgs.includes('--dry-run')) { try { if (qaSourceAdmission) qaCandidateApi.recheckQaCandidate(qaSourceAdmission); if (upgradeContext) { const current = validateInstallation(inspector(builtArtifact).inspect()); assertSameInstallation(upgradeContext.installation, current); verifyRemoteAuthSchema(builtArtifact); } remoteWorker = inspectRemoteWorker(builtArtifact); if (installationLane && remoteWorker.exists) { throw new Error('The reserved fresh-install Worker already exists. Preserve it for recovery; a fresh install must not overwrite existing Worker state.'); } if (selectedEnvironmentTarget && !remoteWorker.exists) { throw new Error( `Claimed environment ${selectedEnvironmentTarget} has no existing Worker; refusing to provision permanent lane infrastructure.`, ); } if (deploymentTargetTuple?.stateMode === 'disposable' && remoteWorker.exists) { throw new Error( `Cloudflare disposable target ${deploymentTargetTuple.target} already has an existing Worker ` + 'and Durable Object state. Remove that disposable state or register its immutable AUTH_DB ID ' + 'and schema generation before deploying.', ); } const deployed = remoteWorker.exists ? deployedAuthDatabase(builtArtifact) : undefined; if (upgradeContext && (!remoteWorker.exists || !deployed?.databaseId)) { throw new Error('Upgrade requires the existing Worker and AUTH_DB; provisioning is forbidden.'); } expectedActiveDeploymentFingerprint = deployed?.fingerprint; previousServingVersionId = deployed?.servingVersionId; if (deployed?.sandboxBound && deploymentProfile === 'core' && !explicitCoreProfile) { throw new Error( 'The live Worker has the coding sandbox (SANDBOX binding), and this command would deploy the core profile ' + 'and remove it. To keep the sandbox, run the same command as `npm run deploy:sandbox` with the same flags and ' + 'environment (for Workers Builds, keep the CHICKPEA_DEPLOY_PROFILE=sandbox build variable). To uninstall it ' + 'on purpose, set CHICKPEA_DEPLOY_PROFILE=core and rerun. Nothing was changed.', ); } builtArtifact = ensureAuthDatabase(builtArtifact, deployed?.databaseId); // This is the final gate immediately before the first mutation of an // existing customer resource. It intentionally reruns after both D1 reuse // injection and D1 provisioning/rebuild. builtArtifact = validateDeploymentArtifact(builtArtifact, { requireDatabaseId: true }); deploymentAuthority = await prepareDeploymentAuthority( builtArtifact, remoteWorker.names, deployed?.setupAuthority, ); if (selectedEnvironmentTarget) { const environmentOptions = { projectRoot, config: builtArtifact.config, configPath: builtArtifact.configPath, targetTuple: deploymentTargetTuple, providerContext: deploymentResourceArgs(), }; if (resumedEnvironmentDeployment) { const finalResume = await environmentPreflightApi.recheckResumedEnvironmentDeployment( resumedEnvironmentDeployment, environmentOptions, ); finalEnvironmentPreflight = finalResume.preflight; environmentMutationLease = finalResume.mutationLease; } else { finalEnvironmentPreflight = await environmentPreflightApi.preflightEnvironmentMutation( selectedEnvironmentTarget, environmentOptions, ); } environmentPreflightApi.assertSameEnvironmentMutationAuthority( initialEnvironmentPreflight, finalEnvironmentPreflight, ); builtArtifact.config.vars = { ...(builtArtifact.config.vars ?? {}), // The registry owns this non-secret, project-scoped baseline setting. // Keep the reviewed standard grant across branch deployments. The // smoke contract exercises reads; it does not narrow OAuth consent. COMPOSIO_SHEETS_READ_AUTH_CONFIG_ID: finalEnvironmentPreflight.registration.providerAuthConfigId, COMPOSIO_SHEETS_WRITE_AUTH_CONFIG_ID: finalEnvironmentPreflight.registration.providerAuthConfigId, ...environmentPreflightApi.environmentDeploymentMetadataBindings( finalEnvironmentPreflight.deploymentMetadata, ), }; writeFileSync( builtArtifact.configPath, `${JSON.stringify(builtArtifact.config, null, 2)}\n`, ); builtArtifact = validateDeploymentArtifact(requireBuiltArtifact(), { requireDatabaseId: true, }); } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } if (selectedEnvironmentTarget) { try { if (qaSourceAdmission) qaCandidateApi.recheckQaCandidate(qaSourceAdmission); environmentMutationLease ??= environmentPreflightApi.beginEnvironmentDeployment( finalEnvironmentPreflight, { projectRoot, providerContext: deploymentResourceArgs() }, ); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } // D1 migrations are forward-only and idempotent. Apply them before the Worker // starts serving a schema it expects. If deploy later fails, rerunning this // command resumes from D1's migration ledger; never attempt schema rollback. if (!deployArgs.includes('--dry-run') && !upgradeContext) { process.stdout.write('Applying reviewed Better Auth migrations to AUTH_DB...\n'); const environmentArgs = deploymentResourceArgs(); // Authority preparation above can await provider reads. Fence the actual // migration inputs again at the irreversible D1 boundary, including resumes. try { if (qaSourceAdmission) qaCandidateApi.recheckQaCandidate(qaSourceAdmission); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } const migration = spawnSync( process.execPath, [ wranglerBin, 'd1', 'migrations', 'apply', 'AUTH_DB', '--remote', '--config', builtArtifact.configPath, ...environmentArgs, ], { cwd: projectRoot, stdio: 'inherit' }, ); if (migration.error) { console.error(`Unable to start AUTH_DB migration: ${migration.error.message}`); process.exit(1); } if (migration.status !== 0) process.exit(migration.status ?? 1); try { verifyRemoteAuthSchema(builtArtifact); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } let preparedSecrets; try { if (deploymentAuthority) { deploymentAuthority.activation = await bindDeploymentActivation(builtArtifact); } // Operator-supplied secrets (for example a QA lane's provider key) ride the // same atomic secrets file, so one deploy yields one live version and the // receipt keeps matching the Worker. Names are logged; values never are. const operatorSecrets = process.env[OPERATOR_SECRETS_ENV] ? readOperatorSecretsFile(process.env[OPERATOR_SECRETS_ENV]) : undefined; if (operatorSecrets) { console.log(`Deploying with operator secrets: ${Object.keys(operatorSecrets).join(', ')}`); } // A claimed lane also carries the standing provider keys from the // operator's lane secrets file, so a rebuilt lane regains them. An explicit // operator secrets file wins over it, and the wrapper's own names win over both. const laneSecrets = resolveLaneSecrets(requestedDeploymentTarget); if (laneSecrets) console.log(describeLaneSecrets(laneSecrets)); // Each lane also keeps one seed token for `npm run lane:seed`. const laneSeedToken = ensureLaneSeedToken(requestedDeploymentTarget); if (laneSeedToken) console.log(`Lane seed token: installed (${LANE_SEED_TOKEN_BINDING}).`); preparedSecrets = createSecretsFile(mergeDeploymentSecrets( { ...(deploymentAuthority?.generatedSecrets ?? {}), ...(laneSeedToken ? { [LANE_SEED_TOKEN_BINDING]: laneSeedToken } : {}), }, { ...(laneSecrets?.secrets ?? {}), ...(operatorSecrets ?? {}) }, )); } catch (error) { console.error(`Unable to prepare the temporary Worker secrets file: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } // The inspection above intentionally happens after the build, when this // deploy knows the exact target and preserved bindings. A second task may // still finish its own deploy while migrations/readiness preparation runs. // Re-read the serving versions at the last safe boundary so overlapping // disposable acceptance runs fail closed instead of silently replacing one // another's Worker and interrupting their admitted Slack turns. if (expectedActiveDeploymentFingerprint) { try { assertActiveDeploymentUnchanged(builtArtifact, expectedActiveDeploymentFingerprint); } catch (error) { removeSecretsFile(preparedSecrets); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } if (selectedEnvironmentTarget) { try { environmentPreflightApi.recheckEnvironmentMutationAuthority( finalEnvironmentPreflight, { projectRoot, config: builtArtifact.config, configPath: builtArtifact.configPath, targetTuple: deploymentTargetTuple, providerContext: deploymentResourceArgs(), mutationLease: environmentMutationLease, }, ); } catch (error) { removeSecretsFile(preparedSecrets); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } if (upgradeContext) { try { assertSameInstallation(upgradeContext.installation, validateInstallation(inspector(builtArtifact).inspect())); verifyRemoteAuthSchema(builtArtifact); writeDeploymentEvent(upgradeContextPath, 'deploying'); } catch (error) { removeSecretsFile(preparedSecrets); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } } try { if (qaSourceAdmission) qaCandidateApi.recheckQaCandidate(qaSourceAdmission); if (prebuiltSandboxImage || sandboxCheckpointsOff) { // Last write before the upload: Wrangler now applies the Container // application from the already-pushed image instead of building it after // the new Worker version is live. if (prebuiltSandboxImage) useSandboxImage(builtArtifact.config, prebuiltSandboxImage); if (sandboxCheckpointsOff) withoutCheckpointBucket(builtArtifact.config); writeFileSync(builtArtifact.configPath, `${JSON.stringify(builtArtifact.config, null, 2)}\n`); } } catch (error) { removeSecretsFile(preparedSecrets); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } const child = spawn( process.execPath, [ wranglerBin, ...(upgradeContext ? ['versions', 'upload'] : ['deploy']), ...deployArgs, ...(preparedSecrets ? ['--secrets-file', preparedSecrets.file] : []), ], { cwd: projectRoot, stdio: ['inherit', 'pipe', 'inherit'] }, ); let activeChild = child; let cleanedSecrets = false; function cleanupSecrets() { if (cleanedSecrets) return; cleanedSecrets = true; removeSecretsFile(preparedSecrets); } process.once('exit', cleanupSecrets); for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) { process.once(signal, () => { cleanupSecrets(); activeChild.kill(signal); process.exit(signal === 'SIGINT' ? 130 : 143); }); } let deployedUrl = ''; let deployedVersionId = ''; let workerUploaded = false; let tail = ''; child.stdout.on('data', (chunk) => { process.stdout.write(chunk); // Line-oriented scan without unbounded buffering: keep only a joining tail // in case the URL straddles a chunk boundary. const text = tail + chunk.toString('utf8'); const match = text.match(/https?:\/\/[^\s]+\.workers\.dev\b/); if (match && !deployedUrl) { deployedUrl = match[0]; } if (!workerUploaded && uploadedBeforeFailure(text)) workerUploaded = true; const versionMatch = text.match(/(?:Current|Worker) Version ID:\s*([A-Za-z0-9-]+)/); if (versionMatch?.[1] && versionMatch[1] !== deployedVersionId) { deployedVersionId = versionMatch[1]; if (upgradeContext) writeDeploymentEvent(upgradeContextPath, 'uploaded', deployedVersionId); } tail = text.slice(-256); }); const DEFAULT_DEPLOYMENT_READINESS_TIMEOUT_MS = 6 * 60 * 1_000; const MAX_DEPLOYMENT_READINESS_TIMEOUT_MS = 10 * 60 * 1_000; const configuredReadinessTimeout = Number(process.env.CHICKPEA_DEPLOY_READINESS_TIMEOUT_MS); const deploymentReadinessTimeoutMs = Number.isFinite(configuredReadinessTimeout) && configuredReadinessTimeout > 0 ? Math.min(configuredReadinessTimeout, MAX_DEPLOYMENT_READINESS_TIMEOUT_MS) : DEFAULT_DEPLOYMENT_READINESS_TIMEOUT_MS; // Test hook: comma-separated `status` or `status:error` readiness responses. const testReadinessStatuses = process.env.DEPLOY_TEST_READINESS_STATUSES ?.split(',') .map((value) => { const [status, error] = value.trim().split(':'); return { status: Number(status), error: error || undefined }; }) .filter(({ status }) => Number.isInteger(status)); let testReadinessIndex = 0; const READINESS_WAIT_REASONS = { worker_version_pending: 'the new Worker version is not serving every request yet', gateway_session_stale_version: "the Slack gateway session still runs the previous Worker version", gateway_session_offline: "this version's Slack gateway session is not connected yet", gateway_session_unconfirmed: "the Slack gateway did not answer on this version's session; reconnecting it", gateway_upgrade_pending: 'Slack gateway HTTP delivery is still being registered', }; function readinessWaitReason(result) { if (result.status === 0) return 'the Worker did not answer the readiness check'; // The previous version does not know this deploy's activation capability. if (result.status === 404) return READINESS_WAIT_REASONS.worker_version_pending; return READINESS_WAIT_REASONS[result.error] ?? `the readiness check returned HTTP ${result.status}${result.error ? ` (${result.error})` : ''}`; } function readinessBaseUrl() { if (upgradeContext) return upgradeContext.target.url; if (deployedUrl) return deployedUrl; if (process.env.DEPLOY_TEST_READINESS_BASE_URL) { return process.env.DEPLOY_TEST_READINESS_BASE_URL; } const configured = builtArtifact.config.vars?.SLACK_TAG_PUBLIC_URL; return typeof configured === 'string' && configured.trim() ? configured.trim().replace(/\/+$/, '') : undefined; } async function requestDeploymentReadiness(baseUrl, versionId, activation) { if (testReadinessStatuses?.length) { const result = testReadinessStatuses[ Math.min(testReadinessIndex, testReadinessStatuses.length - 1) ]; testReadinessIndex += 1; return result; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); try { const response = await fetch(new URL('/internal/deployment/ready', baseUrl), { method: 'POST', headers: { Authorization: `Bearer ${activation.capability}`, 'X-Chickpea-Target-Version': versionId, ...(upgradeContext?.recovery?.digest ? { 'X-Chickpea-Recovery-Digest': upgradeContext.recovery.digest } : {}), }, redirect: 'manual', signal: controller.signal, }); let error; if (response.status !== 204) { try { const body = await response.json(); if (typeof body?.error === 'string' && /^[a-z0-9_]{1,64}$/.test(body.error)) error = body.error; } catch { // A body that is not the readiness JSON only loses the reason. } } return { status: response.status, error }; } catch { return { status: 0 }; } finally { clearTimeout(timeout); } } // Readiness requires the new version to serve, its Slack gateway session to run // that version, and the gateway to answer on that session. Local session health // alone can trust a socket the gateway has dropped, which delays the next Slack // event until Slack retries it about a minute later. async function waitForDeploymentReadiness(baseUrl, versionId, activation) { const deadline = Date.now() + deploymentReadinessTimeoutMs; process.stdout.write('Waiting for the current Worker and Slack gateway version...\n'); let lastReason; while (Date.now() < deadline) { const result = await requestDeploymentReadiness(baseUrl, versionId, activation); if (result.status === 204) { process.stdout.write('Verified current-version deployment readiness.\n'); return; } const reason = readinessWaitReason(result); if (reason !== lastReason) process.stdout.write(` Still waiting: ${reason}.\n`); lastReason = reason; if (testReadinessStatuses?.length && testReadinessIndex >= testReadinessStatuses.length) break; if (!testReadinessStatuses?.length) { await new Promise((resolve) => setTimeout(resolve, 2_000)); } } throw new Error( 'Worker uploaded, but current-version readiness was not confirmed within ' + `${Math.round(deploymentReadinessTimeoutMs / 1_000)} s. Last status: ${lastReason ?? 'no answer'}. ` + 'Slack events can reach this Worker late or not at all until its gateway session connects; ' + 'rerun the deploy or check Admin → Settings → Slack.', ); } const RULE = '────────────────────────────────────────────────────────'; function printPrivateSetupLink(baseUrl, setup) { const privateSetupUrl = setupCapabilityUrl(baseUrl, setup.capability); process.stdout.write( [ '', RULE, ' ✔ Worker deployed.', '', ' 🔐 PRIVATE SETUP LINK — COPY AND OPEN THIS', RULE, `👉 ${privateSetupUrl}`, ].join('\n'), ); } function printPrivateSetupPath(setup) { const privatePath = new URL(setupCapabilityUrl('https://chickpea.invalid', setup.capability)); process.stdout.write( [ '', RULE, ' ✔ Worker deployed.', '', ' 🔐 PRIVATE SETUP PATH — copy and open this on your configured Chickpea domain', RULE, `👉 ${privatePath.pathname}${privatePath.hash}`, ].join('\n'), ); } // Workers Builds receives stdout through a pipe. Let Node exit naturally after // this callback so the final setup link and Cloudflare's completion event can // flush; process.exit() can truncate asynchronous pipe writes. function sandboxRerun() { return rerunCommand({ explicitProductionTarget, deployArgs, workersBuilds: process.env.WORKERS_CI === '1' }); } function sandboxRecovery() { return sandboxPartialDeployRecovery({ workerName: builtArtifact.config.name, previousVersionId: previousServingVersionId, providerContext: deploymentResourceArgs(), rerun: sandboxRerun(), }); } child.on('close', async (code) => { cleanupSecrets(); if (code !== 0) { // Wrangler activates the uploaded version before it creates the Container // application, so a failure after `Uploaded ` is a live partial // deploy. Earlier failures (resource provisioning such as the checkpoint // R2 bucket runs after the asset upload) leave the live version unchanged. if (deploymentProfile === 'sandbox') { console.error(workerUploaded || deployedVersionId ? sandboxRecovery() : sandboxStoppedBeforeUpload({ rerun: sandboxRerun() })); } process.exitCode = code ?? 1; return; } // A dry run deploys nothing — next-steps instructions would be a lie. if (deployArgs.includes('--dry-run')) { return; } const baseUrl = readinessBaseUrl(); if (!baseUrl || !deployedVersionId || !deploymentAuthority) { console.error( 'Worker uploaded, but its version or public origin was unavailable for readiness verification.', ); process.exitCode = 1; return; } try { if (upgradeContext) { // Upload does not change traffic. Recheck the serving installation before // activation, and use an isolated minimal config so Wrangler cannot sync // authored routes, crons, observability or other non-versioned settings. assertSameInstallation(upgradeContext.installation, validateInstallation(inspector(builtArtifact).inspect())); const activationConfig = path.join(path.dirname(upgradeContextPath), 'activation-wrangler.json'); writePrivateJson(activationConfig, { name: upgradeContext.target.worker, account_id: upgradeContext.target.account, compatibility_date: builtArtifact.config.compatibility_date }); await new Promise((resolve, reject) => { activeChild = spawn(process.execPath, [wranglerBin, 'versions', 'deploy', `${deployedVersionId}@100%`, '--yes', '--config', activationConfig, ...deploymentResourceArgs()], { cwd: path.dirname(activationConfig), stdio: 'inherit' }); activeChild.once('error', () => reject(new Error('Unable to activate the uploaded Worker version. Preserve the receipt.'))); activeChild.once('close', (status) => status === 0 ? resolve() : reject(new Error('Worker activation was not verified. Preserve the receipt and resume.'))); }); } await waitForDeploymentReadiness( baseUrl, deployedVersionId, deploymentAuthority.activation, ); if (guardedSandboxDeploy) { const container = verifySandboxContainerApplication({ ...sandboxToolOptions(builtArtifact.configPath), applicationName: sandboxApplicationName(builtArtifact.config), }); if (container.missing) throw new Error(`${container.message}\n${sandboxRecovery()}`); // An unreadable listing is not evidence of failure; say what is unknown. if (!container.ok) process.stdout.write(`! ${container.message}\n`); else { process.stdout.write( `${container.message} The first image rollout can take several minutes (wait for state "ready"). ` + 'Then open Admin → Settings → Coding sandbox, choose Check again, and choose Enable coding sandbox: ' + 'repository grants do not use the Container until it is enabled.\n', ); } if (sandboxCheckpointsOff) { process.stdout.write(`! ${checkpointsOffNotice(checkedAccount?.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID)}\n`); } } if (selectedEnvironmentTarget) { await environmentPreflightApi.completeEnvironmentDeployment( finalEnvironmentPreflight, { deployedVersion: deployedVersionId, projectRoot, providerContext: deploymentResourceArgs(), mutationLease: environmentMutationLease, }, ); } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; return; } if (upgradeContext) { writeDeploymentEvent(upgradeContextPath, 'ready', deployedVersionId); process.stdout.write('\n✔ Upgrade deployment is ready. Existing setup authority preserved.\n'); } else if (selectedEnvironmentTarget) { process.stdout.write('\n✔ Claimed environment deployment reconciled.\n'); // The skill requires a telemetry isolation receipt for the serving version // before any synthetic Slack action. Produce it here, so a verifier never // has to resolve the Worker and account by hand or act before it exists. try { const telemetry = await import('./lib/product-telemetry-preflight.mjs'); // One receipt per check: a re-deploy can serve the same version id again. const receiptPath = path.join(finalEnvironmentPreflight.registration.evidenceRoot, `telemetry-${deployedVersionId}-${new Date().toISOString().replace(/[:.]/g, '-')}.json`); let receipt; try { receipt = await telemetry.verifyProductTelemetry({ worker: builtArtifact.config.name, ...(checkedAccount?.accountId ? { accountId: checkedAccount.accountId } : {}), providerContext: deploymentResourceArgs(), projectRoot, }); } catch (error) { if (!(error instanceof telemetry.ProductTelemetryPreflightError)) throw error; receipt = error.receipt; } telemetry.writeProductTelemetryReceipt(receiptPath, receipt, projectRoot); if (receipt.status === 'passed') { process.stdout.write(`✔ Telemetry isolation verified for the serving version; receipt ${receiptPath}\n`); } else { process.stderr.write(`✖ Telemetry isolation failed (${receipt.failure?.code ?? 'unknown'}); receipt ${receiptPath}. ` + 'Do not send synthetic Slack traffic to this lane until it passes.\n'); process.exitCode = 1; } } catch (error) { process.stderr.write(`✖ Telemetry isolation was not checked: ${error instanceof Error ? error.message : String(error)}. ` + `Run \`npm run verify:telemetry -- --target ${selectedEnvironmentTarget}\` before any synthetic Slack traffic.\n`); process.exitCode = 1; } if (finalEnvironmentPreflight?.setupFlow?.proven === false) { process.stdout.write( '! Setup flow unproven for this revision: the first-run install sources ' + 'differ from the lane baseline. Prove a fresh install on a disposable ' + 'target and re-record the baseline to clear it.\n', ); } } else if (deployedUrl && deploymentAuthority) { printPrivateSetupLink(deployedUrl, deploymentAuthority.setup); } else if (deploymentAuthority) { printPrivateSetupPath(deploymentAuthority.setup); } });