From 9f84652102f786d8a3d1f24ef6db60a57e9dc1c9 Mon Sep 17 00:00:00 2001 From: "Rasmus \"Pez\" Wejlgaard" Date: Fri, 15 May 2026 20:17:28 +0100 Subject: [PATCH] fix: cleanup deploy.yml and share workflow (#108) * fix: cleanup deploy.yml and share workflow * lint issue --- .github/workflows/_deploy-core.yml | 98 +++++++++++++++++++++++++++ .github/workflows/deploy-on-merge.yml | 58 ++-------------- .github/workflows/deploy.yml | 70 ++----------------- ansible/deploy.yml | 69 ++++--------------- 4 files changed, 122 insertions(+), 173 deletions(-) create mode 100644 .github/workflows/_deploy-core.yml diff --git a/.github/workflows/_deploy-core.yml b/.github/workflows/_deploy-core.yml new file mode 100644 index 0000000..371ab75 --- /dev/null +++ b/.github/workflows/_deploy-core.yml @@ -0,0 +1,98 @@ +name: Deploy (core) + +on: + workflow_call: + inputs: + host: + required: true + type: string + playbook: + required: true + type: string + dry_run: + required: false + type: boolean + default: false + secrets: + TAILSCALE_CLIENT_ID: + required: true + TAILSCALE_AUDIENCE: + required: true + SSH_PRIVATE_KEY: + required: true + AGE_SECRET_KEY: + required: true + +jobs: + deploy: + name: Deploy ${{ inputs.playbook }} → ${{ inputs.host }} + runs-on: ubuntu-latest + environment: production + permissions: + id-token: write + steps: + - uses: actions/checkout@v6 + + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: pip-ansible + + - name: Cache Ansible collections + uses: actions/cache@v4 + with: + path: ~/.ansible + key: ansible-galaxy-${{ hashFiles('ansible/requirements.yml') }} + + - name: Set up Tailscale + uses: tailscale/github-action@v4 + with: + oauth-client-id: ${{ secrets.TAILSCALE_CLIENT_ID }} + audience: ${{ secrets.TAILSCALE_AUDIENCE }} + tags: tag:ci + + - name: Set up SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + HOST_IP=$(grep "^${{ inputs.host }}[[:space:]]" ansible/inventory/hosts.ini | grep -o 'ansible_host=[^ ]*' | cut -d= -f2) + if [ -n "$HOST_IP" ]; then + ssh-keyscan -H "$HOST_IP" >> ~/.ssh/known_hosts 2>/dev/null || true + fi + + - name: Install tools + run: | + pip install ansible + wget -qO /tmp/sops.deb https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb + sudo dpkg -i /tmp/sops.deb + + - name: Install Ansible collections + run: ansible-galaxy install -r ansible/requirements.yml + + - name: Decrypt secrets + env: + SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }} + run: | + find . -name '*.enc.yml' -o -name '*.enc.yaml' -o -name '*.enc.env' | while read f; do + out="${f/.enc/}" + sops -d "$f" > "$out" + done + + - name: Run playbook + working-directory: ansible/ + env: + ANSIBLE_HOST_KEY_CHECKING: "false" + run: | + PLAYBOOK="${{ inputs.playbook }}" + PLAYBOOK="${PLAYBOOK#playbooks/}" + PLAYBOOK="${PLAYBOOK%.yml}.yml" + if [ "$PLAYBOOK" != "deploy.yml" ]; then + PLAYBOOK="playbooks/$PLAYBOOK" + fi + ARGS="--limit ${{ inputs.host }}" + if [ "${{ inputs.dry_run }}" = "true" ]; then + ARGS="$ARGS --check --diff" + fi + ansible-playbook "$PLAYBOOK" $ARGS diff --git a/.github/workflows/deploy-on-merge.yml b/.github/workflows/deploy-on-merge.yml index c1bf91d..b4993b4 100644 --- a/.github/workflows/deploy-on-merge.yml +++ b/.github/workflows/deploy-on-merge.yml @@ -7,12 +7,6 @@ on: paths-ignore: - "terraform/**" -# Requires these repository secrets: -# TAILSCALE_CLIENT_ID — Tailscale OAuth client ID (federated identity) -# TAILSCALE_AUDIENCE — Tailscale federated identity audience -# SSH_PRIVATE_KEY — SSH key authorized on target hosts -# AGE_SECRET_KEY — age private key for SOPS decryption - jobs: discover: name: Discover hosts @@ -30,53 +24,13 @@ jobs: deploy: needs: discover - name: Deploy → ${{ matrix.host }} - runs-on: ubuntu-latest - environment: production - permissions: - id-token: write strategy: matrix: host: ${{ fromJson(needs.discover.outputs.hosts) }} fail-fast: false - steps: - - uses: actions/checkout@v6 - - - name: Set up Tailscale - uses: tailscale/github-action@v4 - with: - oauth-client-id: ${{ secrets.TAILSCALE_CLIENT_ID }} - audience: ${{ secrets.TAILSCALE_AUDIENCE }} - tags: tag:ci - - - name: Set up SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - IPS=$(grep 'ansible_host=' ansible/inventory/hosts.ini | awk -F'ansible_host=' '{print $2}' | awk '{print $1}' | tr '\n' ' ') - ssh-keyscan -H $IPS >> ~/.ssh/known_hosts 2>/dev/null || true - - - name: Install tools - run: | - pip install ansible - wget -qO /tmp/sops.deb https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb - sudo dpkg -i /tmp/sops.deb - - - name: Install Ansible collections - run: ansible-galaxy install -r ansible/requirements.yml - - - name: Decrypt secrets - env: - SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }} - run: | - find . -name '*.enc.yml' -o -name '*.enc.yaml' -o -name '*.enc.env' | while read f; do - out="${f/.enc/}" - sops -d "$f" > "$out" - done - - - name: Run playbook - working-directory: ansible/ - env: - ANSIBLE_HOST_KEY_CHECKING: "false" - run: ansible-playbook deploy.yml --limit "${{ matrix.host }}" + uses: ./.github/workflows/_deploy-core.yml + with: + host: ${{ matrix.host }} + playbook: deploy.yml + dry_run: false + secrets: inherit diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5c4359b..eb41ea1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,12 +17,6 @@ on: type: boolean default: true -# Requires these repository secrets: -# TAILSCALE_CLIENT_ID — Tailscale OAuth client ID (federated identity) -# TAILSCALE_AUDIENCE — Tailscale federated identity audience -# SSH_PRIVATE_KEY — SSH key authorized on target hosts -# AGE_SECRET_KEY — age private key for SOPS decryption - jobs: prepare: name: Prepare matrix @@ -46,65 +40,13 @@ jobs: deploy: needs: prepare - name: Deploy ${{ inputs.playbook }} → ${{ matrix.host }} - runs-on: ubuntu-latest - environment: production # requires manual approval in repo settings - permissions: - id-token: write strategy: matrix: host: ${{ fromJson(needs.prepare.outputs.hosts) }} fail-fast: false - steps: - - uses: actions/checkout@v6 - - - name: Set up Tailscale - uses: tailscale/github-action@v4 - with: - oauth-client-id: ${{ secrets.TAILSCALE_CLIENT_ID }} - audience: ${{ secrets.TAILSCALE_AUDIENCE }} - tags: tag:ci - - - name: Set up SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - IPS=$(grep 'ansible_host=' ansible/inventory/hosts.ini | awk -F'ansible_host=' '{print $2}' | awk '{print $1}' | tr '\n' ' ') - ssh-keyscan -H $IPS >> ~/.ssh/known_hosts 2>/dev/null || true - - - name: Install tools - run: | - pip install ansible - wget -qO /tmp/sops.deb https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb - sudo dpkg -i /tmp/sops.deb - - - name: Install Ansible collections - run: ansible-galaxy install -r ansible/requirements.yml - - - name: Decrypt secrets - env: - SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }} - run: | - find . -name '*.enc.yml' -o -name '*.enc.yaml' -o -name '*.enc.env' | while read f; do - out="${f/.enc/}" - sops -d "$f" > "$out" - done - - - name: Run playbook - working-directory: ansible/ - env: - ANSIBLE_HOST_KEY_CHECKING: "false" - run: | - PLAYBOOK="${{ inputs.playbook }}" - PLAYBOOK="${PLAYBOOK#playbooks/}" - PLAYBOOK="${PLAYBOOK%.yml}.yml" - if [ "$PLAYBOOK" != "deploy.yml" ]; then - PLAYBOOK="playbooks/$PLAYBOOK" - fi - - ARGS="--limit ${{ matrix.host }}" - if [ "${{ inputs.dry_run }}" = "true" ]; then - ARGS="$ARGS --check --diff" - fi - ansible-playbook "$PLAYBOOK" $ARGS + uses: ./.github/workflows/_deploy-core.yml + with: + host: ${{ matrix.host }} + playbook: ${{ inputs.playbook }} + dry_run: ${{ inputs.dry_run }} + secrets: inherit diff --git a/ansible/deploy.yml b/ansible/deploy.yml index 7320eee..7a5b6c9 100644 --- a/ansible/deploy.yml +++ b/ansible/deploy.yml @@ -8,37 +8,27 @@ # Full fleet: ansible-playbook deploy.yml # Single host: ansible-playbook deploy.yml --limit helsinki-a # Dry run: ansible-playbook deploy.yml --check --diff +# Services only (skip baseline): ansible-playbook deploy.yml --tags services # # Prerequisites: # - Target host has SSH access via Tailscale # - Target host has a base OS installed (Debian) # - ansible-galaxy install -r requirements.yml -# ────────────────────────────────────────────── -# Stage 1: Common baseline — all hosts -# ────────────────────────────────────────────── -- name: "Stage 1: Common baseline" +- name: Common baseline hosts: all tags: [common, baseline] roles: - role: common - role: dotfiles -# ────────────────────────────────────────────── -# Stage 2: Docker engine — hosts that run containers -# ────────────────────────────────────────────── -- name: "Stage 2: Docker engine" +- name: Docker engine hosts: docker_hosts tags: [docker] roles: - role: docker -# ────────────────────────────────────────────── -# Stage 4: Per-host services -# ────────────────────────────────────────────── - -# helsinki-a: Caddy reverse proxy + status page -- name: "Stage 4a: Caddy + status page (helsinki-a)" +- name: Caddy + status page (helsinki-a) hosts: helsinki-a tags: [services, caddy, status_page] roles: @@ -46,75 +36,40 @@ - role: status_page - role: systemd_services -# All docker hosts: deploy compose services -- name: "Stage 4b: Docker services" +- name: Docker services hosts: docker_hosts tags: [services, docker_services] roles: - role: docker_services -# nuremberg-a: Mail server -- name: "Stage 4c: Mail server (nuremberg-a)" +- name: Mail — poste.io (nuremberg-a) hosts: nuremberg-a tags: [services, mail] + roles: + - role: docker_services -# london-b: Media stack + backups -- name: "Stage 4d: Media stack + backups (london-b)" +- name: Media stack + backups (london-b) hosts: london-b tags: [services, london-b] roles: - role: media_stack - role: backup -# copenhagen-a: Gaming servers -- name: "Stage 4e: Gaming servers (copenhagen-a)" +- name: Gaming servers (copenhagen-a) hosts: copenhagen-a tags: [services, gaming] roles: - role: systemd_services - role: mariadb -# london-a: Proxmox VE hypervisor -- name: "Stage 4f: Proxmox VE (london-a)" +- name: Proxmox VE (london-a) hosts: london-a tags: [services, proxmox] roles: - role: proxmox_ve -# ────────────────────────────────────────────── -# Stage 4g: ZFS scrub scheduling — zfs_hosts -# ────────────────────────────────────────────── -- name: "Stage 4g: ZFS scrub scheduling" +- name: ZFS scrub scheduling hosts: zfs_hosts tags: [services, zfs] roles: - role: zfs - -# ────────────────────────────────────────────── -# Stage 5: Verification -# ────────────────────────────────────────────── -- name: "Stage 5: Post-deploy verification" - hosts: all - tags: [verify] - tasks: - - name: Check SSH is working - ansible.builtin.ping: - - - name: Gather uptime - ansible.builtin.command: uptime - changed_when: false - register: uptime_result - - - name: Check Docker containers (where applicable) - ansible.builtin.command: docker ps --format "table {{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}" - changed_when: false - register: docker_status - when: "'docker_hosts' in group_names" - failed_when: false - - - name: Report host status - ansible.builtin.debug: - msg: | - Host: {{ inventory_hostname }} ({{ host_description | default('no description') }}) - Uptime: {{ uptime_result.stdout }} - Docker: {{ docker_status.stdout_lines | default(['N/A']) | join('\n') }}