Skip to content
This repository was archived by the owner on May 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/actions/ansible-deploy/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Ansible Deploy
description: Run an Ansible playbook with vault decryption enabled

inputs:
ansible-directory:
description: Directory containing the Ansible project
required: false
default: "ansible"
playbook-path:
description: Relative path to the playbook to execute
required: false
default: "playbooks/deploy.yml"
inventory-path:
description: Relative path to the inventory file
required: false
default: "inventory/hosts.ini"
vault-password:
description: Vault password used to decrypt encrypted vars
required: true
tags:
description: Comma-separated tag list to execute
required: false
default: "app_deploy"

outputs:
log-path:
description: Path to the saved ansible-playbook log file
value: ${{ steps.deploy.outputs.log-path }}

runs:
using: composite
steps:
- id: deploy
name: Run ansible-playbook
shell: bash
working-directory: ${{ inputs.ansible-directory }}
env:
VAULT_PASSWORD: ${{ inputs.vault-password }}
PLAYBOOK_PATH: ${{ inputs.playbook-path }}
INVENTORY_PATH: ${{ inputs.inventory-path }}
PLAYBOOK_TAGS: ${{ inputs.tags }}
run: |
set -euo pipefail
umask 077

log_path="${RUNNER_TEMP}/ansible-deploy.log"

cleanup() {
rm -f .vault_pass
}
trap cleanup EXIT

printf '%s\n' "$VAULT_PASSWORD" > .vault_pass

ansible-playbook "$PLAYBOOK_PATH" -i "$INVENTORY_PATH" --tags "$PLAYBOOK_TAGS" | tee "$log_path"

echo "log-path=$log_path" >> "$GITHUB_OUTPUT"
39 changes: 39 additions & 0 deletions .github/actions/ansible-lint/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Ansible Lint
description: Run ansible-lint and syntax checks with vault access

inputs:
ansible-directory:
description: Directory containing the Ansible project
required: false
default: "ansible"
vault-password:
description: Vault password used to decrypt encrypted vars during linting
required: true
playbook-glob:
description: Playbook glob for ansible-lint
required: false
default: "playbooks/*.yml"

runs:
using: composite
steps:
- name: Run ansible-lint and syntax checks
shell: bash
working-directory: ${{ inputs.ansible-directory }}
env:
VAULT_PASSWORD: ${{ inputs.vault-password }}
PLAYBOOK_GLOB: ${{ inputs.playbook-glob }}
run: |
set -euo pipefail
umask 077
cleanup() {
rm -f .vault_pass
}
trap cleanup EXIT

printf '%s\n' "$VAULT_PASSWORD" > .vault_pass

ansible-lint $PLAYBOOK_GLOB
ansible-playbook playbooks/provision.yml --syntax-check
ansible-playbook playbooks/deploy.yml --syntax-check
ansible-playbook playbooks/site.yml --syntax-check
59 changes: 59 additions & 0 deletions .github/actions/ansible-setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Ansible Setup
description: Set up a Python-based Ansible toolchain and required collections

inputs:
python-version:
description: Python version to install
required: false
default: "3.12"
working-directory:
description: Directory containing the Ansible project
required: false
default: "ansible"
python-requirements-path:
description: Path to the pip requirements file
required: false
default: "ansible/requirements-ci.txt"
collection-requirements-path:
description: Path to the ansible-galaxy requirements file
required: false
default: "ansible/requirements.yml"

runs:
using: composite
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}

- name: Cache Ansible toolchain
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.ansible/collections
key: ${{ runner.os }}-py${{ inputs.python-version }}-ansible-${{ hashFiles(inputs.python-requirements-path, inputs.collection-requirements-path) }}
restore-keys: |
${{ runner.os }}-py${{ inputs.python-version }}-ansible-

- name: Install Python dependencies
shell: bash
run: |
set -euo pipefail
rm -rf "${{ inputs.working-directory }}/.venv-ci"
python -m venv "${{ inputs.working-directory }}/.venv-ci"
. "${{ inputs.working-directory }}/.venv-ci/bin/activate"
python -m pip install --upgrade pip
python -m pip install -r "${{ inputs.python-requirements-path }}"

- name: Install Ansible collections
shell: bash
run: |
set -euo pipefail
. "${{ inputs.working-directory }}/.venv-ci/bin/activate"
ansible-galaxy collection install -r "${{ inputs.collection-requirements-path }}"

- name: Add Ansible venv to PATH
shell: bash
run: echo "${{ github.workspace }}/${{ inputs.working-directory }}/.venv-ci/bin" >> "$GITHUB_PATH"
41 changes: 41 additions & 0 deletions .github/actions/ansible-ssh-setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Ansible SSH Setup
description: Install the SSH key material required for Ansible access

inputs:
ssh-private-key:
description: Private SSH key used to connect to the target VM
required: true
ssh-key-path:
description: Destination path for the private key
required: false
default: "~/.ssh/vagrant"
known-host:
description: Optional host to add to known_hosts
required: false
default: ""

runs:
using: composite
steps:
- name: Configure SSH credentials
shell: bash
env:
SSH_PRIVATE_KEY: ${{ inputs.ssh-private-key }}
SSH_KEY_PATH: ${{ inputs.ssh-key-path }}
KNOWN_HOST: ${{ inputs.known-host }}
run: |
set -euo pipefail

