Files
openrabbit/.gitea/workflows/deploy.yml
latte 8cadb2d216
Some checks failed
Docker / docker (push) Successful in 6s
Security / security (push) Successful in 6s
Deploy / deploy-local-runner (push) Has been cancelled
CI / ci (push) Successful in 1m42s
Deploy / deploy-ssh (push) Successful in 7s
Add Gitea Actions workflows, CI config, and docs
2026-02-28 20:40:14 +01:00

452 lines
18 KiB
YAML

# =============================================================================
# Deploy Workflow — Automated Deployment to VPS
# =============================================================================
#
# PURPOSE:
# Deploy your application to a VPS after a successful push to the default
# branch (main). Supports two deployment modes:
#
# (A) local-runner — The deploy job runs directly on a self-hosted act_runner
# installed ON the VPS. No SSH needed. The runner is selected by a label
# (DEPLOY_RUNNER_LABEL). This is the recommended mode.
#
# (B) ssh — The deploy job runs on any runner and SSHs into the VPS to
# execute commands remotely. Requires SSH secrets. Use as fallback when
# you can't install a runner on the target VPS.
#
# SAFE BY DEFAULT:
# ENABLE_DEPLOY=false in .ci/config.env. Deploy never runs unless you
# explicitly enable it. It also never runs on pull_request events.
#
# DEPLOY STRATEGIES:
# compose — docker compose pull && docker compose up -d
# systemd — systemctl restart <service>
# script — run a custom deploy script
#
# TRIGGERS:
# - push to DEFAULT_BRANCH (main) → deploy if enabled
# - tag v* (only if DEPLOY_ON_TAG=true) → deploy if enabled
# - pull_request → NEVER (not in trigger list)
#
# REQUIRED SECRETS (ssh mode only):
# DEPLOY_SSH_KEY — private SSH key (ed25519 or RSA)
# DEPLOY_HOST — VPS hostname or IP
# DEPLOY_USER — SSH username on VPS
# DEPLOY_KNOWN_HOSTS — (optional) known_hosts entry for the VPS
#
# For local-runner mode: NO secrets needed. The runner already has local
# access. Just ensure the runner is registered with the correct label.
#
# See docs/DEPLOY.md for full setup instructions.
# =============================================================================
name: Deploy
# ---------------------------------------------------------------------------
# TRIGGERS
# ---------------------------------------------------------------------------
# Only push events — never pull_request.
# Branch filter is further enforced in the "branch guard" step below,
# because config.env may specify a different DEFAULT_BRANCH.
on:
push:
branches:
- main
tags:
- "v*"
# =============================================================================
# JOB: deploy-local-runner
# =============================================================================
# Runs directly on the VPS via a labeled self-hosted act_runner.
# This job is skipped if DEPLOY_MODE != local-runner.
# ---------------------------------------------------------------------------
jobs:
deploy-local-runner:
# -------------------------------------------------------------------------
# Runner selection:
# The 'runs-on' value is read from config at workflow parse time, but
# Gitea Actions does not support dynamic runs-on from env vars.
# We use the default label here; if you changed DEPLOY_RUNNER_LABEL in
# config.env, you MUST also update this runs-on value to match.
#
# HOW TO CHANGE: replace 'deploy-ovh' below with your label.
# -------------------------------------------------------------------------
runs-on: deploy-ovh
steps:
# -----------------------------------------------------------------------
# Step 1: Load configuration
# -----------------------------------------------------------------------
- name: Checkout (for config only)
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Load config
id: config
run: |
# Source config.env
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
echo "Config loaded."
else
echo "WARNING: .ci/config.env not found, using defaults."
fi
# Export all deploy-related vars with safe defaults
echo "ENABLE_DEPLOY=${ENABLE_DEPLOY:-false}" >> "$GITHUB_ENV"
echo "DEPLOY_MODE=${DEPLOY_MODE:-local-runner}" >> "$GITHUB_ENV"
echo "DEPLOY_WORKDIR=${DEPLOY_WORKDIR:-/opt/app}" >> "$GITHUB_ENV"
echo "DEPLOY_STRATEGY=${DEPLOY_STRATEGY:-compose}" >> "$GITHUB_ENV"
echo "DEPLOY_COMPOSE_FILE=${DEPLOY_COMPOSE_FILE:-docker-compose.yml}" >> "$GITHUB_ENV"
echo "DEPLOY_SYSTEMD_SERVICE=${DEPLOY_SYSTEMD_SERVICE:-}" >> "$GITHUB_ENV"
echo "DEPLOY_SCRIPT=${DEPLOY_SCRIPT:-scripts/deploy.sh}" >> "$GITHUB_ENV"
echo "DEPLOY_ON_TAG=${DEPLOY_ON_TAG:-false}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 2: Gate checks — abort early if deploy should not run
# -----------------------------------------------------------------------
- name: Check if deploy is enabled
run: |
if [ "$ENABLE_DEPLOY" != "true" ]; then
echo "========================================="
echo " Deploy is DISABLED (ENABLE_DEPLOY=$ENABLE_DEPLOY)"
echo " To enable: set ENABLE_DEPLOY=true in .ci/config.env"
echo "========================================="
exit 0
fi
# Ensure this job is for the correct mode
if [ "$DEPLOY_MODE" != "local-runner" ]; then
echo "DEPLOY_MODE=$DEPLOY_MODE (not local-runner). This job is a no-op."
echo "The ssh job will handle deployment instead."
exit 0
fi
echo "DEPLOY_ACTIVE=true" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Branch guard
# Only deploy from DEFAULT_BRANCH. For tags, check DEPLOY_ON_TAG.
# This is a SAFETY net — even though 'on.push.branches' is set above,
# DEFAULT_BRANCH might differ from 'main'.
# -----------------------------------------------------------------------
- name: Branch guard
if: env.DEPLOY_ACTIVE == 'true'
run: |
REF="${GITHUB_REF:-}"
# Tag push?
if echo "$REF" | grep -q '^refs/tags/v'; then
if [ "$DEPLOY_ON_TAG" != "true" ]; then
echo "Tag push detected but DEPLOY_ON_TAG=$DEPLOY_ON_TAG. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on tag: $REF"
exit 0
fi
# Branch push — verify it's DEFAULT_BRANCH
BRANCH="$(echo "$REF" | sed 's|refs/heads/||')"
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "Branch '$BRANCH' is not DEFAULT_BRANCH '$DEFAULT_BRANCH'. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on branch: $BRANCH"
# -----------------------------------------------------------------------
# Step 4: Execute deploy strategy (LOCAL — runs on the VPS itself)
# -----------------------------------------------------------------------
- name: "Deploy: compose"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'compose'
run: |
echo ">>> Deploy strategy: compose"
echo ">>> Working directory: $DEPLOY_WORKDIR"
echo ">>> Compose file: $DEPLOY_COMPOSE_FILE"
cd "$DEPLOY_WORKDIR" || { echo "ERROR: Cannot cd to $DEPLOY_WORKDIR"; exit 1; }
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE pull"
docker compose -f "$DEPLOY_COMPOSE_FILE" pull
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE up -d"
docker compose -f "$DEPLOY_COMPOSE_FILE" up -d
echo "Deploy (compose) complete."
- name: "Deploy: systemd"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'systemd'
run: |
echo ">>> Deploy strategy: systemd"
if [ -z "$DEPLOY_SYSTEMD_SERVICE" ]; then
echo "ERROR: DEPLOY_SYSTEMD_SERVICE is not set."
echo "Set it in .ci/config.env for strategy=systemd."
exit 1
fi
echo ">>> systemctl restart $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl restart "$DEPLOY_SYSTEMD_SERVICE"
echo ">>> systemctl status $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl status "$DEPLOY_SYSTEMD_SERVICE" --no-pager
echo "Deploy (systemd) complete."
- name: "Deploy: script"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'script'
run: |
echo ">>> Deploy strategy: script"
echo ">>> Script: $DEPLOY_SCRIPT"
echo ">>> Workdir arg: $DEPLOY_WORKDIR"
if [ ! -f "$DEPLOY_SCRIPT" ]; then
echo "ERROR: Deploy script not found: $DEPLOY_SCRIPT"
exit 1
fi
chmod +x "$DEPLOY_SCRIPT"
"./$DEPLOY_SCRIPT" "$DEPLOY_WORKDIR"
echo "Deploy (script) complete."
# -----------------------------------------------------------------------
# Step 5: Summary
# -----------------------------------------------------------------------
- name: Deploy summary
if: always()
run: |
echo "=============================="
echo " Deploy (local-runner)"
echo " Enabled: ${ENABLE_DEPLOY:-false}"
echo " Mode: ${DEPLOY_MODE:-local-runner}"
echo " Strategy: ${DEPLOY_STRATEGY:-compose}"
echo " Workdir: ${DEPLOY_WORKDIR:-/opt/app}"
echo " Active: ${DEPLOY_ACTIVE:-false}"
echo "=============================="
# ===========================================================================
# JOB: deploy-ssh
# ===========================================================================
# Runs on a normal runner and SSHs into the VPS to deploy.
# This job is skipped if DEPLOY_MODE != ssh.
# ---------------------------------------------------------------------------
deploy-ssh:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Load configuration
# -----------------------------------------------------------------------
- name: Checkout (for config + scripts)
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Load config
run: |
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
echo "Config loaded."
else
echo "WARNING: .ci/config.env not found, using defaults."
fi
echo "ENABLE_DEPLOY=${ENABLE_DEPLOY:-false}" >> "$GITHUB_ENV"
echo "DEPLOY_MODE=${DEPLOY_MODE:-local-runner}" >> "$GITHUB_ENV"
echo "DEPLOY_WORKDIR=${DEPLOY_WORKDIR:-/opt/app}" >> "$GITHUB_ENV"
echo "DEPLOY_STRATEGY=${DEPLOY_STRATEGY:-compose}" >> "$GITHUB_ENV"
echo "DEPLOY_COMPOSE_FILE=${DEPLOY_COMPOSE_FILE:-docker-compose.yml}" >> "$GITHUB_ENV"
echo "DEPLOY_SYSTEMD_SERVICE=${DEPLOY_SYSTEMD_SERVICE:-}" >> "$GITHUB_ENV"
echo "DEPLOY_SCRIPT=${DEPLOY_SCRIPT:-scripts/deploy.sh}" >> "$GITHUB_ENV"
echo "DEPLOY_ON_TAG=${DEPLOY_ON_TAG:-false}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 2: Gate checks
# -----------------------------------------------------------------------
- name: Check if deploy is enabled
run: |
if [ "$ENABLE_DEPLOY" != "true" ]; then
echo "========================================="
echo " Deploy is DISABLED (ENABLE_DEPLOY=$ENABLE_DEPLOY)"
echo " To enable: set ENABLE_DEPLOY=true in .ci/config.env"
echo "========================================="
exit 0
fi
if [ "$DEPLOY_MODE" != "ssh" ]; then
echo "DEPLOY_MODE=$DEPLOY_MODE (not ssh). This job is a no-op."
echo "The local-runner job will handle deployment instead."
exit 0
fi
echo "DEPLOY_ACTIVE=true" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Branch guard (same logic as local-runner)
# -----------------------------------------------------------------------
- name: Branch guard
if: env.DEPLOY_ACTIVE == 'true'
run: |
REF="${GITHUB_REF:-}"
if echo "$REF" | grep -q '^refs/tags/v'; then
if [ "$DEPLOY_ON_TAG" != "true" ]; then
echo "Tag push detected but DEPLOY_ON_TAG=$DEPLOY_ON_TAG. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on tag: $REF"
exit 0
fi
BRANCH="$(echo "$REF" | sed 's|refs/heads/||')"
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "Branch '$BRANCH' is not DEFAULT_BRANCH '$DEFAULT_BRANCH'. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on branch: $BRANCH"
# -----------------------------------------------------------------------
# Step 4: Set up SSH
#
# Secrets required:
# DEPLOY_SSH_KEY — private key (ed25519 recommended)
# DEPLOY_HOST — VPS IP or hostname
# DEPLOY_USER — SSH username
# DEPLOY_KNOWN_HOSTS — (optional) output of ssh-keyscan for the host
#
# If DEPLOY_KNOWN_HOSTS is not set, StrictHostKeyChecking is disabled.
# This is less secure but avoids first-connect failures in CI.
# For production, always set DEPLOY_KNOWN_HOSTS.
# -----------------------------------------------------------------------
- name: Set up SSH
if: env.DEPLOY_ACTIVE == 'true'
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Write private key (never echo it)
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Known hosts — if provided, use it; otherwise disable strict checking
KNOWN_HOSTS="${{ secrets.DEPLOY_KNOWN_HOSTS }}"
if [ -n "$KNOWN_HOSTS" ]; then
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
echo "known_hosts configured from secret."
else
echo "WARNING: DEPLOY_KNOWN_HOSTS not set. Disabling StrictHostKeyChecking."
echo "For production, set DEPLOY_KNOWN_HOSTS (run: ssh-keyscan your-host)"
{
echo "Host *"
echo " StrictHostKeyChecking no"
echo " UserKnownHostsFile /dev/null"
} > ~/.ssh/config
chmod 600 ~/.ssh/config
fi
# Build SSH command for reuse
SSH_CMD="ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}"
echo "SSH_CMD=${SSH_CMD}" >> "$GITHUB_ENV"
# Verify connectivity
echo "Testing SSH connection..."
$SSH_CMD "echo 'SSH connection successful.'" || {
echo "ERROR: SSH connection failed."
exit 1
}
# -----------------------------------------------------------------------
# Step 5: Execute deploy strategy (REMOTE — via SSH)
# -----------------------------------------------------------------------
- name: "Deploy via SSH: compose"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'compose'
run: |
echo ">>> Deploy strategy: compose (via SSH)"
$SSH_CMD << DEPLOY_EOF
set -e
echo ">>> cd $DEPLOY_WORKDIR"
cd "$DEPLOY_WORKDIR" || { echo "ERROR: Cannot cd to $DEPLOY_WORKDIR"; exit 1; }
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE pull"
docker compose -f "$DEPLOY_COMPOSE_FILE" pull
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE up -d"
docker compose -f "$DEPLOY_COMPOSE_FILE" up -d
echo "Deploy (compose) complete."
DEPLOY_EOF
- name: "Deploy via SSH: systemd"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'systemd'
run: |
echo ">>> Deploy strategy: systemd (via SSH)"
if [ -z "$DEPLOY_SYSTEMD_SERVICE" ]; then
echo "ERROR: DEPLOY_SYSTEMD_SERVICE is not set."
exit 1
fi
$SSH_CMD << DEPLOY_EOF
set -e
echo ">>> sudo systemctl restart $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl restart "$DEPLOY_SYSTEMD_SERVICE"
echo ">>> systemctl status $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl status "$DEPLOY_SYSTEMD_SERVICE" --no-pager
echo "Deploy (systemd) complete."
DEPLOY_EOF
- name: "Deploy via SSH: script"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'script'
run: |
echo ">>> Deploy strategy: script (via SSH)"
# Copy the deploy script to the VPS
scp -i ~/.ssh/deploy_key \
"$DEPLOY_SCRIPT" \
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/_deploy_script.sh"
$SSH_CMD << DEPLOY_EOF
set -e
chmod +x /tmp/_deploy_script.sh
/tmp/_deploy_script.sh "$DEPLOY_WORKDIR"
rm -f /tmp/_deploy_script.sh
echo "Deploy (script) complete."
DEPLOY_EOF
# -----------------------------------------------------------------------
# Step 6: Clean up SSH key (always runs)
# -----------------------------------------------------------------------
- name: Clean up SSH
if: always()
run: |
rm -rf ~/.ssh/deploy_key ~/.ssh/config 2>/dev/null || true
# -----------------------------------------------------------------------
# Step 7: Summary
# -----------------------------------------------------------------------
- name: Deploy summary
if: always()
run: |
echo "=============================="
echo " Deploy (ssh)"
echo " Enabled: ${ENABLE_DEPLOY:-false}"
echo " Mode: ${DEPLOY_MODE:-ssh}"
echo " Strategy: ${DEPLOY_STRATEGY:-compose}"
echo " Workdir: ${DEPLOY_WORKDIR:-/opt/app}"
echo " Active: ${DEPLOY_ACTIVE:-false}"
echo "=============================="