#!/usr/bin/env bash # Write path: repo -> HA instance, for the "api" adapter only (see # services/home-assistant/DESIGN.md, "Deploy path: adapter per instance", # "Sync model", "Validation gate"). # # Scope: automations/scripts/scenes only. Dashboards and helpers are # WebSocket-only (scripts/ha/lib/ha_ws.py has no mutating command, # deliberately) — writing those is a separate, not-yet-built task. # # Hard sequence enforced in lib/deploy_api.py, no shortcuts: DRIFT-CHECK -> # VALIDATE (local + live check_config) -> WRITE (per object) -> VERIFY (per # object). Any drift or validation failure aborts before a single mutating # call is made. --dry-run runs the same DRIFT-CHECK/VALIDATE steps (both # read-only in effect) and stops before WRITE/VERIFY. # # Instances with status != "active" (e.g. ken-legacy: archived, chelsty-ha: # offline) or adapter != "api" are refused unconditionally — see # instances.yaml and DESIGN.md's "Incident log" for why ken-legacy must # never be a deploy target again. # # Usage: deploy.sh [--dry-run] [file...] # no file args -> every automations/scripts/scenes file currently in # services/home-assistant/config// # file args -> only those objects (path as given, repo-root-relative, # or config-dir-relative all accepted) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SERVICE_DIR="$(cd "$SCRIPT_DIR/../../services/home-assistant" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" INSTANCES_FILE="$SERVICE_DIR/instances.yaml" usage() { echo "usage: $0 [--dry-run] [file...]" >&2 } if [[ $# -lt 1 ]]; then usage exit 2 fi INSTANCE="$1" shift DRY_RUN=0 FILE_ARGS=() for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=1 ;; --) ;; *) FILE_ARGS+=("$arg") ;; esac done if [[ ! -f "$INSTANCES_FILE" ]]; then echo "error: instances file not found: $INSTANCES_FILE" >&2 exit 1 fi INSTANCE_CONFIG_OUTPUT="$(python3 "$SCRIPT_DIR/lib/instance_config.py" "$INSTANCES_FILE" "$INSTANCE")" || exit 1 eval "$INSTANCE_CONFIG_OUTPUT" if [[ "$HA_STATUS" != "active" ]]; then echo "error: instance '$INSTANCE' has status='$HA_STATUS', not 'active' — deploy.sh refuses" >&2 echo " non-active instances unconditionally, no override flag (see instances.yaml)." >&2 exit 1 fi if [[ "$HA_ADAPTER" != "api" ]]; then echo "error: instance '$INSTANCE' uses adapter='$HA_ADAPTER' — deploy.sh only implements" >&2 echo " the 'api' write path (see DESIGN.md, 'Deploy path: adapter per instance')." >&2 exit 1 fi api_token_file="${HA_TOKEN_PATH/#\~/$HOME}" if [[ -z "$HA_TOKEN_PATH" || ! -f "$api_token_file" ]]; then echo "error: instance '$INSTANCE' has no token at '${api_token_file:-}' — deploy.sh" >&2 echo " has no fallback without a deploy_agent long-lived access token (see" >&2 echo " DESIGN.md, 'Tokens')." >&2 exit 1 fi CONFIG_DIR="$SERVICE_DIR/config/$INSTANCE" CONFIG_RELPATH="services/home-assistant/config/$INSTANCE" if [[ ! -d "$CONFIG_DIR" ]]; then echo "error: no config/$INSTANCE/ directory — run scripts/ha/import.sh $INSTANCE at least" >&2 echo " once first, so there is a last-known snapshot to drift-check against." >&2 exit 1 fi echo "== ha deploy: instance=$INSTANCE adapter=$HA_ADAPTER dry_run=$DRY_RUN ==" >&2 python3 "$SCRIPT_DIR/lib/deploy_api.py" \ "$HA_BASE_URL" "$api_token_file" "$REPO_ROOT" "$CONFIG_DIR" "$CONFIG_RELPATH" "$INSTANCE" "$DRY_RUN" \ "${FILE_ARGS[@]}"