Skip to content

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 contractcheck → 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.

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

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 (this is what the cloudflare adapter does).

The dispatcher exports to every adapter: DEPLOY_ENV, DEPLOY_DRY_RUN, DEPLOY_SKIP_TESTS, DEPLOY_SKIP_BUILD, DEPLOY_PROJECT_DIR.

lib/contract.sh provides helpers so adapters stay lean — use them:

  • deploy_cfg '<jq filter>' — read a value from the project’s dev.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.

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).

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:

Terminal window
if [[ "$stack" == *" myhost "* ]]; then echo "myhost"; return 0; fi

A 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.

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:

Terminal window
out="$(bash "$DEPLOY" --env prod --dry-run --adapter myhost --project "$TMP/myhost" 2>&1)"
contains "myhost runs deploy step" "myhost push --env prod" "$out"
Terminal window
make test # full suite, including test-deploy-adapters
scripts/deploy/deploy.sh --list # your adapter should appear
scripts/deploy.sh --adapter myhost --env test --dry-run # smoke-test the contract

Then sync to BOB_HOME with scripts/deploy.sh (the BoB deploy) so the runtime picks up the new adapter.

  • 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).