Ctrl K
ComunidadGuías…

apps/home-assistant/smoke-test.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -Eeuo pipefail

app_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
project="${COMPOSE_PROJECT_NAME:-smoke-home-assistant-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-$$}"
config_volume="${HOMEASSISTANT_CONFIG_VOLUME:-${project}-config}"
response="$(mktemp)"
backup_dir="$(mktemp -d)"

admin_user="smoke"
admin_password="Sm0ke-HomeAssistant-Password-2026"
automation_id="fossarysmoke"
automation_entity="automation.fossary_smoke"

# Port 0 asks for an ephemeral port: 8123 on the runner may be taken.
export HOMEASSISTANT_PORT=0
export HOMEASSISTANT_CONFIG_VOLUME="$config_volume"
export TZ=UTC
export COMPOSE_PROJECT_NAME="$project"
export HOMEASSISTANT_BACKUP_DIR="$backup_dir"

compose=(
  docker compose
  --project-name "$project"
  --env-file "$app_dir/.env.example"
  --file "$app_dir/compose.yaml"
)

cleanup() {
  status=$?
  trap - EXIT INT TERM
  set +e
  "${compose[@]}" ps --all
  "${compose[@]}" logs --no-color --timestamps --tail=200
  "${compose[@]}" down --volumes --remove-orphans --timeout 60
  docker volume rm --force "$config_volume" >/dev/null 2>&1
  rm -f "$response"
  rm -rf "$backup_dir"
  exit "$status"
}
# Every failed check names itself: `grep --quiet` and `test` say nothing, and all
# the CI log would keep of them is an exit code.
trap 'echo "Check failed on line $LINENO: $BASH_COMMAND" >&2' ERR
trap cleanup EXIT
trap 'exit 130' INT TERM

"${compose[@]}" config --quiet
"${compose[@]}" pull
# The first start unpacks the configuration into /config, so this waits longer than usual.
"${compose[@]}" up --detach --wait --wait-timeout 600

container_id="$("${compose[@]}" ps --quiet home-assistant)"
test -n "$container_id"
health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$container_id")"
test "$health" = healthy

refresh_endpoint() {
  # Port 0 means a fresh ephemeral port on every container start, so the address
  # is read again after each stop.
  web="$("${compose[@]}" port home-assistant 8123)"
  case "$web" in
    127.0.0.1:[0-9]*) ;;
    *) echo "Unexpected published address: $web" >&2; exit 1 ;;
  esac
  client_id="http://${web}/"
}

refresh_endpoint

# Until the first-run wizard is complete its API needs no authentication.
curl --fail --silent --show-error --location \
  --retry 30 --retry-all-errors --retry-delay 4 \
  --connect-timeout 3 --max-time 30 \
  --output "$response" "http://${web}/api/onboarding"
grep --fixed-strings --quiet '"step":"user"' "$response"

curl --fail --silent --show-error \
  --connect-timeout 3 --max-time 60 \
  --header 'Content-Type: application/json' \
  --data "{\"client_id\":\"${client_id}\",\"name\":\"Smoke Owner\",\"username\":\"${admin_user}\",\"password\":\"${admin_password}\",\"language\":\"en\"}" \
  --output "$response" "http://${web}/api/onboarding/users"
auth_code="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["auth_code"])' "$response")"
test -n "$auth_code"

exchange_token() {
  # $1 is the authorization code, from the first-run wizard or from the login flow.
  curl --fail --silent --show-error \
    --connect-timeout 3 --max-time 30 \
    --data-urlencode "grant_type=authorization_code" \
    --data-urlencode "code=$1" \
    --data-urlencode "client_id=${client_id}" \
    --output "$response" "http://${web}/auth/token"
  python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["access_token"])' "$response"
}

login_token() {
  # A full username and password sign-in: proves the user database survived.
  curl --fail --silent --show-error \
    --connect-timeout 3 --max-time 30 \
    --header 'Content-Type: application/json' \
    --data "{\"client_id\":\"${client_id}\",\"handler\":[\"homeassistant\",null],\"redirect_uri\":\"${client_id}?auth_callback=1\"}" \
    --output "$response" "http://${web}/auth/login_flow"
  local flow_id code
  flow_id="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["flow_id"])' "$response")"
  curl --fail --silent --show-error \
    --connect-timeout 3 --max-time 30 \
    --header 'Content-Type: application/json' \
    --data "{\"client_id\":\"${client_id}\",\"username\":\"${admin_user}\",\"password\":\"${admin_password}\"}" \
    --output "$response" "http://${web}/auth/login_flow/${flow_id}"
  code="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"])' "$response")"
  exchange_token "$code"
}

