# GPS-Denied Onboard — Deployment Scripts > Generated by `/autodev` greenfield Step 16 (Deploy) — Step 7. Five > bash scripts under `scripts/` automate the procedures in > `deployment_procedures.md`. Step 7 is the only step in the deploy > skill that produces executable artefacts; all five scripts honour the > `/run/azaion/in-flight` flight-state gate documented in Step 6. ## Overview | Script | Purpose | Location | |--------|---------|----------| | `deploy.sh` | Main orchestrator (pull → flight-state-check → stop → start → health); `--rollback` flag restores the previous image set | `scripts/deploy.sh` | | `pull-images.sh` | Pull images from `${REGISTRY_HOST}/azaion/:-` (suite Gitea Packages registry) | `scripts/pull-images.sh` | | `start-services.sh` | `docker compose up -d`; waits for HEALTHCHECK; emits `AZAION_UPDATE_EVENT` via journald | `scripts/start-services.sh` | | `stop-services.sh` | Graceful `docker compose down`; saves current image digests to `.previous-tags.env` for rollback | `scripts/stop-services.sh` | | `health-check.sh` | Reads Docker HEALTHCHECK status across the stack (no HTTP endpoint — NFT-SEC-05) | `scripts/health-check.sh` | ## Prerequisites - Docker + Docker Compose v2 installed on the target host (Tier-1 workstation, lab Jetson, airborne Jetson, or operator workstation). - For remote operation: SSH access to the target via `DEPLOY_HOST` env var (same pattern as `scripts/run-tests-jetson.sh` uses). - Registry credentials: `REGISTRY_HOST` + `REGISTRY_USER` + `REGISTRY_TOKEN` (suite-provisioned Woodpecker global secrets per `../_infra/ci/README.md`). Loaded from `.env` at the repo root or passed via the environment. - `.env` file populated from `.env.example`. See `environment_strategy.md` § Environment Variables for per-environment guidance. ## Environment Variables Consumed All scripts source `.env` from the project root if present. The deploy-side variables consumed (beyond the ones already documented in `.env.example`): | Variable | Required by | Purpose | |----------|-------------|---------| | `REGISTRY_HOST` | `pull-images.sh`, `deploy.sh` | Suite Gitea Packages registry hostname (e.g. `git.azaion.com`) | | `REGISTRY_USER` | `pull-images.sh` | Registry user (Woodpecker global secret on CI; operator credentials locally) | | `REGISTRY_TOKEN` | `pull-images.sh` | Registry token (matches Woodpecker global secret); passed to `docker login --password-stdin` | | `DEPLOY_HOST` | All (remote mode) | SSH alias / `user@host` for remote execution. Unset = local execution. | | `AIRBORNE_COMPOSE_FILE` | `start-services.sh`, `stop-services.sh`, `health-check.sh` (when `--target=airborne`) | Override the default airborne compose path (`/etc/gps-denied/docker-compose.airborne.yml`) | | `AZAION_REVISION` | `start-services.sh` (for the audit `AZAION_UPDATE_EVENT` line) | Inherited from the image's `ENV AZAION_REVISION=$CI_COMMIT_SHA` per AZ-204 | | `BRANCH`, `ARCH` | `pull-images.sh`, `deploy.sh` | Tag selector (defaults: `main`, `arm`) | | `WAIT_SECS` | `start-services.sh`, `deploy.sh` | HEALTHCHECK wait budget (default: 120 s) | `.previous-tags.env` is written at the project root by `stop-services.sh` and is git-ignored (added to `.gitignore` in this step). ## Targets Every script accepts `--target ` and picks a sensible compose file by default: | `--target` | Default compose file | Purpose | |------------|----------------------|---------| | `dev` | `docker-compose.yml` | Tier-1 workstation Docker (developer + CI) | | `operator-workstation` | `docker-compose.yml` (reused; operator workstation runs only `operator-orchestrator` + `db`) | Operator deploy of `operator-orchestrator`. Cycle-2 may add a dedicated `docker-compose.operator.yml` that excludes the `companion` service. | | `airborne` | `${AIRBORNE_COMPOSE_FILE:-/etc/gps-denied/docker-compose.airborne.yml}` | Tier-2 airborne Jetson. Cycle-1 ships no compose file at this path — Watchtower drives updates via the parent-suite `_infra/deploy/jetson/docker-compose.yml`. The scripts are still usable for manual cycle-1 operator-issued cycle/restart on the bench Jetson by passing `--compose-file ./docker-compose.test.jetson.yml` or pointing `AIRBORNE_COMPOSE_FILE` at the parent-suite compose. | ## Script Details ### `deploy.sh` Main orchestrator. Runs: 1. `pull-images.sh --target --branch --arch ` (skipped on `--rollback`) 2. Flight-state check (in-band — invokes `stop-services.sh` which performs the actual `/run/azaion/in-flight` probe) 3. `stop-services.sh --target ` (also writes `.previous-tags.env`) 4. `start-services.sh --target --wait-secs ` 5. `health-check.sh --target ` **Usage**: ``` scripts/deploy.sh [--target dev|airborne|operator-workstation] [--branch ] [--arch ] [--compose-file ] [--wait-secs N] [--rollback] [--force] [--help] ``` **Rollback**: when `--rollback` is passed, `deploy.sh` reads `.previous-tags.env` (written by the most recent `stop-services.sh` run), `docker pull`s each saved image digest, then proceeds with the stop → start → health pipeline. Cycle-1 does not retag — the operator owns the registry-side tag promotion per `deployment_procedures.md` § Rollback Procedures. **Force flag** (`--force`): bypasses the `/run/azaion/in-flight` safety gate. **Never pass during a live flight** — this is an emergency escape hatch for stuck flag scenarios (e.g. autopilot service died holding the flag). ### `pull-images.sh` Pulls the cycle-1 image set from the suite registry. Cycle-2 will pick up the airborne `companion-jetson` image automatically when `--target=airborne` is selected (the image name template is already coded for it). **Usage**: ``` scripts/pull-images.sh [--branch ] [--arch ] [--target dev|airborne|operator-workstation] [--verify] [--help] ``` **`--verify`**: after pull, prints each image's RepoDigest + `AZAION_REVISION` env var (per the OCI labels mandated by AZ-204). ### `start-services.sh` `docker compose up -d --remove-orphans`. Polls `docker compose ps --format` until every service that declares a HEALTHCHECK reports `healthy` (default budget: 120 s). On success, emits a structured `AZAION_UPDATE_EVENT` line via journald (`logger -t gps-denied-onboard -p user.notice`). **Usage**: ``` scripts/start-services.sh [--target dev|airborne|operator-workstation] [--compose-file ] [--wait-secs N] [--force] [--help] ``` **Refuses to start the airborne stack when `/run/azaion/in-flight` is set** (unless `--force` is passed) — this matches `deployment_procedures.md` § Deployment Strategy "ground-only safety gate". ### `stop-services.sh` Graceful `docker compose down --remove-orphans`. The companion's stop sequence is governed by Docker's default 10 s grace period in cycle-1; cycle-2 adds `stop_grace_period: 30s` to the `companion` service block (see `deployment_procedures.md` § Graceful Shutdown — Cycle-1 status). Before stopping, writes the current image set to `.previous-tags.env` in the repo root: ``` # Saved by scripts/stop-services.sh on 2026-05-20T05:54:00Z # Used by deploy.sh --rollback to restore the previous image set. # Service tag layout: PREV__IMAGE=@ PREV_COMPANION_IMAGE=gps-denied-onboard/companion@sha256:abc… PREV_OPERATOR_ORCHESTRATOR_IMAGE=gps-denied-onboard/operator-orchestrator@sha256:def… PREV_MOCK_SAT_IMAGE=gps-denied-onboard/mock-suite-sat-service@sha256:… PREV_DB_IMAGE=postgres@sha256:… ``` **Refuses to stop the airborne stack when `/run/azaion/in-flight` is set** (unless `--force` is passed). **Usage**: ``` scripts/stop-services.sh [--target dev|airborne|operator-workstation] [--compose-file ] [--force] [--help] ``` ### `health-check.sh` Reads Docker HEALTHCHECK status across the stack via `docker compose ps --format '{{.Service}}\t{{.State}}\t{{.Health}}'`. No HTTP endpoints (NFT-SEC-05 — the companion has no inbound listener). **Usage**: ``` scripts/health-check.sh [--target dev|airborne|operator-workstation] [--compose-file ] [--help] ``` **Exit codes**: - `0` — all services healthy (or running with no declared HEALTHCHECK, which is the case for services that intentionally have none, e.g. `mock-sat` in test profiles where the HEALTHCHECK is declared elsewhere). - `1` — at least one service is `running` but `unhealthy`. - `2` — at least one service is not `running` (exited, dead, or never started). ## Common Properties All five scripts: - `#!/usr/bin/env bash` + `set -euo pipefail`. - Support `--help` / `-h` (heredoc-based usage block — robust to source-line reordering). - Source `.env` from the project root if present (`set -a` / `set +a` around the source so the variables are exported into the script's environment + subprocesses). - Support **remote execution** via `DEPLOY_HOST=` env var. When set, every docker command is run via `ssh ${DEPLOY_HOST}`. The pre-flight SSH check uses `-o BatchMode=yes -o ConnectTimeout=5` (same pattern as `scripts/run-tests-jetson.sh`). - Are **idempotent** for the running-stack case: `start-services.sh` is safe to re-run on an already-healthy stack; `stop-services.sh` is safe to re-run on an already-stopped stack; `pull-images.sh` is safe to re-run (docker will report "Image is up to date"). - Exit codes are stable per script (documented in each script's `--help` and at the top of this document). ## Local Smoke Test (Tier-1 dev) After authoring, the operator can smoke-test the full chain on a Tier-1 workstation: ```bash # Reset docker compose -f docker-compose.yml down -v # Manual pipeline (does what deploy.sh does, step by step) scripts/pull-images.sh --target dev --branch dev --arch arm # optional in dev; the dev compose builds locally scripts/start-services.sh --target dev --wait-secs 180 # gives 3 min for pip / cmake on first build scripts/health-check.sh --target dev # exit 0 when companion + operator-orchestrator + db + mock-sat are healthy scripts/stop-services.sh --target dev # writes .previous-tags.env ``` A `docker compose ps` between each step verifies the expected service state. Cycle-2 will add an automated smoke test under `tests/e2e/scripts/` that runs this sequence on a CI-clean host. ## Self-verification - [x] All five scripts created under `scripts/` and marked executable (`chmod +x`). - [x] Scripts source `.env` from the project root (when present); `REGISTRY_HOST` / `REGISTRY_USER` / `REGISTRY_TOKEN` consumed by `pull-images.sh`. - [x] `deploy.sh` orchestrates pull → flight-state-check → stop → start → health; `--rollback` restores `.previous-tags.env`. - [x] `pull-images.sh` handles `docker login` via `--password-stdin` and tags images per `${REGISTRY_HOST}/azaion/:-` (suite contract). - [x] `start-services.sh` brings up `docker compose up -d` and waits for HEALTHCHECK; emits `AZAION_UPDATE_EVENT` via `logger` on systemd hosts. - [x] `stop-services.sh` writes `.previous-tags.env` then runs `docker compose down --remove-orphans`; honours the `/run/azaion/in-flight` gate. - [x] `health-check.sh` reads HEALTHCHECK status via `docker compose ps` (no HTTP endpoint — NFT-SEC-05). - [x] Rollback supported via `deploy.sh --rollback`. - [x] Remote deployment via SSH supported through `DEPLOY_HOST` (same pattern as `scripts/run-tests-jetson.sh`). - [x] `.previous-tags.env` added to `.gitignore` (rollback bookmark; not a committed artefact). - [x] All scripts use heredoc-based `--help` (robust to source-line shifts) and `set -euo pipefail`. - [x] `bash -n` syntax-checks pass on all five scripts. ## Cycle-2 Polish (logged, not implemented in cycle-1) 1. **`stop_grace_period: 30s`** on the `companion` service in `docker-compose.yml` + the parent-suite Jetson compose, once the Step 2 BLOCKING gate "TensorRT INT8 cache durability under Docker" measures the actual drain budget on Tier-2 hardware (`deployment_procedures.md` § Graceful Shutdown — Cycle-1 status). 2. **`docker-compose.operator.yml`** — operator-only compose that excludes the `companion` service so `--target=operator-workstation` doesn't pull / start the airborne binary at all. 3. **Tag-rotation helper** — `scripts/promote-tag.sh ` that retags the registry-side `${REGISTRY_HOST}/azaion/:-arm` for production rollouts. Cycle-1 keeps this operator-manual. 4. **`scripts/post-flight-pull.sh`** — pulls FDR segments from the airborne Jetson to the operator workstation and runs `python3 -m gps_denied_onboard.post_flight.summarise` (per `observability.md` § Flight Analytics). 5. **CI-clean smoke test** — `tests/e2e/scripts/test_deploy_pipeline.sh` exercising `pull → start → health → stop → rollback` against a clean Docker host (gated by `RUN_DEPLOY_E2E=1`). 6. **Watchtower post-update hook on the operator workstation** — cycle-2 may add a Watchtower instance on the operator workstation that polls the suite registry and applies updates automatically. Cycle-1 leaves the operator workstation on the `scripts/deploy.sh` operator-driven path.