#!/usr/bin/env bash # The SSH client submits once and follows durable stages on workstation. set -euo pipefail : "${DEPLOY_HOST:?missing DEPLOY_HOST}" : "${DEPLOY_USER:?missing DEPLOY_USER}" : "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}" : "${DEPLOY_KNOWN_HOSTS:?configure pinned DEPLOY_KNOWN_HOSTS}" : "${DEPLOY_RUN_ID:?missing DEPLOY_RUN_ID}" [[ "$DEPLOY_USER" =~ ^[A-Za-z_][A-Za-z0-9_.-]*$ ]] || exit 1 [[ "$DEPLOY_HOST" =~ ^[A-Za-z0-9_.:-]+$ ]] || exit 1 [[ "$DEPLOY_RUN_ID" =~ ^[0-9]+-[0-9]+$ ]] || exit 1 [[ "${DEPLOY_PORT:-22}" =~ ^[0-9]+$ ]] || exit 1 key_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/homelab-deploy-key.XXXXXXXX")" trap 'rm -rf "$key_dir"' EXIT chmod 700 "$key_dir" printf '%s\n' "$DEPLOY_KEY" >"$key_dir/key" printf '%s\n' "$DEPLOY_KNOWN_HOSTS" >"$key_dir/known_hosts" chmod 600 "$key_dir/key" "$key_dir/known_hosts" ssh_opts=(-i "$key_dir/key" -p "${DEPLOY_PORT:-22}" -o BatchMode=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$key_dir/known_hosts" -o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4) controller=.local/lib/homelab-deploy/controller.py case "${1:?start, apply, verify, smoke or summary required}" in start) python3 - <<'PY' >"$key_dir/request.json" import json import os from pathlib import Path release = json.loads(Path('release.json').read_text()) print(json.dumps({'release': release, 'mode': os.environ.get('DEPLOY_MODE', 'changed'), 'refresh_images': os.environ.get('REFRESH_IMAGES', 'false') == 'true'})) PY for attempt in 1 2 3; do rc=0 # shellcheck disable=SC2029 # The run ID and operation are validated local arguments, not remote variables. ssh "${ssh_opts[@]}" "$DEPLOY_USER@$DEPLOY_HOST" python3 "$controller" start "$DEPLOY_RUN_ID" <"$key_dir/request.json" || rc=$? [ "$rc" -eq 0 ] && exit 0 [ "$rc" -eq 255 ] || exit "$rc" sleep 5 done exit "$rc" ;; apply|verify|smoke) result=0 for attempt in 1 2 3; do rc=0 # shellcheck disable=SC2029 # The run ID and operation are validated local arguments, not remote variables. ssh "${ssh_opts[@]}" "$DEPLOY_USER@$DEPLOY_HOST" python3 "$controller" follow "$DEPLOY_RUN_ID" "$1" || rc=$? [ "$rc" -eq 0 ] && break [ "$rc" -eq 255 ] || { result="$rc"; break; } echo "SSH disconnected; reconnecting to the existing deploy ($attempt/3)" if [ "$attempt" -eq 3 ]; then result=255; break; fi sleep 5 done exit "$result" ;; summary) if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then rc=0 # shellcheck disable=SC2029 # The run ID is validated above. ssh "${ssh_opts[@]}" "$DEPLOY_USER@$DEPLOY_HOST" python3 "$controller" summary "$DEPLOY_RUN_ID" >"$key_dir/deploy-summary.md" || rc=$? if [ "$rc" -eq 0 ]; then cat "$key_dir/deploy-summary.md" >>"$GITHUB_STEP_SUMMARY" || echo "WARNING: cannot write the deploy summary" else echo 'Deploy summary is unavailable. The SSH connection failed or the controller did not respond. Check the job log.' >>"$GITHUB_STEP_SUMMARY" || true fi fi ;; *) echo "Unknown SSH operation: $1" >&2; exit 1 ;; esac