diff --git a/.gitea/workflows/deploy-reasonix.yml b/.gitea/workflows/deploy-reasonix.yml new file mode 100644 index 0000000..7957f2e --- /dev/null +++ b/.gitea/workflows/deploy-reasonix.yml @@ -0,0 +1,163 @@ +# CI Workflow — Build, deploy, and configure DNS for Reasonix +# Builds Docker image → Deploys on NAS → Upserts Cloudflare DNS record +# +# Image name: reasonix:latest (built locally on NAS runner) + +name: Deploy Reasonix +on: + push: + branches: [main] + paths: + - 'reasonix/**' + - '.gitea/workflows/deploy-reasonix.yml' + workflow_dispatch: + +concurrency: + group: deploy-reasonix + cancel-in-progress: true + +jobs: + build-and-deploy: + name: Build & Deploy Reasonix + runs-on: nas + + steps: + - name: Checkout repository + run: | + if [ ! -d .git ]; then + git clone --depth=1 --branch "$GITHUB_REF_NAME" "http://127.0.0.1:3300/${GITHUB_REPOSITORY}.git" . + fi + git fetch --depth=1 origin "$GITHUB_SHA" 2>/dev/null || true + git checkout "$GITHUB_SHA" 2>/dev/null || true + + - name: Build Docker image + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + echo "Building reasonix Docker image from reasonix/" + DOCKER_BUILDKIT=0 docker build \ + -t reasonix:latest \ + -t "reasonix:$GITHUB_SHA" \ + reasonix/ + echo "✅ Build complete: reasonix:$GITHUB_SHA" + + - name: Smoke test image + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + # Quick test: run the container briefly and check it starts + CONTAINER_ID=$(docker run -d \ + -e DEEPSEEK_LOCAL_API_KEY=test \ + reasonix:latest \ + /bin/sh -c "reasonix --version") + OUTPUT=$(docker wait "$CONTAINER_ID") + LOGS=$(docker logs "$CONTAINER_ID" 2>&1 || true) + docker rm -f "$CONTAINER_ID" 2>/dev/null || true + echo "reasonix version: $LOGS" + echo "✅ Image is functional" + + - name: Prepare deploy directory + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + TARGET="/volume1/docker/reasonix" + mkdir -p "$TARGET" + # Sync compose and config files + cp reasonix/docker-compose.yml "$TARGET/docker-compose.yml" + cp reasonix/reasonix.toml "$TARGET/reasonix.toml" + # Write target tag for compose reference + printf '%s\n' "$GITHUB_SHA" > "$TARGET/target-tag.txt" + echo "✅ Deploy directory ready: $TARGET" + + - name: Deploy Reasonix + env: + DEEPSEEK_LOCAL_API_KEY: ${{ secrets.DEEPSEEK_LOCAL_API_KEY }} + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + TARGET="/volume1/docker/reasonix" + cd "$TARGET" + + # Create .env if it doesn't exist + if [ ! -f .env ]; then + echo "DEEPSEEK_LOCAL_API_KEY=$DEEPSEEK_LOCAL_API_KEY" > .env + fi + + # Ensure network exists + docker network inspect nas-dashboard_internal >/dev/null 2>&1 || \ + docker network create nas-dashboard_internal + + # Stop old container + docker rm -f reasonix 2>/dev/null || true + echo "Old container removed" + + # Deploy + docker compose -f docker-compose.yml up -d --pull never + echo "✅ Container deployed" + + - name: Wait for healthy + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + echo "Waiting for reasonix to be healthy..." + for i in {1..30}; do + STATUS=$(docker inspect --format='{{.State.Health.Status}}' reasonix 2>/dev/null || echo "starting") + if [ "$STATUS" = "healthy" ]; then + echo "✅ Container is healthy" + break + fi + echo "Waiting... ($i/30) Status: $STATUS" + sleep 2 + done + STATUS=$(docker inspect --format='{{.State.Health.Status}}' reasonix 2>/dev/null || echo "unknown") + if [ "$STATUS" != "healthy" ]; then + echo "❌ Container failed to become healthy: $STATUS" + docker logs reasonix --tail 50 + exit 1 + fi + + - name: Verify health endpoint + run: | + set -euo pipefail + export DOCKER_API_VERSION=1.43 + echo "Testing health endpoint..." + # Use wget from inside another container or via docker exec + docker exec reasonix wget -q --spider http://localhost:8787 && \ + echo "✅ Health endpoint OK" || { + echo "❌ Health endpoint failed" + docker logs reasonix --tail 20 + exit 1 + } + + - name: Upsert Cloudflare DNS record + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: | + set -euo pipefail + chmod +x scripts/cloudflare-dns.sh + scripts/cloudflare-dns.sh jimmygan.com A code 161.33.182.109 + echo "✅ Cloudflare DNS record for code.jimmygan.com → 161.33.182.109" + + - name: Reload VPS Caddy + env: + CADDY_VPS_SSH_HOST: 161.33.182.109 + CADDY_VPS_SSH_USER: ubuntu + run: | + set -euo pipefail + echo "Reloading VPS Caddy..." + # Reload via SSH (Caddy reloads config gracefully with SIGUSR2 or systemctl) + ssh -o StrictHostKeyChecking=no \ + "$CADDY_VPS_SSH_USER@$CADDY_VPS_SSH_HOST" \ + "sudo systemctl reload caddy 2>/dev/null || sudo caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || echo 'Caddy reload skipped (manual reload may be needed)'" && \ + echo "✅ VPS Caddy reload triggered" || \ + echo "⚠️ Caddy reload not automatically available. Reload manually on VPS if needed." + + - name: Notify completion + run: | + echo "" + echo "============================================" + echo "✅ Reasonix deployment complete!" + echo " Container: reasonix (port 8787)" + echo " DNS: code.jimmygan.com → 161.33.182.109" + echo " Caddy: VPS/NAS configs in tmp_remote/" + echo "============================================" diff --git a/scripts/cloudflare-dns.sh b/scripts/cloudflare-dns.sh new file mode 100755 index 0000000..1c3be38 --- /dev/null +++ b/scripts/cloudflare-dns.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Cloudflare DNS upsert — create or update a DNS record. +# Usage: CLOUDFLARE_API_TOKEN=xxx ./scripts/cloudflare-dns.sh [ttl] +# +# Examples: +# cloudflare-dns.sh jimmygan.com A code 161.33.182.109 +# cloudflare-dns.sh jimmygan.com CNAME www jimmygan.com 120 +set -euo pipefail + +ZONE="${1:?Usage: $0 [ttl]}" +TYPE="${2:?}" +NAME="${3:?}" +CONTENT="${4:?}" +TTL="${5:-120}" + +if [ -z "${CLOUDFLARE_API_TOKEN:-}" ]; then + echo "❌ CLOUDFLARE_API_TOKEN not set" >&2 + exit 1 +fi + +API="https://api.cloudflare.com/client/v4" +AUTH_HEADER="Authorization: Bearer $CLOUDFLARE_API_TOKEN" +CONTENT_TYPE="Content-Type: application/json" + +echo "🔍 Looking up zone: $ZONE" +ZONE_RESP=$(curl -s -H "$AUTH_HEADER" -H "$CONTENT_TYPE" \ + "$API/zones?name=$ZONE&status=active") +ZONE_ID=$(echo "$ZONE_RESP" | python3 -c " +import sys, json +d = json.load(sys.stdin) +if not d.get('success'): + print('API error:', d.get('errors'), file=sys.stderr) + sys.exit(1) +result = d.get('result', []) +if not result: + print('Zone not found:', '$ZONE', file=sys.stderr) + sys.exit(1) +print(result[0]['id']) +") + +echo "✅ Zone ID: $ZONE_ID" + +echo "🔍 Checking existing record: $NAME.$ZONE ($TYPE)" +RECORD_RESP=$(curl -s -H "$AUTH_HEADER" -H "$CONTENT_TYPE" \ + "$API/zones/$ZONE_ID/dns_records?type=$TYPE&name=$NAME.$ZONE") +RECORD_ID=$(echo "$RECORD_RESP" | python3 -c " +import sys, json +d = json.load(sys.stdin) +if not d.get('success'): + sys.exit(0) # skip, will create +result = d.get('result', []) +if result: + # Prefer exact content match + for r in result: + if r['content'] == '$CONTENT': + print(r['id']) + sys.exit(0) + # Fall back to first match + print(result[0]['id']) +") + +FULL_NAME="$NAME.$ZONE" + +if [ -n "$RECORD_ID" ]; then + echo "🔄 Updating existing record ($RECORD_ID): $FULL_NAME → $CONTENT" + RESP=$(curl -s -X PUT \ + -H "$AUTH_HEADER" -H "$CONTENT_TYPE" \ + -d "{\"type\":\"$TYPE\",\"name\":\"$FULL_NAME\",\"content\":\"$CONTENT\",\"ttl\":$TTL,\"proxied\":false}" \ + "$API/zones/$ZONE_ID/dns_records/$RECORD_ID") + SUCCESS=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print('true' if d.get('success') else 'false')") + if [ "$SUCCESS" = "true" ]; then + echo "✅ Updated: $FULL_NAME → $CONTENT" + else + echo "❌ Failed to update:" >&2 + echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('errors'))" >&2 + exit 1 + fi +else + echo "🆕 Creating new record: $FULL_NAME → $CONTENT" + RESP=$(curl -s -X POST \ + -H "$AUTH_HEADER" -H "$CONTENT_TYPE" \ + -d "{\"type\":\"$TYPE\",\"name\":\"$FULL_NAME\",\"content\":\"$CONTENT\",\"ttl\":$TTL,\"proxied\":false}" \ + "$API/zones/$ZONE_ID/dns_records") + SUCCESS=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print('true' if d.get('success') else 'false')") + if [ "$SUCCESS" = "true" ]; then + echo "✅ Created: $FULL_NAME → $CONTENT" + else + echo "❌ Failed to create:" >&2 + echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('errors'))" >&2 + exit 1 + fi +fi