name: deploy on: push: branches: [main] paths: # No build/config/terraform/** — this workflow only deploys to Unraid, # and `terraform apply` is run by hand. Add it if that ever changes. - "src/**" - "build/config/ansible/**" jobs: deploy: # Requires a self-hosted Forgejo Actions runner on your LAN (labelled # `unraid-deploy`) with: network access to Vault and to the Unraid hosts' # SSH + Postgres ports, and Python + pip available. See README.md # "Bootstrapping" — this workflow can only run once that runner exists, # which is why the first deploy has to happen by hand. The SSH key # `ansible_user` (root) authenticates with is fetched from Vault below, # not stored as a runner or repo secret — see README.md "SSH access". runs-on: unraid-deploy env: VAULT_ADDR: ${{ secrets.VAULT_ADDR }} VAULT_AUTH_METHOD: approle VAULT_ROLE_ID: ${{ secrets.VAULT_ROLE_ID }} VAULT_SECRET_ID: ${{ secrets.VAULT_SECRET_ID }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # need history to diff — a shallow clone can't compute this - name: Install Ansible and Python dependencies run: | python3 -m pip install --upgrade pip python3 -m pip install ansible hvac psycopg2-binary - name: Install Ansible collections working-directory: build/config/ansible run: ansible-galaxy collection install -r requirements.yml - name: Fetch SSH deploy key from Vault run: | set -eu python3 - <<'PY' import os import hvac client = hvac.Client(url=os.environ["VAULT_ADDR"]) client.auth.approle.login( role_id=os.environ["VAULT_ROLE_ID"], secret_id=os.environ["VAULT_SECRET_ID"], ) secret = client.secrets.kv.v2.read_secret_version( path="homelab/ci/ssh", mount_point="kv" )["data"]["data"] key_path = os.path.join(os.environ["RUNNER_TEMP"], "unraid_ansible_key") with open(key_path, "w") as f: f.write(secret["PRIVATE_KEY"].rstrip() + "\n") os.chmod(key_path, 0o600) PY echo "ANSIBLE_PRIVATE_KEY_FILE=$RUNNER_TEMP/unraid_ansible_key" >> "$GITHUB_ENV" - name: Determine changed stacks id: changed run: | set -eu # Three possible outcomes, and they are not the same thing: # all=true deploy every stack on every host # only_stacks=a,b deploy just those # skip=true nothing here affects the Unraid deployment # An empty only_stacks must never be read as "deploy everything" — # that is exactly what a Proxmox-only or Terraform-only push # produces. # # A change under build/config/ansible/ can affect how every stack # is deployed (the role, inventory, playbook), so don't try to # narrow it. Same if the diff can't be computed at all (force-push, # or a first push where `before` is all-zeros): fall back to a full # deploy rather than silently deploying nothing. if ! changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" 2>/dev/null); then echo "all=true" >> "$GITHUB_OUTPUT" exit 0 fi if echo "$changed_files" | grep -q '^build/config/ansible/'; then echo "all=true" >> "$GITHUB_OUTPUT" exit 0 fi # Only changes that affect the Unraid deployment count, since that # is all this job runs. A stack's common/ and ansible/unraid/ do; # its terraform/ and ansible/proxmox/ don't. stacks=$(echo "$changed_files" \ | { grep '^src/' || true; } \ | { grep -Ev '/(terraform|ansible/proxmox)/' || true; } \ | awk -F/ '{ print ($2 == "shared") ? $3 : $2 }' \ | sort -u | paste -sd, -) if [ -z "$stacks" ]; then echo "skip=true" >> "$GITHUB_OUTPUT" else echo "only_stacks=$stacks" >> "$GITHUB_OUTPUT" fi # Unraid only. Deploying to Proxmox from CI would need the runner to # reach the Proxmox API and the guests, and `terraform apply` is # deliberately kept manual — creating and destroying LXCs on a push is # a bigger blast radius than restarting a Compose stack. Run # playbooks/proxmox.yml by hand for now. - name: Run deploy playbook if: steps.changed.outputs.skip != 'true' working-directory: build/config/ansible run: | if [ "${{ steps.changed.outputs.all }}" = "true" ]; then ansible-playbook playbooks/unraid.yml else ansible-playbook playbooks/unraid.yml -e only_stacks="${{ steps.changed.outputs.only_stacks }}" fi