managarten/.github/workflows/cd-macmini.yml
Till JS b1b9bbc269
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Docker Validate / Validate Dockerfiles (push) Waiting to run
Docker Validate / Build calendar-web (push) Blocked by required conditions
Docker Validate / Build quotes-web (push) Blocked by required conditions
Docker Validate / Build todo-backend (push) Blocked by required conditions
Docker Validate / Build todo-web (push) Blocked by required conditions
Docker Validate / Build mana-auth (push) Blocked by required conditions
Docker Validate / Build mana-sync (push) Blocked by required conditions
Docker Validate / Build mana-media (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
chore: rename repo mana-monorepo → managarten
Phase-3-Rename des ehemaligen Multi-App-Monorepos zum eigenständigen
Produkt-Repo. Verein heißt mana e.V., Plattform-Domain bleibt mana.how,
apps/mana/ bleibt unverändert — nur der Repo-Container kriegt den
neuen Namen "managarten" (Garten der mana-Apps).

Geändert:
- package.json#name + #description
- README.md (Titel + erster Absatz)
- TROUBLESHOOTING.md
- alle Mac-Mini-Skripte (Pfade ~/projects/mana-monorepo → ~/projects/managarten)
- COMPOSE_PROJECT_NAME-default in scripts/mac-mini/status.sh
- .github/workflows/cd-macmini.yml + mirror-to-forgejo.yml
- apps/docs (astro.config.mjs + content)
- .claude/settings.local.json (Bash-Permission-Pfade)
- alle docs/*.md Pfad-Referenzen
- launchd plists, .env.macmini.example, infrastructure/

Forgejo-Repo + GitHub-Repo bereits via API umbenannt. Lokales
Verzeichnis-Rename + Mac-Mini-Cutover folgen separat.
2026-05-09 01:16:02 +02:00

641 lines
26 KiB
YAML

# CD Pipeline: Auto-deploy to Mac Mini on push to main
#
# Requires a self-hosted GitHub Actions runner on the Mac Mini.
# Setup: see docs/MAC_MINI_RUNNER_SETUP.md
#
# Flow:
# Push → main : Detects changed services across the FULL push range
# (github.event.before..sha), rebuilds & restarts only
# those containers.
#
# Service → path mapping lives in one array (SERVICE_SOURCES) — add a
# new service by adding one line. The Deploy job reads a single
# `services` output, so no per-service wiring in two places.
name: CD Mac Mini
on:
push:
branches:
- main
workflow_dispatch:
inputs:
service:
description: 'Service to deploy (or "all" for everything)'
required: false
default: 'all'
type: choice
options:
- all
- mana-auth
- mana-ai
- mana-credits
- mana-research
- mana-events
- mana-geocoding
- mana-user
- mana-subscriptions
- mana-analytics
- mana-search
- mana-sync
- mana-notify
- mana-crawler
- mana-api-gateway
- mana-media
- mana-llm
- mana-landing-builder
- mana-web
- mana-api
- manavoxel-web
- memoro-server
- memoro-audio-server
concurrency:
group: cd-macmini
cancel-in-progress: false # Don't cancel in-progress deploys
env:
PROJECT_DIR: /Users/mana/projects/managarten
COMPOSE_FILE: docker-compose.macmini.yml
ENV_FILE: .env.macmini
DOCKER_BUILDKIT: 1
PATH: /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
jobs:
# ===========================================
# Detect what changed
# ===========================================
detect-changes:
name: Detect Changes
runs-on: self-hosted
if: github.event_name == 'push'
outputs:
services: ${{ steps.changes.outputs.services }}
any-changes: ${{ steps.changes.outputs.any-changes }}
steps:
- name: Check for changes
id: changes
run: |
cd "${{ env.PROJECT_DIR }}"
# Compare the full push range, not just the tip. github.event.before
# is the commit main pointed at BEFORE this push — so a push of N
# commits lands with before = HEAD~N. Previously we only looked at
# HEAD~1 HEAD, which made pushes of more than one commit silently
# drop all-but-the-tip's changes.
BEFORE="${{ github.event.before }}"
AFTER="${{ github.sha }}"
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
# First push to the branch / force-push with no shared history.
# Fall back to the last commit only — better than failing open.
echo "No valid 'before' SHA (first push or force reset), using HEAD~1"
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
else
# Make sure we actually have the 'before' commit locally. The
# self-hosted runner works in a long-lived clone that usually does,
# but a forced push to a rebased branch can leave it missing.
if ! git cat-file -e "$BEFORE^{commit}" 2>/dev/null; then
echo "Fetching origin to resolve $BEFORE..."
git fetch origin main --no-tags --quiet || true
fi
CHANGED=$(git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
fi
# Shared packages trigger rebuilds for all web services that import
# them. Go services use packages/shared-go/ and are handled per-svc.
SHARED_CHANGED="false"
if echo "$CHANGED" | grep -qE "^packages/(shared-ui|shared-theme|shared-icons|shared-tailwind|shared-auth|shared-branding|shared-i18n|shared-utils|shared-types|shared-pwa|shared-vite-config)/"; then
SHARED_CHANGED="true"
fi
# Root-level SvelteKit base Dockerfile or pnpm-lock also force a
# web-app rebuild — the per-app Dockerfiles do FROM sveltekit-base.
WEB_BASE_CHANGED="false"
if echo "$CHANGED" | grep -qE "^(docker/Dockerfile.sveltekit-base|pnpm-lock.yaml)$"; then
WEB_BASE_CHANGED="true"
fi
# Service → source paths. One line per compose service. Space-
# separated paths are OR'd: any of them matching triggers a rebuild.
# Keep names aligned with `docker compose config --services`.
#
# NOTE 2026-05-08 — 5 Plattform-Services (mana-auth, mana-credits,
# mana-notify, mana-media, mana-llm) bauen ab dem Cutover aus
# `../mana/services/...`. Ihre Source-Pfade liegen im Schwester-Repo
# `mana/` und werden von diesem `git diff` (das nur das Monorepo
# sieht) NICHT mehr automatisch erkannt. Manuelles Deploy geht
# weiterhin via `workflow_dispatch` mit `service: mana-<x>` —
# `docker compose build` zieht dann aus `../mana/`. Für
# Auto-Detect bei Plattform-Code-Änderungen gehört ein eigener
# CD-Workflow ins `mana/`-Repo (Offener Punkt, Phase 8).
SERVICE_SOURCES=(
"mana-ai|services/mana-ai/"
"mana-research|services/mana-research/"
"mana-events|services/mana-events/"
"mana-geocoding|services/mana-geocoding/"
"mana-user|services/mana-user/"
"mana-subscriptions|services/mana-subscriptions/"
"mana-analytics|services/mana-analytics/"
"mana-search|services/mana-search/ packages/shared-go/"
"mana-sync|services/mana-sync/ packages/shared-go/"
"mana-crawler|services/mana-crawler/ packages/shared-go/"
"mana-api-gateway|services/mana-api-gateway/ packages/shared-go/"
"mana-landing-builder|services/mana-landing-builder/ packages/shared-landing-ui/ packages/shared-types/"
"mana-web|apps/mana/apps/web/ apps/mana/packages/"
"mana-api|apps/api/"
"manavoxel-web|apps/manavoxel/apps/web/ apps/manavoxel/packages/"
"memoro-server|apps/memoro/apps/server/ apps/memoro/packages/"
"memoro-audio-server|apps/memoro/apps/audio-server/"
)
echo "Changed files (first 40):"
echo "$CHANGED" | head -40
echo ""
echo "Shared web packages changed: $SHARED_CHANGED"
echo "SvelteKit base changed: $WEB_BASE_CHANGED"
echo ""
SERVICES=""
for entry in "${SERVICE_SOURCES[@]}"; do
name="${entry%%|*}"
paths="${entry#*|}"
hit="false"
for path in $paths; do
if echo "$CHANGED" | grep -q "^$path"; then
hit="true"
break
fi
done
# Web services also rebuild on shared-package or base-image churn
case "$name" in
*-web|mana-api|mana-landing-builder)
if [ "$SHARED_CHANGED" = "true" ] || [ "$WEB_BASE_CHANGED" = "true" ]; then
hit="true"
fi
;;
esac
if [ "$hit" = "true" ]; then
SERVICES="$SERVICES $name"
echo " ✓ $name"
fi
done
# Trim leading whitespace
SERVICES="${SERVICES# }"
ANY="false"
[ -n "$SERVICES" ] && ANY="true"
echo ""
echo "Deploy list: ${SERVICES:-<none>}"
echo "services=$SERVICES" >> "$GITHUB_OUTPUT"
echo "any-changes=$ANY" >> "$GITHUB_OUTPUT"
# ===========================================
# Deploy changed services
# ===========================================
deploy:
name: Deploy
runs-on: self-hosted
needs: [detect-changes]
if: |
always() &&
(needs.detect-changes.result == 'success' && needs.detect-changes.outputs.any-changes == 'true') ||
github.event_name == 'workflow_dispatch'
steps:
- name: Pull latest code
run: |
cd "${{ env.PROJECT_DIR }}"
git pull origin main
- name: Init deploy tracking
id: init
run: |
cd "${{ env.PROJECT_DIR }}"
source scripts/deploy-metrics.sh
deploy_timer_start
echo "start_epoch=$DEPLOY_START_EPOCH" >> $GITHUB_OUTPUT
ensure_deploy_schema
- name: Ensure env vars exist
run: |
cd "${{ env.PROJECT_DIR }}"
# Add CALENDAR_ENCRYPTION_KEY if not present
if ! grep -q "CALENDAR_ENCRYPTION_KEY" "${{ env.ENV_FILE }}" 2>/dev/null; then
echo "CALENDAR_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> "${{ env.ENV_FILE }}"
echo "Added CALENDAR_ENCRYPTION_KEY to ${{ env.ENV_FILE }}"
fi
- name: Determine services to deploy
id: services
run: |
cd "${{ env.PROJECT_DIR }}"
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
INPUT="${{ inputs.service }}"
if [ "$INPUT" == "all" ]; then
echo "Manual deploy: all services"
echo "deploy-all=true" >> $GITHUB_OUTPUT
echo "services=" >> $GITHUB_OUTPUT
exit 0
fi
echo "services=$INPUT" >> $GITHUB_OUTPUT
echo "deploy-all=false" >> $GITHUB_OUTPUT
echo "Services to deploy: $INPUT"
else
SERVICES="${{ needs.detect-changes.outputs.services }}"
echo "services=$SERVICES" >> $GITHUB_OUTPUT
echo "deploy-all=false" >> $GITHUB_OUTPUT
echo "Services to deploy: $SERVICES"
fi
- name: Build shared base image
run: |
cd "${{ env.PROJECT_DIR }}"
SERVICES="${{ steps.services.outputs.services }}"
DEPLOY_ALL="${{ steps.services.outputs.deploy-all }}"
NEEDS_WEB_BASE=false
if [ "$DEPLOY_ALL" == "true" ]; then
NEEDS_WEB_BASE=true
else
for svc in $SERVICES; do
case "$svc" in *-web|mana-api|mana-landing-builder) NEEDS_WEB_BASE=true; break ;; esac
done
fi
if [ "$NEEDS_WEB_BASE" == "true" ]; then
echo "=== Building shared SvelteKit base image ==="
docker build -f docker/Dockerfile.sveltekit-base -t sveltekit-base:local . 2>&1 | tail -5
echo "SvelteKit base image built"
else
echo "No web apps to deploy, skipping SvelteKit base image"
fi
- name: Build and deploy services
id: build
run: |
cd "${{ env.PROJECT_DIR }}"
source scripts/deploy-metrics.sh
DEPLOY_ALL="${{ steps.services.outputs.deploy-all }}"
SERVICES="${{ steps.services.outputs.services }}"
# Determine final service list
if [ "$DEPLOY_ALL" == "true" ]; then
# Get all service names from compose file
SERVICES=$(docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" config --services | tr '\n' ' ')
echo "=== Rebuilding ALL services ==="
elif [ -z "$SERVICES" ]; then
echo "No services to deploy"
echo "build-times=" >> $GITHUB_OUTPUT
exit 0
else
echo "=== Rebuilding: $SERVICES ==="
fi
# mana-web's Vite build needs 8 GiB of Node heap and Colima's
# VM is sized at 12 GiB. With ~3.5 GiB of other containers
# running, peak RSS occasionally OOMs the build (we hit this
# on 2026-04-28). Pause the non-critical monitoring stack
# for the duration of the build to free ~700 MiB of headroom;
# the trap inside the wrapper restores it on exit, even on
# build failure. No-op if mana-web isn't in $SERVICES.
PAUSE_MONITORING=false
if echo " $SERVICES " | grep -q ' mana-web '; then
PAUSE_MONITORING=true
echo "=== Pausing monitoring stack (mana-web build needs RAM headroom) ==="
./scripts/mac-mini/build-memory-headroom.sh start
fi
# Resume monitoring no matter how the build phase exits.
if [ "$PAUSE_MONITORING" = "true" ]; then
trap './scripts/mac-mini/build-memory-headroom.sh stop' EXIT
fi
# Build each service individually to capture build times
BUILD_TIMES=""
for svc in $SERVICES; do
echo "--- Building $svc ---"
build_start=$(date +%s)
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build "$svc" 2>&1 || true
build_end=$(date +%s)
build_dur=$(( build_end - build_start ))
BUILD_TIMES="$BUILD_TIMES $svc:$build_dur"
echo " $svc built in ${build_dur}s"
done
# Resume monitoring before the migration / start steps run —
# they need cAdvisor + exporters back online to record the
# deploy metrics step further down.
if [ "$PAUSE_MONITORING" = "true" ]; then
./scripts/mac-mini/build-memory-headroom.sh stop
trap - EXIT
fi
# Apply Drizzle schema migrations BEFORE we restart the
# service containers — additive-only, see
# scripts/mac-mini/safe-db-push.sh for the destructive guard.
# If a service has no Drizzle config or no schema diff this is
# a fast no-op. We must source POSTGRES_PASSWORD from the env
# file because the workflow env doesn't carry it.
echo "=== Applying schema migrations ==="
set -a
# shellcheck source=/dev/null
. "$ENV_FILE"
set +a
PG_PASSWORD="${POSTGRES_PASSWORD:-mana123}"
# `drizzle-kit` reads `drizzle.config.ts`, which itself
# `import {defineConfig} from 'drizzle-kit'`. Node's resolver
# only finds that import when the package lives in the local
# node_modules — `pnpm dlx` puts it in the global cache,
# invisible to a from-cwd resolve. So before running any
# migration we install workspace deps for every Drizzle
# service in this deploy. pnpm's lockfile cache makes the
# second-and-later runs near-instant.
DRIZZLE_SVCS=""
for svc in $SERVICES; do
if [ -f "services/$svc/drizzle.config.ts" ] || [ -f "services/$svc/drizzle.config.js" ]; then
DRIZZLE_SVCS="$DRIZZLE_SVCS $svc"
fi
done
if [ -n "$DRIZZLE_SVCS" ]; then
echo "Installing workspace deps for Drizzle services:$DRIZZLE_SVCS"
# Use pnpm's path-based filter (`--filter ./services/<svc>...`)
# because our service package names don't follow a uniform
# convention (`@mana/auth` vs `@mana/credits-service` etc.).
# The trailing `...` includes transitive workspace deps.
FILTER_FLAGS=""
for svc in $DRIZZLE_SVCS; do
FILTER_FLAGS="$FILTER_FLAGS --filter ./services/$svc..."
done
# shellcheck disable=SC2086
pnpm install $FILTER_FLAGS --frozen-lockfile --ignore-scripts 2>&1 | tail -5 || true
fi
# Most services live in mana_platform; mana-sync (Go, no
# Drizzle) and a handful of others use mana_sync. Per-service
# routing is read straight from compose's DATABASE_URL env.
for svc in $SERVICES; do
# Pull the literal DATABASE_URL from the compose definition,
# then swap host postgres → localhost (we run on the host,
# not inside the docker network).
db_url=$(docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" config "$svc" 2>/dev/null \
| awk '/DATABASE_URL:/ {print $2; exit}' \
| sed 's|@postgres:|@localhost:|')
if [ -z "$db_url" ]; then continue; fi
DATABASE_URL="$db_url" PROJECT_DIR="${{ env.PROJECT_DIR }}" \
./scripts/mac-mini/safe-db-push.sh "$svc" || {
echo "[deploy] safe-db-push failed for $svc — aborting before restart"
exit 1
}
done
# Start all services at once (no rebuild, images already built)
echo "=== Starting services ==="
if [ "$DEPLOY_ALL" == "true" ]; then
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d
else
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --no-deps $SERVICES
fi
echo "=== Waiting for services to start ==="
sleep 10
echo "build-times=$BUILD_TIMES" >> $GITHUB_OUTPUT
- name: Health checks
id: health
run: |
cd "${{ env.PROJECT_DIR }}"
source scripts/deploy-metrics.sh
# Service → internal health URL (localhost, post-deploy curl on the
# Mac Mini). Services without an entry here get a "no health endpoint
# configured" skip — check_health_timed handles it gracefully.
health_url_for() {
case "$1" in
mana-auth) echo "http://localhost:3001/health" ;;
mana-ai) echo "http://localhost:3067/health" ;;
mana-credits) echo "http://localhost:3002/health" ;;
mana-research) echo "http://localhost:3068/health" ;;
mana-geocoding) echo "http://localhost:3018/health" ;;
mana-user) echo "http://localhost:3062/health" ;;
mana-subscriptions) echo "http://localhost:3063/health" ;;
mana-analytics) echo "http://localhost:3064/health" ;;
mana-search) echo "http://localhost:3012/health" ;;
mana-sync) echo "http://localhost:3050/health" ;;
mana-notify) echo "http://localhost:3013/health" ;;
mana-crawler) echo "http://localhost:3014/health" ;;
mana-api-gateway) echo "http://localhost:3016/health" ;;
mana-media) echo "http://localhost:3011/health" ;;
mana-llm) echo "http://localhost:3025/health" ;;
mana-events) echo "http://localhost:3065/health" ;;
mana-api) echo "http://localhost:3060/health" ;;
mana-web) echo "http://localhost:5000/health" ;;
manavoxel-web) echo "http://localhost:5028/health" ;;
memoro-server) echo "http://localhost:3015/health" ;;
*) echo "" ;;
esac
}
DEPLOY_ALL="${{ steps.services.outputs.deploy-all }}"
SERVICES="${{ steps.services.outputs.services }}"
# On "deploy all", health-check a representative set (add more as
# endpoints stabilise). Per-push deploys only check what was rebuilt.
if [ "$DEPLOY_ALL" == "true" ]; then
SERVICES="mana-auth mana-ai mana-credits mana-research mana-user mana-subscriptions mana-analytics mana-sync mana-api-gateway mana-api mana-web"
fi
HEALTH_RESULTS=""
echo "=== Health Checks ==="
for svc in $SERVICES; do
url=$(health_url_for "$svc")
if [ -z "$url" ]; then
echo " - $svc: no health endpoint configured"
HEALTH_RESULTS="$HEALTH_RESULTS $svc:skipped:0:0"
continue
fi
result=$(check_health_timed "$svc" "$url" 2>/dev/null) || true
status=$(echo "$result" | awk '{print $1}')
elapsed=$(echo "$result" | awk '{print $2}')
http_code=$(echo "$result" | awk '{print $3}')
if [ -z "$status" ]; then
status="skipped"
elapsed="0"
http_code="0"
fi
if [ "$status" = "ok" ]; then
echo " ✓ $svc: OK (${elapsed}s)"
else
echo " ✗ $svc: $status (HTTP $http_code, ${elapsed}s)"
fi
HEALTH_RESULTS="$HEALTH_RESULTS $svc:$status:$http_code:$elapsed"
done
echo "health-results=$HEALTH_RESULTS" >> $GITHUB_OUTPUT
- name: Record deploy metrics
if: always()
run: |
cd "${{ env.PROJECT_DIR }}"
source scripts/deploy-metrics.sh
START_EPOCH="${{ steps.init.outputs.start_epoch }}"
NOW=$(date +%s)
DURATION=$(( NOW - START_EPOCH ))
# Determine overall status
STATUS="success"
if [ "${{ job.status }}" != "success" ]; then
STATUS="failure"
fi
# Determine services list
DEPLOY_ALL="${{ steps.services.outputs.deploy-all }}"
SERVICES="${{ steps.services.outputs.services }}"
if [ "$DEPLOY_ALL" == "true" ]; then
SERVICES_CSV="all"
else
SERVICES_CSV=$(echo "$SERVICES" | tr ' ' ',')
fi
COMMIT_MSG=$(git log -1 --pretty=%s 2>/dev/null | head -c 200 || echo "unknown")
BRANCH="${{ github.ref_name }}"
# Insert deployment row
DEPLOY_ID=$(insert_deployment \
"${{ github.run_id }}" \
"${{ github.run_attempt }}" \
"${{ github.sha }}" \
"$COMMIT_MSG" \
"$BRANCH" \
"${{ github.event_name }}" \
"${{ github.actor }}" \
"$SERVICES_CSV" \
"$STATUS" 2>/dev/null) || DEPLOY_ID=""
if [ -n "$DEPLOY_ID" ]; then
# Finalise with duration
finalise_deployment "$DEPLOY_ID" "$STATUS" "$DURATION" 2>/dev/null || true
# Helper: lookup value from "key:val key2:val2" string
# Usage: lookup "key" "key:val key2:val2" [field_index] (default: 2nd field)
lookup() {
local needle="$1" haystack="$2" field="${3:-2}"
for item in $haystack; do
if [ "${item%%:*}" = "$needle" ]; then
echo "$item" | cut -d: -f"$field"
return
fi
done
echo ""
}
BUILD_TIMES="${{ steps.build.outputs.build-times }}"
HEALTH_RESULTS="${{ steps.health.outputs.health-results }}"
# Collect unique service names from both build and health data
ALL_SVCS=$(echo "$BUILD_TIMES $HEALTH_RESULTS" | tr ' ' '\n' | cut -d: -f1 | sort -u | tr '\n' ' ')
for svc in $ALL_SVCS; do
[ -z "$svc" ] && continue
build_dur=$(lookup "$svc" "$BUILD_TIMES" 2)
build_dur="${build_dur:-0}"
img_mb=$(get_image_size_mb "$svc" 2>/dev/null || echo "0")
startup=$(lookup "$svc" "$HEALTH_RESULTS" 4)
startup="${startup:-0}"
health=$(lookup "$svc" "$HEALTH_RESULTS" 2)
health="${health:-skipped}"
http_code=$(lookup "$svc" "$HEALTH_RESULTS" 3)
http_code="${http_code:-0}"
insert_deploy_service "$DEPLOY_ID" "$svc" "$build_dur" "$img_mb" "$startup" "$health" "$http_code" 2>/dev/null || true
push_service_metrics "$svc" "$build_dur" "$img_mb" "$health" 2>/dev/null || true
done
fi
# Push overall metrics to Pushgateway
push_deploy_metrics "$STATUS" "$DURATION" "$BRANCH" 2>/dev/null || true
echo "Deploy tracking recorded: status=$STATUS duration=${DURATION}s"
- name: Notify on failure
if: failure()
run: |
cd "${{ env.PROJECT_DIR }}"
SERVICES="${{ steps.services.outputs.services }}"
[ "${{ steps.services.outputs.deploy-all }}" == "true" ] && SERVICES="all"
COMMIT_MSG=$(git log -1 --pretty=%s 2>/dev/null | head -c 100)
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
MSG="⚠️ Deploy failed: ${SERVICES} (commit ${COMMIT_MSG} by ${{ github.actor }}) — ${RUN_URL}"
echo "$MSG"
- name: Cleanup old images
if: always()
run: |
cd "${{ env.PROJECT_DIR }}"
echo "=== Pruning dangling images ==="
docker image prune -f 2>/dev/null || true
echo "=== Pruning unused images older than 7 days ==="
docker image prune -a -f --filter "until=168h" 2>/dev/null || true
- name: Summary
if: always()
run: |
cd "${{ env.PROJECT_DIR }}"
START_EPOCH="${{ steps.init.outputs.start_epoch }}"
NOW=$(date +%s)
DURATION=$(( NOW - START_EPOCH ))
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Duration:** ${DURATION}s" >> $GITHUB_STEP_SUMMARY
echo "**Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.services.outputs.deploy-all }}" == "true" ]; then
echo "**Services:** All" >> $GITHUB_STEP_SUMMARY
else
echo "**Services:** ${{ steps.services.outputs.services }}" >> $GITHUB_STEP_SUMMARY
fi
# Build times table
BUILD_TIMES="${{ steps.build.outputs.build-times }}"
if [ -n "$BUILD_TIMES" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Build Times" >> $GITHUB_STEP_SUMMARY
echo "| Service | Duration |" >> $GITHUB_STEP_SUMMARY
echo "|---------|----------|" >> $GITHUB_STEP_SUMMARY
for entry in $BUILD_TIMES; do
svc="${entry%%:*}"
dur="${entry#*:}"
echo "| $svc | ${dur}s |" >> $GITHUB_STEP_SUMMARY
done
fi
# Health results table
HEALTH_RESULTS="${{ steps.health.outputs.health-results }}"
if [ -n "$HEALTH_RESULTS" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Health Checks" >> $GITHUB_STEP_SUMMARY
echo "| Service | Status | HTTP | Startup |" >> $GITHUB_STEP_SUMMARY
echo "|---------|--------|------|---------|" >> $GITHUB_STEP_SUMMARY
for entry in $HEALTH_RESULTS; do
svc=$(echo "$entry" | cut -d: -f1)
h_status=$(echo "$entry" | cut -d: -f2)
h_code=$(echo "$entry" | cut -d: -f3)
h_time=$(echo "$entry" | cut -d: -f4)
icon="✓"
[ "$h_status" != "ok" ] && icon="✗"
echo "| $svc | $icon $h_status | $h_code | ${h_time}s |" >> $GITHUB_STEP_SUMMARY
done
fi