key_path="${SSH_KEY_PATH/#\~/$HOME}"

install -d -m 700 "$HOME/.ssh"
install -d -m 700 "$(dirname "$key_path")"
printf '%s\n' "$SSH_PRIVATE_KEY" > "$key_path"
chmod 600 "$key_path"

touch "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"

if [ -n "$KNOWN_HOST" ]; then
ssh-keyscan -H "$KNOWN_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
fi
50 changes: 50 additions & 0 deletions .github/actions/http-healthcheck/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: HTTP Healthcheck
description: Poll an HTTP endpoint until it returns healthy JSON

inputs:
url:
description: URL to poll
required: true
retries:
description: Number of polling attempts before failure
required: false
default: "10"
delay-seconds:
description: Delay between retries in seconds
required: false
default: "3"
jq-filter:
description: jq expression that must evaluate to true
required: false
default: '.status == "healthy"'

runs:
using: composite
steps:
- name: Poll health endpoint
shell: bash
env:
URL: ${{ inputs.url }}
RETRIES: ${{ inputs.retries }}
DELAY_SECONDS: ${{ inputs.delay-seconds }}
JQ_FILTER: ${{ inputs.jq-filter }}
run: |
set -euo pipefail

response=""

for attempt in $(seq 1 "$RETRIES"); do
if response="$(curl -fsSL "$URL")"; then
break
fi

if [ "$attempt" -eq "$RETRIES" ]; then
echo "Health check failed after $RETRIES attempts: $URL" >&2
exit 1
fi

sleep "$DELAY_SECONDS"
done

echo "$response" | jq .
echo "$response" | jq -e "$JQ_FILTER" >/dev/null
147 changes: 147 additions & 0 deletions .github/workflows/ansible-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: Ansible Deployment

on:
push:
branches:
- master
- "lab*"
paths:
- ansible/**
- .github/actions/ansible-setup/**
- .github/actions/ansible-lint/**
- .github/actions/ansible-ssh-setup/**
- .github/actions/ansible-deploy/**
- .github/actions/http-healthcheck/**
- .github/workflows/ansible-deploy.yml
- "!ansible/docs/**"
pull_request:
branches:
- master
paths:
- ansible/**
- .github/actions/ansible-setup/**
- .github/actions/ansible-lint/**
- .github/actions/ansible-ssh-setup/**
- .github/actions/ansible-deploy/**
- .github/actions/http-healthcheck/**
- .github/workflows/ansible-deploy.yml
- "!ansible/docs/**"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ansible-deploy-${{ github.ref }}
cancel-in-progress: true

env:
ANSIBLE_DIRECTORY: ansible
DEPLOY_PLAYBOOK: playbooks/deploy.yml
DEPLOY_TAGS: app_deploy

jobs:
lint:
name: Ansible Lint
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Ansible toolchain
uses: ./.github/actions/ansible-setup

- name: Run lint and syntax checks
uses: ./.github/actions/ansible-lint
with:
ansible-directory: ${{ env.ANSIBLE_DIRECTORY }}
vault-password: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}

deploy:
name: Deploy Application
needs: lint
if: github.event_name != 'pull_request'
runs-on:
- self-hosted
- linux
- vagrant
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Ansible toolchain
uses: ./.github/actions/ansible-setup

- name: Resolve target host from inventory
working-directory: ${{ env.ANSIBLE_DIRECTORY }}
run: |
set -euo pipefail
target_host="$(
awk '
/^[[:space:]]*#/ { next }
/^\[/ { next }
NF {
for (i = 1; i <= NF; i++) {
if ($i ~ /^ansible_host=/) {
split($i, value, "=")
print value[2]
exit
}
}
}
' inventory/hosts.ini
)"

if [ -z "$target_host" ]; then
echo "Could not determine ansible_host from inventory/hosts.ini" >&2
exit 1
fi

echo "TARGET_VM_HOST=$target_host" >> "$GITHUB_ENV"

- name: Configure SSH access to the target VM
uses: ./.github/actions/ansible-ssh-setup
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
known-host: ${{ env.TARGET_VM_HOST }}

- name: Prepare vault password file
working-directory: ${{ env.ANSIBLE_DIRECTORY }}
env:
VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
run: |
set -euo pipefail
umask 077
printf '%s\n' "$VAULT_PASSWORD" > .vault_pass

- name: Verify target connectivity
working-directory: ${{ env.ANSIBLE_DIRECTORY }}
run: ansible webservers -m ansible.builtin.ping

- id: deploy
name: Deploy web application
uses: ./.github/actions/ansible-deploy
with:
ansible-directory: ${{ env.ANSIBLE_DIRECTORY }}
playbook-path: ${{ env.DEPLOY_PLAYBOOK }}
vault-password: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
tags: ${{ env.DEPLOY_TAGS }}

- name: Upload deployment log
if: always() && steps.deploy.outputs.log-path != ''
uses: actions/upload-artifact@v4
with:
name: ansible-deploy-log
path: ${{ steps.deploy.outputs.log-path }}

- name: Verify application health
uses: ./.github/actions/http-healthcheck
with:
url: http://${{ env.TARGET_VM_HOST }}:5000/health

- name: Remove vault password file
if: always()
working-directory: ${{ env.ANSIBLE_DIRECTORY }}
run: rm -f .vault_pass
Loading