# CI/CD Automation

Guide to automating Auberge deployments with CI/CD pipelines.

## Overview

Auberge supports fully automated deployments via the `--force` flag, which skips interactive prompts while still displaying warnings.

**Key features:**

- Skip confirmation prompts
- Explicit host and playbook selection
- Warning messages always displayed
- Exit codes for pipeline integration

## The --force Flag

### What It Does

`--force` (or `-f`) skips interactive confirmations:

```bash
# With --force: no prompts
auberge ansible run --host production --playbook playbooks/apps.yml --force

# Without --force: prompts for confirmation
auberge ansible run --host production --playbook playbooks/apps.yml
# ? Confirm execution? [y/N]:
```

### What It Doesn't Do

**--force does NOT skip warnings:**

```bash
auberge ansible bootstrap production --ip 10.0.0.1 --force
```

**Output:**

```
⚠ WARNING: Ensure provider firewall allows SSH_PORT (2222)
⚠ WARNING: Bootstrap will change SSH port from 22 to 2222
⚠ WARNING: Ensure Cloudflare API token is configured

Proceeding with bootstrap...
```

**Warnings always display** - you're responsible for heeding them.

## GitHub Actions

### Complete Workflow Example

```yaml
name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch: # Manual trigger

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Install Auberge
        run: cargo install auberge

      - name: Configure secrets
        env:
          SSH_PORT: ${{ secrets.SSH_PORT }}
          ADMIN_USER_NAME: ${{ secrets.ADMIN_USER_NAME }}
          CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
        run: |
          auberge config init
          auberge config set ssh_port "$SSH_PORT"
          auberge config set admin_user_name "$ADMIN_USER_NAME"
          auberge config set cloudflare_dns_api_token "$CLOUDFLARE_DNS_API_TOKEN"

      - name: Set up SSH key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh/identities
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/identities/ansible_production
          chmod 600 ~/.ssh/identities/ansible_production

      - name: Deploy applications
        run: |
          auberge ansible run \
            --host production \
            --playbook playbooks/apps.yml \
            --force \
            --skip-tags bootstrap

      - name: Verify deployment
        run: |
          ssh -i ~/.ssh/identities/ansible_production \
              -p ${{ secrets.SSH_PORT }} \
              ansible@${{ secrets.AUBERGE_HOST }} \
              "systemctl is-active baikal freshrss navidrome"
```

### Required GitHub Secrets

Set these in repository settings → Secrets and variables → Actions:

- `SSH_PORT` - Custom SSH port
- `SSH_PRIVATE_KEY` - ansible user's SSH private key (full content)
- `ADMIN_USER_NAME` - Admin username
- `CLOUDFLARE_DNS_API_TOKEN` - Cloudflare API token
- Other app-specific secrets (BAIKAL_ADMIN_PASSWORD, etc.)

## GitLab CI

### .gitlab-ci.yml

```yaml
stages:
  - deploy

deploy_production:
  stage: deploy
  image: rust:latest
  only:
    - main
  before_script:
    - cargo install auberge
    - mkdir -p ~/.ssh/identities
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/identities/ansible_production
    - chmod 600 ~/.ssh/identities/ansible_production
    - auberge config set ssh_port "$SSH_PORT"
    - auberge config set admin_user_name "$ADMIN_USER_NAME"
    - auberge config set cloudflare_dns_api_token "$CLOUDFLARE_DNS_API_TOKEN"
  script:
    - auberge ansible run
      --host production
      --playbook playbooks/apps.yml
      --force
      --skip-tags bootstrap
  after_script:
    - ssh -i ~/.ssh/identities/ansible_production
      -p $SSH_PORT
      ansible@$AUBERGE_HOST
      "systemctl status php*-fpm freshrss navidrome"
```

### Required GitLab Variables

Set in Settings → CI/CD → Variables:

- `SSH_PORT`
- `SSH_PRIVATE_KEY` (masked, file type)
- `ADMIN_USER_NAME`
- `CLOUDFLARE_DNS_API_TOKEN` (masked)

## Best Practices

### Use Separate Environments

```yaml
# .github/workflows/deploy-staging.yml
- name: Deploy to staging
  run: |
    auberge ansible run \
      --host staging \
      --playbook playbooks/apps.yml \
      --force

# .github/workflows/deploy-production.yml
- name: Deploy to production
  run: |
    auberge ansible run \
      --host production \
      --playbook playbooks/apps.yml \
      --force
```

### Create Backup Before Deploy

```yaml
- name: Create backup
  run: |
    auberge backup create --host production --yes

- name: Deploy
  run: |
    auberge ansible run \
      --host production \
      --playbook playbooks/apps.yml \
      --force

- name: Verify deployment
  run: |
    # Health checks
    curl -f https://cal.example.com || exit 1
```

### Use Check Mode First

```yaml
- name: Check what would change
  run: |
    auberge deploy \
      --all \
      --host production \
      --check \
      --force

- name: Deploy if check passed
  run: |
    auberge deploy \
      --all \
      --host production \
      --force
```

### Tag-Based Deployments

```yaml
- name: Deploy only changed apps
  run: |
    # Detect which apps changed
    CHANGED_APPS=$(git diff --name-only HEAD~1 | grep 'ansible/roles/' | cut -d'/' -f3 | tr '\n' ',' | sed 's/,$//')

    if [ -n "$CHANGED_APPS" ]; then
      auberge ansible run \
        --host production \
        --tags "$CHANGED_APPS" \
        --force
    else
      echo "No app changes detected"
    fi
```

