mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
Two follow-up fixes after the first migration-step deploy revealed
gaps:
1. \`pnpm dlx drizzle-kit\` doesn't work — the drizzle.config.ts file
itself does \`import { defineConfig } from 'drizzle-kit'\`, and
Node's resolver only finds that import via local node_modules,
not pnpm's dlx cache. Reverted to plain \`pnpm exec drizzle-kit\`
and require the workspace to be installed.
2. CD now runs \`pnpm install --filter ./services/<svc>... --frozen-
lockfile --ignore-scripts\` once at the start of the migration
step for every Drizzle service in the deploy. Path-based filter
(not name-based) because our service package names follow no
uniform convention (\`@mana/auth\` vs \`@mana/credits-service\` vs
\`@mana/events\`). pnpm's lockfile cache makes second-and-later
runs near-instant.
3. Dropped the \`--silent\` flag from \`pnpm exec drizzle-kit --version\`
— it isn't a recognised pnpm-exec flag and causes a 254 exit code,
making the script's "is drizzle-kit available?" probe always fail.
Smoke-tested locally — script now runs cleanly against mana-auth's
schema, reports "no changes detected", cleans up the probe SQL file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
636 lines
26 KiB
YAML
636 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/mana-monorepo
|
|
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`.
|
|
SERVICE_SOURCES=(
|
|
"mana-auth|services/mana-auth/"
|
|
"mana-ai|services/mana-ai/"
|
|
"mana-credits|services/mana-credits/"
|
|
"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-notify|services/mana-notify/ packages/shared-go/"
|
|
"mana-crawler|services/mana-crawler/ packages/shared-go/"
|
|
"mana-api-gateway|services/mana-api-gateway/ packages/shared-go/"
|
|
"mana-media|services/mana-media/"
|
|
"mana-llm|services/mana-llm/"
|
|
"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
|