Appearance
Add a Deploy Adapter
Get a task done. Add support for a new deploy target (a stack or hosting platform) to the pluggable deploy-adapter framework. (Back to how-to home.)
The framework lives at scripts/deploy/ and is invoked through /deploy (or scripts/deploy.sh). It resolves an adapter for the project and runs the deploy contract — check → build → migrate → deploy → verify — parameterized by environment (--env test|prod). Adding an adapter is a one-file change: drop adapters/<name>.sh; resolution and dispatch pick it up automatically.
The contract
An adapter is a bash file sourced by the dispatcher. It sets ADAPTER_NAME and implements either the five ordered step functions or a single monolithic adapter_run:
| Function | Step | Notes |
|---|---|---|
adapter_check <env> | check | pre-flight gates (clean tree, types, tests) |
adapter_build <env> | build | produce the deployable artifact |
adapter_migrate <env> | migrate | apply schema/data migrations to <env> |
adapter_deploy <env> | deploy | ship to <env> |
adapter_verify <env> | verify | health-check the deployed <env> |
adapter_run <env> | (all) | escape hatch: run the whole pipeline yourself |
adapter_prepare <env> | (before all) | optional: resolve and validate the target; never skipped by --skip-tests/--skip-build; return 3 to refuse |
Each function receives the target environment (test or prod) as $1. Any undefined step is a no-op (a static site has no migrate). If adapter_run is defined it takes precedence over the step functions — use it to wrap a proven, indivisible pipeline.
What "clean tree" means. A check that refuses a dirty tree should ignore the files BoB's own tooling writes into a project: the session ledger, the decision log, .dash/ and the rest of scripts/lib/derived-paths.json. Ask node scripts/lib/derived-paths.js undeclared-dirt <dir>, which prints only the dirty paths that are not declared there, the same rule the segmentation checkpoint applies. The cloudflare adapter does this, and it names the paths it refuses over (#3257).
A step that returns 3 means refused: the dispatcher reports "status":"refused" and exits 3 instead of 1. The cloudflare adapter uses adapter_prepare for this — it plans the deploy from the project's wrangler config (lib/wrangler-config.js: wrangler.json / wrangler.jsonc / wrangler.toml) and refuses any target it cannot map to a declared environment, so --env test never runs a bare, production wrangler deploy (#3068).
The dispatcher exports to every adapter: DEPLOY_ENV, DEPLOY_DRY_RUN, DEPLOY_SKIP_TESTS, DEPLOY_SKIP_BUILD, DEPLOY_PROJECT_DIR.
Shared helpers
lib/contract.sh provides helpers so adapters stay lean — use them:
deploy_cfg '<jq filter>'— read a value from the project'sdev.json(empty if absent).run_cmd '<label>' '<command>'— run a command, honoring--dry-run(echo intent, don't execute).http_health '<url>' [tries]— curl a URL with backoff; 0 on first HTTP 200.pass/fail/warn/info/header— consistent logging.
Step 1 — Write the adapter
Create scripts/deploy/adapters/<name>.sh. Minimal discrete-step example:
bash
#!/usr/bin/env bash
# myhost.sh — deploy adapter for MyHost.
ADAPTER_NAME="myhost"
adapter_check() { local env="$1"; run_cmd "test" "npm test"; }
adapter_build() { local env="$1"; run_cmd "build" "npm run build"; }
adapter_migrate() { local env="$1"; run_cmd "migrate ($env)" "$(deploy_cfg '.deploy.migrate')"; }
adapter_deploy() { local env="$1"; run_cmd "deploy ($env)" "myhost push --env $env"; }
adapter_verify() { local env="$1"; http_health "$(deploy_cfg ".access.\"$env\"")"; }Do not call set -e or exit inside step functions — return non-zero to signal failure; the dispatcher aborts the pipeline on the first failing step and reports status:error. Honor --dry-run by routing real side effects through run_cmd (or guarding on DEPLOY_DRY_RUN).
Step 2 — Wire resolution
Auto-detection lives in lib/resolve.sh::_autodetect_adapter, which maps a detected stack → adapter name using scripts/fleet/readiness.js (the single source of truth for stack detection). Add a branch there if your adapter should be picked automatically:
bash
if [[ "$stack" == *" myhost "* ]]; then echo "myhost"; return 0; fiA project can always select an adapter explicitly without touching resolution — via dev.json .deploy.adapter or the --adapter <name> flag. An adapter name is valid iff adapters/<name>.sh exists.
Step 3 — Add a test
Extend tests/test-deploy-adapters.sh with a fixture + a --dry-run dispatch assertion (no real deploys), then confirm it is wired into the make test target:
bash
out="$(bash "$DEPLOY" --env prod --dry-run --adapter myhost --project "$TMP/myhost" 2>&1)"
contains "myhost runs deploy step" "myhost push --env prod" "$out"Step 4 — Verify and deploy
Run the suite first:
Terminal:
bash
make test # full suite, including test-deploy-adaptersTerminal only — the test suite has no slash command.
Then confirm the adapter resolves and smoke-test the contract. Both actions have a slash command (in a Claude Code session) and a terminal form (the dispatcher directly):
| Action | Slash command | Terminal |
|---|---|---|
| List adapters (yours should appear) | /deploy --list | scripts/deploy/deploy.sh --list |
| Smoke-test the contract (dry-run) | /deploy --adapter myhost --env test --dry-run | scripts/deploy/deploy.sh --adapter myhost --env test --dry-run |
Then sync to BOB_HOME with the BoB source→runtime sync so the runtime picks up the new adapter (this is not the deploy dispatcher above):
Terminal:
bash
scripts/deploy.sh # the BoB deploy (source → ~/.claude)Terminal only — the BoB sync runs outside any Claude session.
Reference
- Framework:
scripts/deploy/(deploy.sh,lib/contract.sh,lib/resolve.sh,adapters/) - Command: /deploy
- Environment targets come from
scripts/promotion/promotion.js(decide()→deploy: test|prod).