name: Deploy to Production run-name: ๐Ÿš€ Deploy by @${{ gitea.actor }} # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Owner: DevOps (Architekt) # CD v3 โ€” 2026-06-13 # # Triggers: # 1. AUTOMATIC after successful CI on main (workflow_run) # โ†’ Uses safe defaults: patch bump, all services, main ref. # โ†’ Commits marked with [skip ci] are filtered at job level # (prevents version-bump loops). # 2. MANUAL via workflow_dispatch with full parameter control. # # Concurrency: one deploy at a time. # Queued deploys wait โ€” no race conditions with parallel builds. # # Version Management: # The VERSION file in the repo root is the single source of truth. # Version bumps happen in the Dev workflow BEFORE merge to main. # The deploy workflow only reads, validates, and logs the version. # The [skip ci] filter remains as a safety layer for auto-triggers. # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ concurrency: group: deploy-production cancel-in-progress: false on: # โ”€โ”€ Auto-Trigger: after successful CI on main โ”€โ”€ workflow_run: workflows: ["CI - Build & Test"] types: [completed] branches: [main] # โ”€โ”€ Manual Trigger (full control) โ”€โ”€ workflow_dispatch: inputs: service: description: 'Service to deploy (empty = all)' required: false default: '' type: string no_cache: description: 'Disable Docker build cache' required: false default: false type: boolean git_ref: description: 'Git ref to deploy (branch, tag, or commit SHA; default: main)' required: false default: 'main' type: string jobs: deploy: name: Deploy Nexus runs-on: ubuntu-latest if: | (github.event_name == 'workflow_dispatch') || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && !contains(github.event.workflow_run.head_commit.message, '[skip ci]')) # โ”€โ”€ Env for the deploy target path โ”€โ”€ env: DEPLOY_PATH: /opt/openclaw/data/openclaw/workspace/nexus ENV_TMPFILE: /tmp/nexus-deploy-env ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }} ENV_JWT_KEY: ${{ secrets.ENV_JWT_KEY }} ENV_OWNER_PASSWORD: ${{ secrets.ENV_OWNER_PASSWORD }} ENV_OPENCLAW_TOKEN: ${{ secrets.ENV_OPENCLAW_TOKEN }} steps: # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 1: Checkout # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || 'main' }} fetch-depth: 0 fetch-tags: true # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 2: Set up Git identity # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Configure Git run: | git config user.email "devops@noveria.net" git config user.name "DevOps" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 3: Resolve deploy version # # Reads VERSION from repo root โ€” the single source of truth. # Validates semver format, logs version + git metadata. # No git mutation: version bumps happen in the Dev workflow. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Resolve Version id: version run: | set -euo pipefail # 1. Check VERSION exists if [ ! -f VERSION ]; then echo "โŒ VERSION file not found" exit 1 fi # 2. Read and validate semver format VERSION=$(cat VERSION | tr -d '[:space:]') if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then echo "โŒ Invalid semver in VERSION: '$VERSION'" exit 1 fi # 3. Log version, git ref, and describe GIT_REF=$(git rev-parse --short HEAD) GIT_DESCRIBE=$(git describe --always --dirty) echo "๐Ÿ“ฆ Deploy version: v${VERSION}" echo "๐Ÿ”– Git ref: ${GIT_REF}" echo "๐Ÿท๏ธ Git describe: ${GIT_DESCRIBE}" # 4. Set outputs for downstream steps echo "version=${VERSION}" >> "$GITEA_OUTPUT" echo "mutated_main=false" >> "$GITEA_OUTPUT" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 4: Build .env from secrets (SAFE) # # Secrets are written to /tmp/nexus-deploy-env โ€” NEVER # to a file inside the workspace that gets rsync'd to # the host. The temp file is deleted immediately after # compose operations complete. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Prepare .env (secrets โ†’ temp file) run: | set -euo pipefail cat > "${ENV_TMPFILE}" < /tmp/nexus-deploy-env if [ -n '${SERVICE_ARG}' ]; then echo '๐Ÿš€ Deploying service: ${SERVICE_ARG}' docker compose --env-file /tmp/nexus-deploy-env build ${BUILD_ARGS} ${SERVICE_ARG} docker compose --env-file /tmp/nexus-deploy-env up -d --wait --force-recreate ${SERVICE_ARG} else echo '๐Ÿš€ Deploying all services' docker compose --env-file /tmp/nexus-deploy-env build ${BUILD_ARGS} docker compose --env-file /tmp/nexus-deploy-env up -d --wait --force-recreate fi " < "${ENV_TMPFILE}" echo "โœ… Docker compose up completed" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 7: Clean up temp .env # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Clean up temp .env if: always() run: | if [ -f "${ENV_TMPFILE}" ]; then shred -u "${ENV_TMPFILE}" 2>/dev/null || rm -f "${ENV_TMPFILE}" echo "๐Ÿงน Temp .env removed" fi # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 8: Health Check (exponential backoff) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Health Check run: | echo "๐Ÿฅ Health check..." RETRY=0 MAX=6 WAIT=1 while [ $RETRY -lt $MAX ]; do RETRY=$((RETRY + 1)) if curl -sf --max-time 10 https://nexus.noveria.net/health; then echo "" echo "โœ… Health check passed (attempt $RETRY/$MAX)" exit 0 fi echo "โณ Attempt $RETRY/$MAX failed, waiting ${WAIT}s..." sleep $WAIT # Fibonacci-ish backoff: 1,2,3,5,8,13 NEXT=$((WAIT + RETRY)) [ $NEXT -le 15 ] && WAIT=$NEXT || WAIT=15 done echo "โŒ Health check failed after $MAX attempts" exit 1 # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 9: Smoke Test # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Smoke Test run: | echo "๐Ÿ” Smoke test..." PASS=0 FAIL=0 BASE="https://nexus.noveria.net" check() { local path="$1" label="$2" expected="${3:-200}" local code code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${BASE}${path}") printf " %-25s HTTP %s" "${label}:" "${code}" if [ "$code" = "$expected" ]; then echo " โœ…" PASS=$((PASS + 1)) else echo " โŒ (expected $expected)" FAIL=$((FAIL + 1)) fi } check "/dashboard" "Dashboard" 200 check "/health" "Health API" 200 check "/api/v1/operations/snapshot" "Operations API (auth)" 401 echo "" echo "Results: $PASS passed, $FAIL failed" if [ "$FAIL" -gt 0 ]; then echo "โŒ Smoke test failed!" exit 1 fi echo "โœ… Smoke test passed โ€” v${{ steps.version.outputs.version }} is live" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 10: Deployment Summary # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: Deployment Summary if: always() run: | TRIGGER="${{ github.event_name == 'workflow_run' && 'Auto (CI success)' || 'Manual (workflow_dispatch)' }}" echo "" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo " ๐Ÿ“ฆ Deploy Summary" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo " Version: v${{ steps.version.outputs.version }}" echo " Git ref: ${{ github.event_name == 'workflow_dispatch' && inputs.git_ref || 'main' }}" echo " Service: ${{ github.event_name == 'workflow_dispatch' && inputs.service || 'all' }}" echo " Trigger: ${TRIGGER}" echo " Actor: @${{ gitea.actor }}" echo " Status: ${{ job.status }}" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # Step 11: Failure โ†’ Reviewer Handoff # # On failure: DevOps (Architekt) analyses the log, # notifies Reviewer (Code-Fixer) with the exact error. # This output provides a ready-to-copy message. # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• - name: ๐Ÿ”ด Failure โ€” Reviewer Handoff if: failure() run: | echo "" echo "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”" echo "โ”‚ ๐Ÿ”ด DEPLOY FAILED โ€” Reviewer muss fixen โ”‚" echo "โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค" echo "โ”‚ โ”‚" echo "โ”‚ Version: v${{ steps.version.outputs.version }}" echo "โ”‚ Job: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}" echo "โ”‚ โ”‚" echo "โ”‚ โ†’ DevOps (Architekt) analysiert den Fehler โ”‚" echo "โ”‚ โ†’ Reviewer (Code-Fixer) behebt das Problem โ”‚" echo "โ”‚ โ†’ DevOps verifiziert mit neuem Deploy โ”‚" echo "โ”‚ โ”‚" echo "โ”‚ Rollback: Trigger 'Rollback to Previous Version' โ”‚" echo "โ”‚ workflow manuell in Gitea Actions. โ”‚" echo "โ”‚ โ”‚" echo "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜"