name: deploy on: push: branches: - main workflow_dispatch: concurrency: group: deploy-main cancel-in-progress: false jobs: redeploy: runs-on: [self-hosted, linux, arch, homelab, prod] steps: - name: Redeploy workstation shell: bash env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} # Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every # manifest to carry label app.kubernetes.io/managed-by=homelab-deploy, # otherwise previously applied resources get deleted on the next run. APPLY_PRUNE: ${{ vars.APPLY_PRUNE }} run: | set -euo pipefail : "${DEPLOY_HOST:?missing DEPLOY_HOST}" : "${DEPLOY_USER:?missing DEPLOY_USER}" : "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}" deploy_port="${DEPLOY_PORT:-22}" deploy_path="${DEPLOY_PATH:-/srv/homelab}" ssh_key="$RUNNER_TEMP/deploy_key" mkdir -p "$RUNNER_TEMP" printf '%s\n' "$DEPLOY_KEY" > "$ssh_key" chmod 600 "$ssh_key" ssh_opts=( -i "$ssh_key" -p "$deploy_port" -o BatchMode=yes -o StrictHostKeyChecking=accept-new ) ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \ "DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF' set -euo pipefail repo="${DEPLOY_PATH:-/srv/homelab}" if [ ! -d "$repo/.git" ]; then echo "Repository not found at $repo" exit 1 fi git -C "$repo" fetch origin main git -C "$repo" reset --hard origin/main # Runtime selection: a service is k8s-managed when $SERVICE/k8s/active # exists. Otherwise it is compose-managed, and only k8s/routing/* # manifests (external Services / EndpointSlices / ServersTransport / # Ingresses that route to docker backends) are applied. # migrate: touch SERVICE/k8s/active (+ move routing files up) # rollback: rm SERVICE/k8s/active collect_k8s() { find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ ! -path '*/routing/*' ! -path '*/overlays/*' \ ! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \ ! -name '*values.y*ml' ! -name 'patch-*.y*ml' \ | sort } collect_k8s_inactive() { find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ \( -name 'namespace.y*ml' -o -path '*/routing/*' \) \ ! -path '*/overlays/*' ! -name '*.example.y*ml' \ | sort } mapfile -t compose_stacks < <( find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort ) mapfile -t k8s_manifests < <( for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do if [ -f "$kd/active" ]; then collect_k8s "$kd" else collect_k8s_inactive "$kd" fi done ) echo "== Validate compose stacks ==" for cf in "${compose_stacks[@]}"; do dir=$(dirname "$cf") if [ -f "$dir/k8s/active" ]; then echo " skip (k8s-managed): $dir" continue fi echo " config: $cf" docker compose -f "$cf" config --quiet done echo "== Validate k8s manifests (kubectl dry-run) ==" for m in "${k8s_manifests[@]}"; do echo " apply --dry-run=client $m" kubectl apply --dry-run=client -f "$m" >/dev/null done echo "== Applying Kubernetes manifests ==" ns_files=() other_files=() for m in "${k8s_manifests[@]}"; do case "$m" in */namespace.y?ml) ns_files+=("$m") ;; *) other_files+=("$m") ;; esac done prune_opts=() if [ "${APPLY_PRUNE:-false}" = "true" ]; then prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy) fi if [ "${#ns_files[@]}" -gt 0 ]; then echo " namespaces first: ${ns_files[*]}" kubectl apply -f "${ns_files[@]}" fi if [ "${#other_files[@]}" -gt 0 ]; then echo " resources: ${other_files[*]}" kubectl apply "${prune_opts[@]}" -f "${other_files[@]}" fi echo "== Redeploying docker compose stacks ==" for cf in "${compose_stacks[@]}"; do dir=$(dirname "$cf") if [ -f "$dir/k8s/active" ]; then echo " skip (k8s-managed): $dir" continue fi echo " compose: $dir" if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then docker compose -f "$cf" build docker compose -f "$cf" push fi docker compose -f "$cf" up -d --pull always --remove-orphans done EOF