f95463ef50
Root cause: Dual-source architecture for owner password (Gitea secret ENV_OWNER_PASSWORD vs host .env OWNER_PASSWORD) caused drift when the DB was ever re-seeded or the volume recreated. Changes: - Add SeedAudit entity + migration to track one-time seed operations - EnsureDatabaseAsync checks SeedAudit BEFORE seeding — owner is never re-created even if the Users table is wiped - Deploy and rollback workflows now read OWNER_PASSWORD from the host's persistent .env (single source of truth) instead of Gitea secrets - compose.yaml documented: OWNER_PASSWORD only used during initial seed - Cleanup: .gitignore extended for core dumps, changelog/deployment.md updated with 2026-06-20 session notes After this fix the DB is the single source of truth for the owner password after initial seed. The host .env is the single reference for the initial value.
287 lines
14 KiB
YAML
287 lines
14 KiB
YAML
name: Rollback to Previous Version
|
|
run-name: 🔙 Rollback by @${{ gitea.actor }}
|
|
|
|
# ───────────────────────────────────────────────────────
|
|
# Owner: DevOps (Architekt)
|
|
# Trigger: EXCLUSIVELY manual (workflow_dispatch).
|
|
#
|
|
# This workflow reverts the deploy path to the code at a
|
|
# given git tag/ref, then rebuilds and redeploys the stack.
|
|
#
|
|
# Strategy: git checkout <tag> → docker compose up -d --build
|
|
# This is a "full restart rollback" — safest for containerized
|
|
# apps where DB schema changes may need the matching API binary.
|
|
#
|
|
# DB migrations: the API runs MigrateAsync on startup. If the
|
|
# rollback-tag's migration history is a prefix of the current DB,
|
|
# EF Core handles this gracefully (no-op for already-applied
|
|
# migrations). If the tag predates a destructive migration, manual
|
|
# DB intervention is needed — that's an edge case surfaced to DevOps.
|
|
# ───────────────────────────────────────────────────────
|
|
concurrency:
|
|
group: deploy-production
|
|
cancel-in-progress: false
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
target_tag:
|
|
description: 'Git tag to roll back to (e.g. v0.2.49)'
|
|
required: true
|
|
type: string
|
|
confirm:
|
|
description: 'Type "ROLLBACK" to confirm'
|
|
required: true
|
|
type: string
|
|
|
|
jobs:
|
|
rollback:
|
|
name: Rollback Nexus
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
DEPLOY_PATH: /home/projekte_bao/openclaw/data/openclaw/workspace/nexus
|
|
ENV_TMPFILE: /tmp/nexus-rollback-env
|
|
ENV_POSTGRES_PASSWORD: ${{ secrets.ENV_POSTGRES_PASSWORD }}
|
|
ENV_JWT_KEY: ${{ secrets.ENV_JWT_KEY }}
|
|
ENV_OPENCLAW_TOKEN: ${{ secrets.ENV_OPENCLAW_TOKEN }}
|
|
|
|
steps:
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 0: Safety gate — require explicit confirmation
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Safety Gate
|
|
run: |
|
|
if [ "${{ inputs.confirm }}" != "ROLLBACK" ]; then
|
|
echo "❌ Rollback aborted: confirmation string must be 'ROLLBACK'"
|
|
echo " You entered: '${{ inputs.confirm }}'"
|
|
exit 1
|
|
fi
|
|
echo "✅ Rollback confirmed — proceeding to ${{ inputs.target_tag }}"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 1: Checkout target tag
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Checkout target tag
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: refs/tags/${{ inputs.target_tag }}
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 2: Verify tag exists
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Verify tag
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
ACTUAL_TAG=$(git describe --tags --exact-match 2>/dev/null || echo "")
|
|
if [ -z "$ACTUAL_TAG" ]; then
|
|
echo "❌ Tag '${{ inputs.target_tag }}' not found in repository"
|
|
echo " Available tags:"
|
|
git tag -l 'v*' | sort -V | tail -20
|
|
exit 1
|
|
fi
|
|
|
|
echo "✅ Checked out: $ACTUAL_TAG"
|
|
echo " Commit: $(git rev-parse --short HEAD)"
|
|
echo " Message: $(git log -1 --oneline)"
|
|
|
|
# Read version from VERSION file at this tag
|
|
if [ -f VERSION ]; then
|
|
VERSION=$(cat VERSION | tr -d '[:space:]')
|
|
echo " VERSION: $VERSION"
|
|
fi
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 3: Prepare .env from secrets + host .env (safe temp file)
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Prepare .env (secrets + host .env → temp file)
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Read OWNER_PASSWORD from the host's persistent .env
|
|
HOST_OWNER_PASSWORD=""
|
|
if [ -f "${DEPLOY_PATH}/.env" ]; then
|
|
HOST_OWNER_PASSWORD=$(grep '^OWNER_PASSWORD=' "${DEPLOY_PATH}/.env" | cut -d= -f2- || true)
|
|
fi
|
|
if [ -z "${HOST_OWNER_PASSWORD}" ]; then
|
|
echo "❌ OWNER_PASSWORD not found in ${DEPLOY_PATH}/.env"
|
|
exit 1
|
|
fi
|
|
|
|
cat > "${ENV_TMPFILE}" <<EOF
|
|
# Nexus Production Environment — auto-generated by CD pipeline
|
|
POSTGRES_DB=nexus
|
|
POSTGRES_USER=nexus
|
|
POSTGRES_PASSWORD=${ENV_POSTGRES_PASSWORD}
|
|
JWT_KEY=${ENV_JWT_KEY}
|
|
JWT_ISSUER=nexus
|
|
JWT_AUDIENCE=nexus-web
|
|
OWNER_EMAIL=vmbao62@hotmail.de
|
|
OWNER_PASSWORD=${HOST_OWNER_PASSWORD}
|
|
OWNER_DISPLAY_NAME=
|
|
OPENCLAW_BASE_URL=http://host.docker.internal:18789
|
|
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN}
|
|
OPENCLAW_GATEWAY_PASSWORD=
|
|
EOF
|
|
|
|
chmod 600 "${ENV_TMPFILE}"
|
|
echo "✅ .env written to ${ENV_TMPFILE} (mode 600)"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 4: Sync rollback code to host
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Sync code to host
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
docker run --rm \
|
|
-v "${{ gitea.workspace }}:/src:ro" \
|
|
-v "${DEPLOY_PATH}:/dest" \
|
|
alpine:latest \
|
|
sh -c "
|
|
cd /src && \
|
|
find . -mindepth 1 -maxdepth 1 \
|
|
! -name .git \
|
|
-exec cp -r {} /dest/ \; && \
|
|
DEST_OWNER=\$(stat -c '%u:%g' /dest) && \
|
|
chown -R \"\$DEST_OWNER\" /dest
|
|
"
|
|
|
|
echo "✅ Rollback code (${{ inputs.target_tag }}) synced to ${DEPLOY_PATH}"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 5: Rebuild & Redeploy
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Rebuild & Redeploy
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
docker run --rm \
|
|
-v "${DEPLOY_PATH}:/workspace/nexus" \
|
|
-v "/tmp:/tmp-host:ro" \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-w /workspace/nexus \
|
|
docker:cli \
|
|
sh -c "
|
|
set -e
|
|
echo '🔙 Rolling back to ${{ inputs.target_tag }}'
|
|
docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") build --no-cache
|
|
docker compose --env-file /tmp-host/$(basename "${ENV_TMPFILE}") up -d --wait --force-recreate
|
|
"
|
|
|
|
echo "✅ Rollback redeploy completed"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 6: 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 7: Health Check
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Health Check
|
|
run: |
|
|
echo "🏥 Health check after rollback..."
|
|
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
|
|
NEXT=$((WAIT + RETRY))
|
|
[ $NEXT -le 15 ] && WAIT=$NEXT || WAIT=15
|
|
done
|
|
echo "❌ Health check failed after $MAX attempts"
|
|
exit 1
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 8: Smoke Test
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Smoke Test
|
|
run: |
|
|
echo "🔍 Smoke test after rollback..."
|
|
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 "✅ Rollback to ${{ inputs.target_tag }} successful"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 9: Rollback Summary
|
|
# ═══════════════════════════════════════════════════
|
|
- name: Rollback Summary
|
|
if: always()
|
|
run: |
|
|
echo ""
|
|
echo "═══════════════════════════════════════"
|
|
echo " 🔙 Rollback Summary"
|
|
echo "═══════════════════════════════════════"
|
|
echo " Rolled to: ${{ inputs.target_tag }}"
|
|
echo " Triggered: @${{ gitea.actor }}"
|
|
echo " Status: ${{ job.status }}"
|
|
echo "═══════════════════════════════════════"
|
|
|
|
# ═══════════════════════════════════════════════════
|
|
# Step 10: Failure → Reviewer Handoff
|
|
# ═══════════════════════════════════════════════════
|
|
- name: 🔴 Rollback Failed — Reviewer Handoff
|
|
if: failure()
|
|
run: |
|
|
echo ""
|
|
echo "┌─────────────────────────────────────────────────────────────┐"
|
|
echo "│ 🔴 ROLLBACK FAILED — Reviewer muss fixen │"
|
|
echo "├─────────────────────────────────────────────────────────────┤"
|
|
echo "│ │"
|
|
echo "│ Target: ${{ inputs.target_tag }}"
|
|
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 "│ Letzter bekannter funktionierender Stand: │"
|
|
echo "│ → 'git log --oneline -5' zeigt letzte Commits │"
|
|
echo "│ → Manuellen Rollback erwägen: │"
|
|
echo "│ cd /home/projekte_bao/openclaw/data/openclaw/workspace/nexus │"
|
|
echo "│ docker compose up -d (vorheriger Stand) │"
|
|
echo "│ │"
|
|
echo "└─────────────────────────────────────────────────────────────┘"
|