feat(ci): add CD pipeline with self-hosted runner for Mac Mini auto-deploy

Adds a GitHub Actions workflow that detects changed services on push to
main and automatically rebuilds/restarts only the affected Docker containers
on the Mac Mini. Includes setup guide for the self-hosted runner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-11 11:36:23 +01:00
parent f784612f83
commit e926a39818
2 changed files with 366 additions and 0 deletions

247
.github/workflows/cd-macmini.yml vendored Normal file
View file

@ -0,0 +1,247 @@
# 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 → rebuilds & restarts only those containers
#
# The runner executes directly on the Mac Mini, so it has access to
# Docker, docker-compose, and the local project directory.
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
- matrix-web
- mana-core-auth
- chat-backend
- chat-web
- todo-backend
- todo-web
- calendar-backend
- calendar-web
- clock-backend
- clock-web
- contacts-backend
- contacts-web
- matrix-mana-bot
concurrency:
group: cd-macmini
cancel-in-progress: false # Don't cancel in-progress deploys
env:
PROJECT_DIR: /Users/till/projects/manacore-monorepo
COMPOSE_FILE: docker-compose.macmini.yml
ENV_FILE: .env.macmini
jobs:
# ===========================================
# Detect what changed
# ===========================================
detect-changes:
name: Detect Changes
runs-on: self-hosted
if: github.event_name == 'push'
outputs:
matrix-web: ${{ steps.changes.outputs.matrix-web }}
mana-core-auth: ${{ steps.changes.outputs.mana-core-auth }}
chat-backend: ${{ steps.changes.outputs.chat-backend }}
chat-web: ${{ steps.changes.outputs.chat-web }}
todo-backend: ${{ steps.changes.outputs.todo-backend }}
todo-web: ${{ steps.changes.outputs.todo-web }}
calendar-backend: ${{ steps.changes.outputs.calendar-backend }}
calendar-web: ${{ steps.changes.outputs.calendar-web }}
clock-backend: ${{ steps.changes.outputs.clock-backend }}
clock-web: ${{ steps.changes.outputs.clock-web }}
contacts-backend: ${{ steps.changes.outputs.contacts-backend }}
contacts-web: ${{ steps.changes.outputs.contacts-web }}
matrix-mana-bot: ${{ steps.changes.outputs.matrix-mana-bot }}
any-changes: ${{ steps.changes.outputs.any-changes }}
steps:
- name: Check for changes
id: changes
run: |
cd "${{ env.PROJECT_DIR }}"
# Get changed files between previous and current commit
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
# Shared packages trigger rebuilds for all services that use them
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)/"; then
SHARED_CHANGED="true"
fi
check_changes() {
local name=$1
shift
local result="false"
for path in "$@"; do
if echo "$CHANGED" | grep -q "^$path"; then
result="true"
break
fi
done
# Shared package changes trigger rebuild
if [ "$SHARED_CHANGED" == "true" ]; then
result="true"
fi
echo "$name=$result" >> $GITHUB_OUTPUT
echo " $name: $result"
}
echo "Changed files:"
echo "$CHANGED" | head -20
echo ""
echo "Shared packages changed: $SHARED_CHANGED"
echo ""
check_changes "matrix-web" "apps/matrix/apps/web/" "apps/matrix/packages/"
check_changes "mana-core-auth" "services/mana-core-auth/"
check_changes "chat-backend" "apps/chat/apps/backend/" "apps/chat/packages/"
check_changes "chat-web" "apps/chat/apps/web/" "apps/chat/packages/"
check_changes "todo-backend" "apps/todo/apps/backend/" "apps/todo/packages/"
check_changes "todo-web" "apps/todo/apps/web/" "apps/todo/packages/"
check_changes "calendar-backend" "apps/calendar/apps/backend/" "apps/calendar/packages/"
check_changes "calendar-web" "apps/calendar/apps/web/" "apps/calendar/packages/"
check_changes "clock-backend" "apps/clock/apps/backend/" "apps/clock/packages/"
check_changes "clock-web" "apps/clock/apps/web/" "apps/clock/packages/"
check_changes "contacts-backend" "apps/contacts/apps/backend/" "apps/contacts/packages/"
check_changes "contacts-web" "apps/contacts/apps/web/" "apps/contacts/packages/"
check_changes "matrix-mana-bot" "services/matrix-mana-bot/" "packages/matrix-bot-common/"
# Check if anything needs deploying
ANY="false"
for svc in matrix-web mana-core-auth chat-backend chat-web todo-backend todo-web calendar-backend calendar-web clock-backend clock-web contacts-backend contacts-web matrix-mana-bot; do
val=$(grep "^$svc=" $GITHUB_OUTPUT | tail -1 | cut -d= -f2)
if [ "$val" == "true" ]; then
ANY="true"
break
fi
done
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: Determine services to deploy
id: services
run: |
cd "${{ env.PROJECT_DIR }}"
SERVICES=""
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
exit 0
else
SERVICES="$INPUT"
fi
else
# Build list from detected changes
if [ "${{ needs.detect-changes.outputs.matrix-web }}" == "true" ]; then SERVICES="$SERVICES matrix-web"; fi
if [ "${{ needs.detect-changes.outputs.mana-core-auth }}" == "true" ]; then SERVICES="$SERVICES mana-auth"; fi
if [ "${{ needs.detect-changes.outputs.chat-backend }}" == "true" ]; then SERVICES="$SERVICES chat-backend"; fi
if [ "${{ needs.detect-changes.outputs.chat-web }}" == "true" ]; then SERVICES="$SERVICES chat-web"; fi
if [ "${{ needs.detect-changes.outputs.todo-backend }}" == "true" ]; then SERVICES="$SERVICES todo-backend"; fi
if [ "${{ needs.detect-changes.outputs.todo-web }}" == "true" ]; then SERVICES="$SERVICES todo-web"; fi
if [ "${{ needs.detect-changes.outputs.calendar-backend }}" == "true" ]; then SERVICES="$SERVICES calendar-backend"; fi
if [ "${{ needs.detect-changes.outputs.calendar-web }}" == "true" ]; then SERVICES="$SERVICES calendar-web"; fi
if [ "${{ needs.detect-changes.outputs.clock-backend }}" == "true" ]; then SERVICES="$SERVICES clock-backend"; fi
if [ "${{ needs.detect-changes.outputs.clock-web }}" == "true" ]; then SERVICES="$SERVICES clock-web"; fi
if [ "${{ needs.detect-changes.outputs.contacts-backend }}" == "true" ]; then SERVICES="$SERVICES contacts-backend"; fi
if [ "${{ needs.detect-changes.outputs.contacts-web }}" == "true" ]; then SERVICES="$SERVICES contacts-web"; fi
if [ "${{ needs.detect-changes.outputs.matrix-mana-bot }}" == "true" ]; then SERVICES="$SERVICES matrix-mana-bot"; fi
fi
echo "services=$SERVICES" >> $GITHUB_OUTPUT
echo "deploy-all=false" >> $GITHUB_OUTPUT
echo "Services to deploy: $SERVICES"
- name: Deploy all services
if: steps.services.outputs.deploy-all == 'true'
run: |
cd "${{ env.PROJECT_DIR }}"
echo "=== Rebuilding and restarting ALL services ==="
docker compose -f "${{ env.COMPOSE_FILE }}" --env-file "${{ env.ENV_FILE }}" up -d --build
echo "=== Waiting for services to start ==="
sleep 15
docker compose -f "${{ env.COMPOSE_FILE }}" ps
- name: Deploy changed services
if: steps.services.outputs.deploy-all == 'false' && steps.services.outputs.services != ''
run: |
cd "${{ env.PROJECT_DIR }}"
SERVICES="${{ steps.services.outputs.services }}"
echo "=== Rebuilding: $SERVICES ==="
docker compose -f "${{ env.COMPOSE_FILE }}" --env-file "${{ env.ENV_FILE }}" up -d --build --no-deps $SERVICES
echo "=== Waiting for services to start ==="
sleep 10
- name: Health checks
run: |
cd "${{ env.PROJECT_DIR }}"
check_health() {
local name=$1
local url=$2
local status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$url" 2>/dev/null || echo "000")
if [ "$status" == "200" ]; then
echo " ✓ $name: OK"
else
echo " ✗ $name: FAILED (HTTP $status)"
fi
}
echo "=== Health Checks ==="
check_health "Auth API" "http://localhost:3001/health"
check_health "Matrix Web" "http://localhost:5180/health"
check_health "Chat Backend" "http://localhost:3030/health"
check_health "Chat Web" "http://localhost:5010/health"
check_health "Todo Backend" "http://localhost:3031/health"
check_health "Todo Web" "http://localhost:5011/health"
check_health "Calendar Backend" "http://localhost:3032/health"
check_health "Calendar Web" "http://localhost:5012/health"
check_health "Clock Backend" "http://localhost:3033/health"
check_health "Clock Web" "http://localhost:5013/health"
- name: Summary
run: |
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
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

View file

@ -0,0 +1,119 @@
# GitHub Actions Self-Hosted Runner auf dem Mac Mini einrichten
Diese Anleitung auf dem Mac Mini ausführen.
## 1. Runner-Token holen
1. Gehe zu: https://github.com/Memo-2023/manacore-monorepo/settings/actions/runners/new
2. Wähle **macOS** und **ARM64**
3. Kopiere den Token aus dem `--token` Parameter (sieht so aus: `AXXXX...`)
## 2. Runner installieren
```bash
# Verzeichnis erstellen
mkdir -p ~/actions-runner && cd ~/actions-runner
# Runner herunterladen (ARM64 macOS)
curl -o actions-runner.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-osx-arm64-2.322.0.tar.gz
# Entpacken
tar xzf actions-runner.tar.gz
# Konfigurieren (Token von Schritt 1 einsetzen)
./config.sh --url https://github.com/Memo-2023/manacore-monorepo --token DEIN_TOKEN_HIER
# Bei den Prompts:
# Runner group: [Enter] (default)
# Runner name: mac-mini
# Labels: [Enter] (default: self-hosted,macOS,ARM64)
# Work folder: [Enter] (default: _work)
```
## 3. Als LaunchAgent einrichten (Autostart)
```bash
# Installiert den LaunchAgent automatisch
cd ~/actions-runner
./svc.sh install
# Starten
./svc.sh start
# Status prüfen
./svc.sh status
```
## 4. Prüfen ob es läuft
```bash
# Lokal prüfen
./svc.sh status
# Oder im Browser:
# https://github.com/Memo-2023/manacore-monorepo/settings/actions/runners
# → Runner "mac-mini" sollte als "Idle" angezeigt werden
```
## 5. Docker-Zugriff sicherstellen
Der Runner braucht Zugriff auf Docker:
```bash
# Prüfen ob Docker läuft
docker info
# Prüfen ob docker compose funktioniert
docker compose version
# Prüfen ob das Projekt-Verzeichnis existiert
ls ~/projects/manacore-monorepo/docker-compose.macmini.yml
```
## 6. Test-Deployment auslösen
Entweder:
- Push auf `main` machen (deployt automatisch geänderte Services)
- Oder manuell: https://github.com/Memo-2023/manacore-monorepo/actions/workflows/cd-macmini.yml → "Run workflow" → Service wählen
## Fehlerbehebung
### Runner ist offline
```bash
cd ~/actions-runner
./svc.sh status
./svc.sh stop
./svc.sh start
```
### Runner-Token abgelaufen
```bash
cd ~/actions-runner
./svc.sh stop
./config.sh remove --token ALTES_TOKEN
# Neuen Token holen (siehe Schritt 1)
./config.sh --url https://github.com/Memo-2023/manacore-monorepo --token NEUES_TOKEN
./svc.sh start
```
### Logs ansehen
```bash
# Runner-Logs
tail -f ~/actions-runner/_diag/Runner_*.log
# Workflow-Logs: im GitHub UI unter Actions-Tab
```
## Was passiert nach dem Setup?
Bei jedem Push auf `main`:
1. Der Runner erkennt welche Services sich geändert haben
2. Pullt den neuesten Code (`git pull`)
3. Baut nur die geänderten Docker-Container neu (`docker compose up -d --build <service>`)
4. Führt Health Checks durch
5. Ergebnis ist im GitHub Actions-Tab sichtbar
Manuelle Deploys sind jederzeit möglich über den "Run workflow" Button im Actions-Tab.