This commit is contained in:
Colin Seymour
2026-06-14 15:59:01 +00:00
parent 6e2c577bfe
commit 632aa56a3c
+53 -29
View File
@@ -18,15 +18,6 @@
set -euo pipefail set -euo pipefail
# DEBUG: when set to a truthy value (1/true/yes/on), print verbose diagnostics
# and surface API responses/errors that are normally hidden. Unset (or 0/false)
# keeps the default, quiet output.
DEBUG="${DEBUG:-0}"
case "${DEBUG,,}" in
1 | true | yes | on) DEBUG=1 ;;
*) DEBUG=0 ;;
esac
HA_URL="http://localhost:8123" HA_URL="http://localhost:8123"
ALEXBELGIUM_REPO="https://github.com/alexbelgium/hassio-addons" ALEXBELGIUM_REPO="https://github.com/alexbelgium/hassio-addons"
ADMIN_USER="admin" ADMIN_USER="admin"
@@ -65,16 +56,15 @@ log() { echo -e "${CYAN}==>${NC} ${BOLD}$*${NC}"; }
warn() { echo -e "${YELLOW}WARNING:${NC} $*" >&2; } warn() { echo -e "${YELLOW}WARNING:${NC} $*" >&2; }
err() { group_end; echo -e "${RED}ERROR:${NC} $*" >&2; exit 1; } err() { group_end; echo -e "${RED}ERROR:${NC} $*" >&2; exit 1; }
ok() { echo -e "${CYAN}==>${NC} ${GREEN}$*${NC}"; } ok() { echo -e "${CYAN}==>${NC} ${GREEN}$*${NC}"; }
# debug() prints only when DEBUG is enabled and writes to stderr, so it never
# pollutes function output captured via $(...).
debug() { debug() {
[ "$DEBUG" = "1" ] || return 0 [ -n "$DEBUG" ] || return 0
echo -e "${DIM}DEBUG:${NC} $*" >&2 echo -e "${DIM}DEBUG:${NC} $*" >&2
} }
# debug_response() logs an API response when debugging, truncated so large
# payloads (e.g. addon logs) stay readable.
debug_response() { debug_response() {
[ "$DEBUG" = "1" ] || return 0 [ -n "$DEBUG" ] || return 0
local resp="$1" max=800 local resp="$1" max=800
if [ "${#resp}" -gt "$max" ]; then if [ "${#resp}" -gt "$max" ]; then
debug "response (truncated): ${resp:0:$max}..." debug "response (truncated): ${resp:0:$max}..."
@@ -89,7 +79,7 @@ debug_response() {
# unchanged. Markers go to stderr to stay ordered with debug() and err() output. # unchanged. Markers go to stderr to stay ordered with debug() and err() output.
_group_open=0 _group_open=0
group_start() { group_start() {
[ "$DEBUG" = "1" ] && [ "${GITHUB_ACTIONS:-}" = "true" ] || return 0 [ -n "$DEBUG" ] && [ "${GITHUB_ACTIONS:-}" = "true" ] || return 0
echo "::group::$*" >&2 echo "::group::$*" >&2
_group_open=1 _group_open=1
} }
@@ -132,6 +122,26 @@ supervisor_api_with_body() {
printf '%s\n' "$resp" printf '%s\n' "$resp"
} }
# supervisor_api_post() performs a state-changing POST (optionally with a JSON
# body) and aborts via err() if the Supervisor returns {"result":"error",...}.
# These endpoints (install/start/options/reload) are synchronous, so without
# this check a failure is silently swallowed and the script then polls for a
# state that will never arrive. The response is emitted on stdout on success.
supervisor_api_post() {
local endpoint="$1" body="${2:-}"
local resp result
if [ -n "$body" ]; then
resp=$(supervisor_api_with_body POST "$endpoint" "$body")
else
resp=$(supervisor_api POST "$endpoint")
fi
result=$(printf '%s' "$resp" | jq -r '.result // empty' 2>/dev/null)
if [ "$result" = "error" ]; then
err "Supervisor API error on POST ${endpoint}: $(printf '%s' "$resp" | jq -r '.message // .' 2>/dev/null)"
fi
printf '%s\n' "$resp"
}
# POST to the local HA API for an optional onboarding step. Failures are # POST to the local HA API for an optional onboarding step. Failures are
# non-fatal; the request and any response/error are shown only when debugging. # non-fatal; the request and any response/error are shown only when debugging.
ha_post_optional() { ha_post_optional() {
@@ -261,7 +271,7 @@ add_addon_repo() {
fi fi
log "Adding addon repository: ${ALEXBELGIUM_REPO}" log "Adding addon repository: ${ALEXBELGIUM_REPO}"
supervisor_api_with_body POST "/store/repositories" \ supervisor_api_post "/store/repositories" \
"{\"repository\":\"${ALEXBELGIUM_REPO}\"}" > /dev/null "{\"repository\":\"${ALEXBELGIUM_REPO}\"}" > /dev/null
# Wait for the store to refresh by checking if the postgres addon is available # Wait for the store to refresh by checking if the postgres addon is available
@@ -297,7 +307,7 @@ install_postgres() {
fi fi
log "Installing PostgreSQL addon..." log "Installing PostgreSQL addon..."
supervisor_api POST "/addons/${postgres_slug}/install" > /dev/null supervisor_api_post "/addons/${postgres_slug}/install" > /dev/null
# Wait for installation to complete # Wait for installation to complete
log "Waiting for PostgreSQL installation..." log "Waiting for PostgreSQL installation..."
@@ -328,7 +338,7 @@ configure_and_start_postgres() {
--arg db "$POSTGRES_DB" \ --arg db "$POSTGRES_DB" \
'{options: {POSTGRES_PASSWORD: $pw, POSTGRES_DB: $db, env_vars: []}}') '{options: {POSTGRES_PASSWORD: $pw, POSTGRES_DB: $db, env_vars: []}}')
supervisor_api_with_body POST "/addons/${postgres_slug}/options" \ supervisor_api_post "/addons/${postgres_slug}/options" \
"${options_json}" > /dev/null "${options_json}" > /dev/null
# Check if already running # Check if already running
@@ -341,7 +351,7 @@ configure_and_start_postgres() {
fi fi
log "Starting PostgreSQL addon..." log "Starting PostgreSQL addon..."
supervisor_api POST "/addons/${postgres_slug}/start" > /dev/null supervisor_api_post "/addons/${postgres_slug}/start" > /dev/null
# Wait for postgres to be ready # Wait for postgres to be ready
log "Waiting for PostgreSQL to be ready..." log "Waiting for PostgreSQL to be ready..."
@@ -382,9 +392,27 @@ remove_addon_image() {
jq 'del(.image)' "$CONFIG_JSON" > "${CONFIG_JSON}.tmp" && mv "${CONFIG_JSON}.tmp" "$CONFIG_JSON" jq 'del(.image)' "$CONFIG_JSON" > "${CONFIG_JSON}.tmp" && mv "${CONFIG_JSON}.tmp" "$CONFIG_JSON"
ok "Removed 'image' key from config.json" ok "Removed 'image' key from config.json"
# Reload the store so the Supervisor re-reads the local addon config # Reload the store so the Supervisor re-reads the local addon config, then
# confirm the store entry actually has no image before installing. The reload
# is eventually-consistent: if we install while the store still has the
# prebuilt image set, the Supervisor tries to PULL ghcr.io/...-{arch}:dev
# (which is never published) and fails with addon_unknown_error.
log "Reloading addon store..." log "Reloading addon store..."
supervisor_api POST "/store/reload" > /dev/null supervisor_api_post "/store/reload" > /dev/null
log "Verifying TeslaMate will build locally..."
local attempt=0 image
while [ "$attempt" -lt 12 ]; do
image=$(supervisor_api_jq "/store/addons/${TESLAMATE_SLUG}" '.data.image // empty')
if [ -z "$image" ]; then
ok "TeslaMate store entry has no image - will build from Dockerfile"
return 0
fi
debug "Attempt $((attempt + 1))/12: store still reports image=${image}"
sleep 5
attempt=$((attempt + 1))
done
err "Store still reports a prebuilt image for TeslaMate after reload; aborting to avoid pulling a non-existent image"
} }
install_teslamate() { install_teslamate() {
@@ -400,7 +428,7 @@ install_teslamate() {
fi fi
log "Installing TeslaMate addon..." log "Installing TeslaMate addon..."
supervisor_api POST "/addons/${TESLAMATE_SLUG}/install" > /dev/null supervisor_api_post "/addons/${TESLAMATE_SLUG}/install" > /dev/null
# Wait for installation to complete (the TeslaMate image is large) # Wait for installation to complete (the TeslaMate image is large)
log "Waiting for TeslaMate installation..." log "Waiting for TeslaMate installation..."
@@ -443,11 +471,7 @@ configure_and_start_teslamate() {
options_json=$(jq -n --argjson cur "$current_options" --argjson ovr "$overrides" '{options: ($cur + $ovr)}') options_json=$(jq -n --argjson cur "$current_options" --argjson ovr "$overrides" '{options: ($cur + $ovr)}')
debug "options payload: ${options_json}" debug "options payload: ${options_json}"
local response supervisor_api_post "/addons/${TESLAMATE_SLUG}/options" "${options_json}" > /dev/null
response=$(supervisor_api_with_body POST "/addons/${TESLAMATE_SLUG}/options" "${options_json}")
if [ "$(echo "$response" | jq -r '.result')" != "ok" ]; then
err "Failed to set TeslaMate options: $(echo "$response" | jq -r '.message // .')"
fi
# Check if already running # Check if already running
local state local state
@@ -460,7 +484,7 @@ configure_and_start_teslamate() {
fi fi
log "Starting TeslaMate addon..." log "Starting TeslaMate addon..."
supervisor_api POST "/addons/${TESLAMATE_SLUG}/start" > /dev/null supervisor_api_post "/addons/${TESLAMATE_SLUG}/start" > /dev/null
# Wait for addon state to become "started" # Wait for addon state to become "started"
log "Waiting for TeslaMate addon to start..." log "Waiting for TeslaMate addon to start..."