Skip to content
Open
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
99 changes: 99 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# https://github.com/gitattributes/gitattributes/blob/fddc586cf0f10ec4485028d0d2dd6f73197a4258/Common.gitattributes
# Common settings that generally should always be used with your language specific settings

# Auto detect text files and perform LF normalization
* text=auto

#
# The above will handle all files NOT found below
#

# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text eol=crlf
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain

# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary

# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.ksh text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text

# Archives
*.7z binary
*.bz binary
*.bz2 binary
*.bzip2 binary
*.gz binary
*.lz binary
*.lzma binary
*.rar binary
*.tar binary
*.taz binary
*.tbz binary
*.tbz2 binary
*.tgz binary
*.tlz binary
*.txz binary
*.xz binary
*.Z binary
*.zip binary
*.zst binary

# Text files where line endings should be preserved
*.patch -text

#
# Exclude files from exporting
#

.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
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@v6
with:
python-version: ${{ inputs.python-version }}

- name: Cache Ansible toolchain
uses: actions/cache@v5
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
45 changes: 45 additions & 0 deletions .github/actions/python-setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Python uv Setup
description: Set up Python + uv, cache dependencies, and install project deps

inputs:
python-version:
description: Python version to install
required: false
default: "3.14"
uv-version:
description: uv version to install
required: false
default: "0.11.14"
working-directory:
description: Project directory containing pyproject.toml
required: false
default: "app_python"
lockfile-path:
description: Path to uv.lock for cache key invalidation
required: false
default: "app_python/uv.lock"
install-args:
description: Extra arguments passed to uv sync
required: false
default: "--locked"

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

- name: Setup uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
enable-cache: true
cache-dependency-glob: ${{ inputs.lockfile-path }}

- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync ${{ inputs.install-args }}
Loading