ci(reasonix): add deploy workflow + Cloudflare DNS upsert script
Deploy Reasonix / Build & Deploy Reasonix (push) Has been cancelled

This commit is contained in:
Gan, Jimmy
2026-07-11 05:31:38 +08:00
parent a334f55575
commit 95daf1f167
2 changed files with 255 additions and 0 deletions
+163
View File
@@ -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 "============================================"
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# Cloudflare DNS upsert — create or update a DNS record.
# Usage: CLOUDFLARE_API_TOKEN=xxx ./scripts/cloudflare-dns.sh <zone> <type> <name> <content> [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 <zone> <type> <name> <content> [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