### Rollback on Failure

```yaml
- name: Deploy
  id: deploy
  run: |
    auberge ansible run \
      --host production \
      --playbook playbooks/apps.yml \
      --force

- name: Rollback on failure
  if: failure() && steps.deploy.conclusion == 'failure'
  run: |
    auberge backup restore latest \
      --host production \
      --yes
```

## Security Considerations

### Protect SSH Keys

**Never commit SSH private keys** to version control.

✓ **Good:** Use GitHub Secrets or GitLab Variables
✗ **Bad:** Store in repository

### Use Deploy Keys

Generate dedicated SSH keys for CI/CD:

```bash
# On CI runner or locally
ssh-keygen -t ed25519 -f ~/.ssh/ci_deploy_key -C "ci-deploy@example.com"

# Add public key to VPS
ssh-copy-id -i ~/.ssh/ci_deploy_key.pub ansible@vps-host
```

Add private key to CI secrets:

```bash
cat ~/.ssh/ci_deploy_key  # Copy to GitHub Secrets
```

### Restrict Runner Permissions

Use least-privilege principles:

```yaml
permissions:
  contents: read # Don't grant write unless needed
```

### Audit Deployments

Log all deployments:

```yaml
- name: Log deployment
  run: |
    echo "Deployed by: ${{ github.actor }}"
    echo "Commit: ${{ github.sha }}"
    echo "Branch: ${{ github.ref }}"
    date
```

## Testing in CI

### Ansible Syntax Check

```yaml
- name: Check Ansible syntax
  run: |
    cd ansible && ansible-playbook playbooks/apps.yml --syntax-check
```

### Lint Playbooks

```yaml
- name: Lint Ansible playbooks
  run: |
    ansible-lint playbooks/
```

### Validate Inventory

```yaml
- name: Validate inventory
  run: |
    ansible-inventory -i ansible/inventory.yml --list
```

## Deployment Strategies

### Blue-Green Deployment

Maintain two VPS hosts:

```yaml
- name: Deploy to blue
  run: |
    auberge ansible run --host blue --playbook playbooks/apps.yml --force

- name: Switch DNS to blue
  run: |
    auberge dns set-all --host blue --force

- name: Drain green
  run: |
    # Wait for connections to drain
    sleep 300

- name: Update green
  run: |
    auberge ansible run --host green --playbook playbooks/apps.yml --force
```

### Canary Deployment

Deploy to subset of hosts first:

```yaml
- name: Deploy to canary
  run: |
    auberge ansible run --host canary --playbook playbooks/apps.yml --force

- name: Smoke test
  run: |
    curl -f https://cal.canary.example.com || exit 1

- name: Deploy to production
  run: |
    auberge ansible run --host production --playbook playbooks/apps.yml --force
```

## Monitoring Deployments

### Send Notifications

```yaml
- name: Notify on success
  if: success()
  run: |
    curl -X POST $WEBHOOK_URL \
      -d "Deployment to production succeeded"

- name: Notify on failure
  if: failure()
  run: |
    curl -X POST $WEBHOOK_URL \
      -d "Deployment to production failed"
```

### Log to External Service

```yaml
- name: Log deployment
  run: |
    curl -X POST https://logging-service.example.com/events \
      -H "Content-Type: application/json" \
      -d '{
        "event": "deployment",
        "status": "success",
        "commit": "${{ github.sha }}",
        "actor": "${{ github.actor }}"
      }'
```

## Example: Minimal Production Pipeline

```yaml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo install auberge

      - name: Set up SSH
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh/identities
          echo "$SSH_KEY" > ~/.ssh/identities/ansible_production
          chmod 600 ~/.ssh/identities/ansible_production

      - name: Configure
        env:
          SSH_PORT: ${{ secrets.SSH_PORT }}
          CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
        run: |
          auberge config set ssh_port "$SSH_PORT"
          auberge config set cloudflare_dns_api_token "$CLOUDFLARE_DNS_API_TOKEN"

      - name: Deploy
        run: |
          auberge ansible run \
            --host production \
            --playbook playbooks/apps.yml \
            --force \
            --skip-tags bootstrap
```

## Troubleshooting

### SSH Host Key Verification Failed

Add host key to known_hosts:

```yaml
- name: Add host key
  run: |
    ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.AUBERGE_HOST }} >> ~/.ssh/known_hosts
```

### Config Values Not Set

Ensure config is properly written:

```yaml
- name: Debug config
  run: |
    auberge config list
```

### Permission Denied (SSH)

Check SSH key permissions:

```yaml
- run: chmod 600 ~/.ssh/identities/ansible_production
```

### Ansible Connection Timeout

Increase timeout in inventory:

```yaml
ansible_ssh_common_args: >-
  -o ConnectTimeout=30
  -o ConnectionAttempts=3
```

## Related Pages

- [Running Playbooks](deployment/running-playbooks.md) - Manual execution
- [Bootstrap](deployment/bootstrap.md) - Initial VPS setup
- [Secrets Management](configuration/secrets.md) - Handling sensitive data
- [SSH Keys](configuration/ssh-keys.md) - SSH configuration