token="$(exchange_token "$auth_code")"
test -n "$token"

api_get() {
  curl --fail --silent --show-error --location \
    --retry 20 --retry-all-errors --retry-delay 3 \
    --connect-timeout 3 --max-time 30 \
    --header "Authorization: Bearer ${token}" \
    --output "$response" "http://${web}$1"
}

# The core must reach RUNNING, not recovery or safe mode. The state is polled
# rather than trusted on the first try: the healthcheck and `/api/config` answer
# before the integrations have loaded, so the first response carries
# `state: NOT_RUNNING`. A curl retry does not help — the HTTP code is already 200.
state=""
deadline=$((SECONDS + 240))
while :; do
  api_get /api/config
  state="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["state"])' "$response")"
  if [[ $state == RUNNING ]]; then
    break
  fi
  if ((SECONDS >= deadline)); then
    echo "Home Assistant did not reach RUNNING within 240s, last state: $state" >&2
    exit 1
  fi
  echo "Waiting for RUNNING, current state: $state"
  sleep 5
done

python3 - "$response" <<'PY'
import json, sys
config = json.load(open(sys.argv[1]))
assert config["state"] == "RUNNING", config["state"]
assert config["recovery_mode"] is False, "recovery mode"
assert config["safe_mode"] is False, "safe mode"
assert config["config_dir"] == "/config", config["config_dir"]
print(f"Home Assistant {config['version']} state={config['state']}")
PY

# The automation is both an application-level check and the artifact restore is
# verified against: the editor writes it to /config/automations.yaml.
curl --fail --silent --show-error \
  --connect-timeout 3 --max-time 30 \
  --header "Authorization: Bearer ${token}" \
  --header 'Content-Type: application/json' \
  --data '{"alias":"Fossary smoke","description":"","triggers":[{"trigger":"time","at":"03:15:00"}],"conditions":[],"actions":[{"action":"persistent_notification.create","data":{"message":"smoke"}}],"mode":"single"}' \
  --output "$response" "http://${web}/api/config/automation/config/${automation_id}"
grep --fixed-strings --quiet '"result":"ok"' "$response"

curl --fail --silent --show-error \
  --connect-timeout 3 --max-time 60 \
  --header "Authorization: Bearer ${token}" \
  --header 'Content-Type: application/json' \
  --data '{}' \
  --output "$response" "http://${web}/api/services/automation/reload"

api_get "/api/states/${automation_entity}"
grep --fixed-strings --quiet '"state":"on"' "$response"
docker exec "$container_id" grep --fixed-strings --quiet 'Fossary smoke' /config/automations.yaml

# Backup and restore must preserve both the automation and the user database.
bash "$app_dir/backup.sh"
archives=("$backup_dir"/home-assistant-*.tar.gz)
test -f "${archives[0]}"

# backup.sh starts the container but does not wait for it to be ready.
"${compose[@]}" up --detach --wait --wait-timeout 600
refresh_endpoint

# The server must come back up after being stopped for the archive.
api_get /api/config
grep --fixed-strings --quiet '"state":"RUNNING"' "$response"

curl --fail --silent --show-error --request DELETE \
  --connect-timeout 3 --max-time 30 \
  --header "Authorization: Bearer ${token}" \
  --output "$response" "http://${web}/api/config/automation/config/${automation_id}"
docker exec "$container_id" sh -c '! grep -q "Fossary smoke" /config/automations.yaml'

bash "$app_dir/restore.sh" "${archives[0]}"
"${compose[@]}" up --detach --wait --wait-timeout 600

container_id="$("${compose[@]}" ps --quiet home-assistant)"
refresh_endpoint
docker exec "$container_id" grep --fixed-strings --quiet 'Fossary smoke' /config/automations.yaml
docker exec "$container_id" test -f /config/home-assistant_v2.db

# After a restore both the sign-in and the core state are checked.
token="$(login_token)"
test -n "$token"
api_get /api/config
grep --fixed-strings --quiet '"state":"RUNNING"' "$response"
api_get "/api/states/${automation_entity}"
grep --fixed-strings --quiet "${automation_entity}" "$response"

echo "Home Assistant smoke test passed on $web"