Add a Deploy Adapter
Add a Deploy Adapter
Section titled “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
Section titled “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
Section titled “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
Section titled “Step 1 — Write the adapter”Create scripts/deploy/adapters/<name>.sh. Minimal discrete-step example:
#!/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
Section titled “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:
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
Section titled “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:
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
Section titled “Step 4 — Verify and deploy”Run the suite first:
Terminal:
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:
scripts/deploy.sh # the BoB deploy (source → ~/.claude)Terminal only — the BoB sync runs outside any Claude session.
Reference
Section titled “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).