452 lines
18 KiB
YAML
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 "=============================="
|