From 803740190cfea754f97fa031a2032a22e5eadf25 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Fri, 15 May 2026 19:04:15 -0400 Subject: [PATCH] chore(deploy): preflight docker workspace snapshot sync --- .beads/issues.jsonl | 1 + bun.lock | 1 + deployment/docker/workspace-root/bun.lock | 1 + ...ploy-preflight-docker-workspace-check.html | 83 ++++++++++++++ scripts/deploy.ts | 103 ++++++++++++++---- 5 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ead6db3..f2c75f6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,3 +1,4 @@ +{"_type":"issue","id":"islandflow-k4f","title":"Gate deploy script on docker workspace snapshot sync","description":"Prevent frozen-lockfile build failures during deploy by adding a local preflight in scripts/deploy.ts that runs bun run check:docker-workspace and aborts with a clear sync+commit remediation message when stale.","status":"closed","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T23:01:44Z","created_by":"dirtydishes","updated_at":"2026-05-15T23:04:11Z","started_at":"2026-05-15T23:01:48Z","closed_at":"2026-05-15T23:04:11Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-xll","title":"Fix bun.lock drift causing frozen-lockfile Docker build failures","description":"Docker image builds fail in multiple targets (candles, web, ingest services) because bun install --frozen-lockfile detects lockfile changes. Update workspace lockfile to match manifests and verify frozen install succeeds.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-15T22:52:38Z","created_by":"dirtydishes","updated_at":"2026-05-15T22:55:23Z","started_at":"2026-05-15T22:52:40Z","closed_at":"2026-05-15T22:55:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9nd","title":"Hosted synthetic tape redesign with internal control surface","description":"Implement hosted synthetic market redesign with shared deterministic regime engine, internal JetStream KV control plane, ingest coupling across options and equities, and an internal bottom-right synthetic-control drawer with Next proxy routes. Preserve the six public smart-money categories while adding hidden subtype families, soft coverage accounting, and backend-only admin endpoints.\n","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-14T01:25:02Z","created_by":"dirtydishes","updated_at":"2026-05-14T02:10:03Z","started_at":"2026-05-14T01:25:09Z","closed_at":"2026-05-14T02:10:03Z","close_reason":"Completed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"islandflow-9dz","title":"Tune synthetic smart-money scenario coverage","description":"Redesign synthetic smart-money option prints so the emitted scenarios trigger each classifier category more consistently while staying directionally plausible. Focus on scenario mix, DTE/moneyness, price placement, and event/structure context so the Electron demo reliably shows institutional directional, retail whale, event-driven, vol seller, arbitrage, and hedge reactive hits.\n","status":"in_progress","priority":1,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-13T21:36:37Z","created_by":"dirtydishes","updated_at":"2026-05-13T21:36:41Z","started_at":"2026-05-13T21:36:41Z","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/bun.lock b/bun.lock index c660953..46160a7 100644 --- a/bun.lock +++ b/bun.lock @@ -39,6 +39,7 @@ "packages/bus": { "name": "@islandflow/bus", "dependencies": { + "@islandflow/types": "workspace:*", "nats": "^2.24.0", }, }, diff --git a/deployment/docker/workspace-root/bun.lock b/deployment/docker/workspace-root/bun.lock index c660953..46160a7 100644 --- a/deployment/docker/workspace-root/bun.lock +++ b/deployment/docker/workspace-root/bun.lock @@ -39,6 +39,7 @@ "packages/bus": { "name": "@islandflow/bus", "dependencies": { + "@islandflow/types": "workspace:*", "nats": "^2.24.0", }, }, diff --git a/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html b/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html new file mode 100644 index 0000000..fbeb67d --- /dev/null +++ b/docs/turns/2026-05-15-deploy-preflight-docker-workspace-check.html @@ -0,0 +1,83 @@ + + + + + + Turn Report - 2026-05-15 - Deploy preflight docker workspace check + + + +

Turn Report: Deploy script preflight guard for Docker workspace snapshot

+

Date/Time: 2026-05-15 19:03:09 EDT

+ +

Summary

+
+ Updated scripts/deploy.ts so ./deploy now fails fast when + deployment/docker/workspace-root is stale. The script now runs + bun run check:docker-workspace during local prechecks and prints a clear remediation + message to run sync + commit before deployment. +
+ +

Changes Made

+ +
Refusing deploy: deployment/docker/workspace-root is out of sync.
+Run bun run sync:docker-workspace, commit updated snapshot files, then retry deploy.
+ + +

Context

+

+ The deployment compose stack builds from a snapshot under + deployment/docker/workspace-root. If that snapshot drifts from the active + workspace graph, Docker build-time bun install --frozen-lockfile fails remotely. + This change catches drift locally before any remote rollout starts. +

+ +

Important Implementation Details

+ + +

Validation

+ + +

Issues, Limitations, and Mitigations

+ + +

Follow-up Work

+ + + diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 87abd52..b76a393 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -11,16 +11,32 @@ const REMOTE_HOST = "delta@152.53.80.229"; const REMOTE_REPO = "/home/delta/islandflow"; const REMOTE_DEPLOYMENT = "/home/delta/islandflow/deployment/docker"; const SSH_KEY = path.join(process.env.HOME ?? "", ".ssh", "delta_ed25519"); -const SSH_OPTIONS = ["-i", SSH_KEY, "-o", "IdentitiesOnly=yes", "-o", "BatchMode=yes"]; +const SSH_OPTIONS = [ + "-i", + SSH_KEY, + "-o", + "IdentitiesOnly=yes", + "-o", + "BatchMode=yes", +]; const ALLOWED_REMOTE_UNTRACKED = new Set([ "deployment/docker/signal-cli-0.14.3-Linux-native.tar.gz", - "deployment/npm/" + "deployment/npm/", ]); const API_CONTAINER = "islandflow-vps-api-1"; const WEB_CONTAINER = "islandflow-vps-web-1"; -const PUBLIC_APP_URL = process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io"; -const PUBLIC_API_HEALTH_URL = process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null; -const LOG_SERVICES = ["api", "web", "compute", "candles", "ingest-options", "ingest-equities"]; +const PUBLIC_APP_URL = + process.env.DEPLOY_PUBLIC_APP_URL?.trim() || "https://flow.deltaisland.io"; +const PUBLIC_API_HEALTH_URL = + process.env.DEPLOY_PUBLIC_API_HEALTH_URL?.trim() || null; +const LOG_SERVICES = [ + "api", + "web", + "compute", + "candles", + "ingest-options", + "ingest-equities", +]; const scriptPath = fileURLToPath(import.meta.url); const repoRoot = path.resolve(path.dirname(scriptPath), ".."); @@ -55,12 +71,16 @@ function formatCommand(command: string, args: string[]): string { .join(" "); } -function runChecked(command: string, args: string[], options: SpawnSyncOptions = {}): void { +function runChecked( + command: string, + args: string[], + options: SpawnSyncOptions = {}, +): void { console.log(`$ ${formatCommand(command, args)}`); const result = spawnSync(command, args, { cwd: repoRoot, stdio: "inherit", - ...options + ...options, }); if (result.status !== 0) { @@ -68,12 +88,16 @@ function runChecked(command: string, args: string[], options: SpawnSyncOptions = } } -function captureChecked(command: string, args: string[], options: SpawnSyncOptions = {}): string { +function captureChecked( + command: string, + args: string[], + options: SpawnSyncOptions = {}, +): string { const result = spawnSync(command, args, { cwd: repoRoot, encoding: "utf8", stdio: ["inherit", "pipe", "pipe"], - ...options + ...options, }); if (result.status !== 0) { @@ -84,7 +108,11 @@ function captureChecked(command: string, args: string[], options: SpawnSyncOptio return result.stdout ?? ""; } -function runRemoteScript(title: string, script: string, args: string[] = []): void { +function runRemoteScript( + title: string, + script: string, + args: string[] = [], +): void { section(title); const sshArgs = [...SSH_OPTIONS, REMOTE_HOST, "bash", "-s", "--", ...args]; console.log(`$ ${formatCommand("ssh", sshArgs)}`); @@ -92,7 +120,7 @@ function runRemoteScript(title: string, script: string, args: string[] = []): vo cwd: repoRoot, input: script, encoding: "utf8", - stdio: ["pipe", "inherit", "inherit"] + stdio: ["pipe", "inherit", "inherit"], }); if (result.status !== 0) { @@ -100,7 +128,10 @@ function runRemoteScript(title: string, script: string, args: string[] = []): vo } } -function parseArgs(rawArgs: string[]): { mode: DeployMode; forceRecreate: boolean } { +function parseArgs(rawArgs: string[]): { + mode: DeployMode; + forceRecreate: boolean; +} { if (rawArgs.includes("--help") || rawArgs.includes("-h")) { usage(0); } @@ -114,7 +145,9 @@ function parseArgs(rawArgs: string[]): { mode: DeployMode; forceRecreate: boolea if ( (positional.length === 1 && positional[0] === "current-branch") || - (positional.length === 2 && positional[0] === "current" && positional[1] === "branch") + (positional.length === 2 && + positional[0] === "current" && + positional[1] === "branch") ) { return { mode: "current-branch", forceRecreate }; } @@ -129,12 +162,28 @@ function assertSshKeyExists(): void { } } +function localWorkspaceSnapshotPrecheck(): void { + console.log("$ bun run check:docker-workspace"); + const result = spawnSync("bun", ["run", "check:docker-workspace"], { + cwd: repoRoot, + stdio: "inherit", + }); + + if (result.status !== 0) { + console.error( + "Refusing deploy: deployment/docker/workspace-root is out of sync. Run `bun run sync:docker-workspace`, commit updated snapshot files, then retry deploy.", + ); + process.exit(result.status ?? 1); + } +} + function localMainPrecheck(): void { section("Local Precheck"); runChecked("git", ["fetch", "origin"]); runChecked("git", ["status", "--short", "--branch"]); runChecked("git", ["rev-parse", "--verify", "HEAD"]); runChecked("git", ["rev-parse", "origin/main"]); + localWorkspaceSnapshotPrecheck(); } function currentBranchName(): string { @@ -155,10 +204,12 @@ function localBranchPrecheck(branch: string): void { const porcelain = captureChecked("git", ["status", "--porcelain=v1"]).trim(); if (porcelain) { console.error( - `Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.` + `Refusing to deploy ${branch} with uncommitted local changes. Commit the intended state first.`, ); process.exit(1); } + + localWorkspaceSnapshotPrecheck(); } function publishCurrentBranch(branch: string): void { @@ -169,8 +220,8 @@ function publishCurrentBranch(branch: string): void { { cwd: repoRoot, encoding: "utf8", - stdio: ["inherit", "pipe", "pipe"] - } + stdio: ["inherit", "pipe", "pipe"], + }, ); if (upstreamResult.status === 0) { @@ -218,12 +269,18 @@ while IFS= read -r line; do ;; esac done <<< "$status" -` +`, ); } -function remoteRollout(mode: DeployMode, branch: string | null, forceRecreate: boolean): void { - const composeArgs = forceRecreate ? "up -d --build --force-recreate" : "up -d --build"; +function remoteRollout( + mode: DeployMode, + branch: string | null, + forceRecreate: boolean, +): void { + const composeArgs = forceRecreate + ? "up -d --build --force-recreate" + : "up -d --build"; const switchCommand = mode === "main" ? `git switch main @@ -242,7 +299,7 @@ ${switchCommand} cd "${REMOTE_DEPLOYMENT}" docker compose ${composeArgs} -` +`, ); } @@ -257,7 +314,7 @@ docker compose ps docker compose logs --tail=100 ${LOG_SERVICES.join(" ")} docker exec ${API_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:4000/health"); console.log(await r.text())' docker exec ${WEB_CONTAINER} bun -e 'const r = await fetch("http://127.0.0.1:3000/"); console.log(r.status)' -` +`, ); } @@ -271,7 +328,7 @@ function publicVerification(): void { } console.log( - "Skipping separate public API health check; same-origin mode relies on the public app check plus container-local API verification." + "Skipping separate public API health check; same-origin mode relies on the public app check plus container-local API verification.", ); } @@ -293,7 +350,7 @@ function main(): void { console.log( mode === "main" ? "Deploying origin/main to the existing Islandflow VPS checkout." - : "Deploying the current local branch to the existing Islandflow VPS checkout." + : "Deploying the current local branch to the existing Islandflow VPS checkout.", ); if (mode === "main") {