# Deploy Workflow — ${REPO_NAME} ## Overview The deploy workflow (`.gitea/workflows/deploy.yml`) automates deployment to a VPS after a push to the default branch. It supports two modes and three deployment strategies. **Disabled by default.** Set `ENABLE_DEPLOY=true` in `.ci/config.env` to enable. ## Deployment Modes ### Mode A: Local Runner (Recommended) The deploy job runs **directly on the VPS** via a self-hosted `act_runner`. No SSH keys or network credentials needed — the runner already has local access. ``` ┌──────────┐ push ┌──────────┐ runs-on: deploy-ovh ┌───────────┐ │ Developer │ ────────────► │ Gitea │ ────────────────────────► │ VPS │ │ │ │ Server │ │ act_runner│ └──────────┘ └──────────┘ │ (local) │ └───────────┘ ``` **Pros:** - No SSH secrets to manage - No network exposure (runner is already on the VPS) - Simpler setup, fewer moving parts - Runner can access local Docker socket, systemd, files directly **Cons:** - Requires installing `act_runner` on the VPS - One runner per VPS (or one runner with multiple labels) ### Mode B: SSH (Fallback) The deploy job runs on **any runner** (e.g., shared ubuntu-latest) and SSHs into the VPS to execute deploy commands remotely. ``` ┌──────────┐ push ┌──────────┐ runs job ┌────────┐ SSH ┌─────┐ │ Developer │ ────────────► │ Gitea │ ──────────────► │ Runner │ ────────► │ VPS │ └──────────┘ └──────────┘ │(shared)│ └─────┘ └────────┘ ``` **Pros:** - Works without installing anything on the VPS - Can deploy to VPSes you don't fully control **Cons:** - Requires SSH secrets (private key, host, user) - VPS SSH port must be reachable from the runner - More moving parts, more to go wrong ## Deploy Strategies Regardless of mode, the workflow supports three strategies for what actually happens on the VPS: ### `compose` (default) ```bash cd /opt/myapp docker compose -f docker-compose.yml pull docker compose -f docker-compose.yml up -d ``` Best for: Docker Compose-based projects. Pulls latest images and recreates containers with zero-downtime (depends on your compose config). ### `systemd` ```bash sudo systemctl restart my-service ``` Best for: Applications managed as systemd services (e.g., a Go binary, Python app with gunicorn, etc.). ### `script` ```bash ./scripts/deploy.sh /opt/myapp ``` Best for: Custom deploy logic that doesn't fit compose or systemd. The script receives `DEPLOY_WORKDIR` as `$1`. ## Setup Guide ### Local Runner Mode #### Step 1: Install act_runner on the VPS ```bash # Download the latest act_runner binary wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64 chmod +x act_runner-linux-amd64 sudo mv act_runner-linux-amd64 /usr/local/bin/act_runner ``` #### Step 2: Register the runner with your label ```bash # Register with Gitea (get the token from: Site Admin → Runners → Create) act_runner register \ --instance https://git.hiddenden.cafe \ --token YOUR_RUNNER_TOKEN \ --labels deploy-ovh # Or for repo-level: Repository Settings → Actions → Runners → Create ``` #### Step 3: Configure runner labels The runner config file (usually `~/.config/act_runner/config.yaml` or next to the binary) contains the labels: ```yaml runner: labels: - "deploy-ovh:host" ``` The `:host` suffix means the job runs directly on the host (not in a container). This is important for accessing Docker, systemd, and local files. **After changing labels, restart the runner:** ```bash sudo systemctl restart act_runner ``` #### Step 4: Update workflow runs-on In `.gitea/workflows/deploy.yml`, the `deploy-local-runner` job has: ```yaml runs-on: deploy-ovh ``` If you changed `DEPLOY_RUNNER_LABEL` in config.env, you **must also** update this `runs-on` value in the workflow file to match. Gitea Actions does not support dynamic `runs-on` from environment variables. #### Step 5: Enable deploy In `.ci/config.env`: ```env ENABLE_DEPLOY=true DEPLOY_MODE=local-runner DEPLOY_RUNNER_LABEL=deploy-ovh DEPLOY_WORKDIR=/opt/myapp DEPLOY_STRATEGY=compose ``` #### Step 6: Verify VPS prerequisites For `compose` strategy: ```bash # Docker and compose plugin must be installed docker --version docker compose version # The deploy workdir must exist with a docker-compose.yml ls /opt/myapp/docker-compose.yml ``` For `systemd` strategy: ```bash # The service must exist systemctl cat my-service ``` For `script` strategy: ```bash # The script must be in the repo and executable ``` ### SSH Mode #### Step 1: Create an SSH key ```bash ssh-keygen -t ed25519 -C "deploy@gitea" -f deploy_key -N "" ``` #### Step 2: Add the public key to the VPS ```bash ssh-copy-id -i deploy_key.pub user@your-vps ``` #### Step 3: Add secrets to Gitea Go to **Repository Settings → Actions → Secrets** and add: | Secret | Value | |--------|-------| | `DEPLOY_SSH_KEY` | Contents of `deploy_key` (private key) | | `DEPLOY_HOST` | VPS IP or hostname | | `DEPLOY_USER` | SSH username | | `DEPLOY_KNOWN_HOSTS` | Output of `ssh-keyscan your-vps` (recommended) | #### Step 4: Enable deploy In `.ci/config.env`: ```env ENABLE_DEPLOY=true DEPLOY_MODE=ssh DEPLOY_WORKDIR=/opt/myapp DEPLOY_STRATEGY=compose ``` ## Configuration Reference | Variable | Default | Description | |----------|---------|-------------| | `ENABLE_DEPLOY` | `false` | Master switch. Deploy never runs unless `true`. | | `DEPLOY_MODE` | `local-runner` | `local-runner` or `ssh` | | `DEPLOY_RUNNER_LABEL` | `deploy-ovh` | Runner label for local-runner mode | | `DEPLOY_WORKDIR` | `/opt/${REPO_NAME}` | Working directory on the VPS | | `DEPLOY_STRATEGY` | `compose` | `compose`, `systemd`, or `script` | | `DEPLOY_COMPOSE_FILE` | `docker-compose.yml` | Compose file path (relative to workdir) | | `DEPLOY_SYSTEMD_SERVICE` | _(empty)_ | Systemd service name (required for systemd strategy) | | `DEPLOY_SCRIPT` | `scripts/deploy.sh` | Deploy script path (relative to repo root) | | `DEPLOY_ON_TAG` | `false` | Also deploy on `v*` tag pushes | ## Safety ### Branch Protection The workflow only deploys from the `DEFAULT_BRANCH` (main). This is enforced in two places: 1. **Trigger filter**: `on.push.branches: [main]` 2. **Branch guard step**: Runtime check comparing the actual ref to `DEFAULT_BRANCH` ### Never on Pull Requests The `pull_request` event is intentionally absent from the trigger list. Deploy cannot run on PRs. ### Protected Branches For maximum safety, enable **branch protection** on `main` in Gitea: 1. Go to **Repository Settings → Branches → Branch Protection** 2. Enable for `main` 3. Require: status checks to pass, review approval, no force push 4. This ensures only reviewed, passing code reaches main → gets deployed ### Secrets - **Local-runner mode**: No secrets needed. The runner has local access. - **SSH mode**: Private key is written to a temp file, used once, then deleted. It is never echoed to logs. ## Runner Label Examples | Label | Description | |-------|-------------| | `deploy-ovh` | OVH VPS runner | | `vps-prod` | Production VPS | | `deploy-hetzner` | Hetzner VPS runner | | `staging-runner` | Staging environment | ## Troubleshooting ### Local Runner Mode - [ ] **Runner registered?** Check Gitea Admin → Runners or Repo Settings → Runners - [ ] **Label matches?** `runs-on` in workflow must match the runner's label exactly - [ ] **Runner online?** Check runner status in Gitea; restart if offline - [ ] **Label suffix correct?** Use `:host` for direct host execution (not container) - [ ] **Docker accessible?** Runner user must be in the `docker` group - [ ] **Workdir exists?** `DEPLOY_WORKDIR` must exist and be readable - [ ] **Compose file present?** Check `docker-compose.yml` exists in the workdir - [ ] **Permissions?** For systemd: runner user needs sudo for systemctl ### SSH Mode - [ ] **Secrets set?** Check all four secrets are in repo settings - [ ] **Key format?** Private key must include `-----BEGIN` header - [ ] **SSH port open?** VPS firewall must allow port 22 (or custom port) from runner - [ ] **User permissions?** SSH user needs access to Docker/systemd/workdir - [ ] **Known hosts?** Set `DEPLOY_KNOWN_HOSTS` for production (avoids MITM risk) - [ ] **Key not passphrase-protected?** CI keys must have no passphrase ### General - [ ] **ENABLE_DEPLOY=true?** Check `.ci/config.env` - [ ] **Pushing to main?** Deploy only triggers on `DEFAULT_BRANCH` - [ ] **Workflow file correct?** YAML syntax errors prevent the workflow from running - [ ] **Check Actions tab** in Gitea for workflow run logs