From a9554f3e5df61108640c37a9d60d4860eb5efa4f Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 15 Jan 2026 15:34:07 +0100 Subject: [PATCH] Initial commit: nult - Ansible deployment toolkit Merged from veridion-gitea and veridion-act-runner-gitea repos. nult (Null-T) - instant teleportation from Strugatsky's Noon Universe. Like Null-T, this toolkit instantly deploys infrastructure. Roles: - gitea: Gitea server with PostgreSQL (Docker Compose) - act_runner: Gitea Actions runner Playbooks: - gitea.yml: Deploy Gitea server - act-runner.yml: Deploy Act Runner - site.yml: Deploy all services Co-Authored-By: Claude Opus 4.5 --- .ansible-lint | 23 +++ .gitignore | 45 ++++++ act-runner.yml | 53 ++++++ gitea.yml | 23 +++ group_vars/all/vars.yml | 53 ++++++ group_vars/all/vault.yml.example | 29 ++++ inventory/hosts.yml | 37 +++++ roles/act_runner/defaults/main.yml | 99 ++++++++++++ roles/act_runner/handlers/main.yml | 26 +++ roles/act_runner/tasks/binary.yml | 88 ++++++++++ roles/act_runner/tasks/config.yml | 56 +++++++ roles/act_runner/tasks/docker.yml | 91 +++++++++++ roles/act_runner/tasks/main.yml | 75 +++++++++ roles/act_runner/tasks/nodejs.yml | 54 +++++++ roles/act_runner/tasks/systemd.yml | 37 +++++ roles/act_runner/tasks/user.yml | 54 +++++++ roles/act_runner/tasks/verify.yml | 80 ++++++++++ .../templates/act_runner.service.j2 | 71 ++++++++ roles/act_runner/templates/config.yaml.j2 | 113 +++++++++++++ roles/gitea/defaults/main.yml | 151 ++++++++++++++++++ roles/gitea/handlers/main.yml | 23 +++ roles/gitea/tasks/backup.yml | 138 ++++++++++++++++ roles/gitea/tasks/config.yml | 139 ++++++++++++++++ roles/gitea/tasks/deploy.yml | 126 +++++++++++++++ roles/gitea/tasks/main.yml | 39 +++++ roles/gitea/tasks/preflight.yml | 144 +++++++++++++++++ roles/gitea/tasks/upgrade.yml | 67 ++++++++ roles/gitea/templates/docker-compose.yml.j2 | 72 +++++++++ site.yml | 23 +++ 29 files changed, 2029 insertions(+) create mode 100644 .ansible-lint create mode 100644 .gitignore create mode 100644 act-runner.yml create mode 100644 gitea.yml create mode 100644 group_vars/all/vars.yml create mode 100644 group_vars/all/vault.yml.example create mode 100644 inventory/hosts.yml create mode 100644 roles/act_runner/defaults/main.yml create mode 100644 roles/act_runner/handlers/main.yml create mode 100644 roles/act_runner/tasks/binary.yml create mode 100644 roles/act_runner/tasks/config.yml create mode 100644 roles/act_runner/tasks/docker.yml create mode 100644 roles/act_runner/tasks/main.yml create mode 100644 roles/act_runner/tasks/nodejs.yml create mode 100644 roles/act_runner/tasks/systemd.yml create mode 100644 roles/act_runner/tasks/user.yml create mode 100644 roles/act_runner/tasks/verify.yml create mode 100644 roles/act_runner/templates/act_runner.service.j2 create mode 100644 roles/act_runner/templates/config.yaml.j2 create mode 100644 roles/gitea/defaults/main.yml create mode 100644 roles/gitea/handlers/main.yml create mode 100644 roles/gitea/tasks/backup.yml create mode 100644 roles/gitea/tasks/config.yml create mode 100644 roles/gitea/tasks/deploy.yml create mode 100644 roles/gitea/tasks/main.yml create mode 100644 roles/gitea/tasks/preflight.yml create mode 100644 roles/gitea/tasks/upgrade.yml create mode 100644 roles/gitea/templates/docker-compose.yml.j2 create mode 100644 site.yml diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..ccc17e4 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,23 @@ +# ============================================================================= +# Ansible-lint Configuration +# ============================================================================= +# See: https://ansible.readthedocs.io/projects/lint/configuring/ + +# Use production profile (strictest) +profile: production + +# Exclude paths +exclude_paths: + - .git/ + - .gitignore + +# Enable offline mode (don't download roles/collections) +offline: true + +# Warn about these rules instead of failing +warn_list: + - experimental + +# Skip these rules entirely (if needed) +# skip_list: +# - yaml[line-length] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8eab61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# ============================================================================= +# Gitignore for Ansible Gitea Deployment +# ============================================================================= +# Reference: https://docs.ansible.com/ansible/latest/vault_guide/index.html + +# Encrypted vault files (contain secrets) +# Users should create these locally with: ansible-vault create +group_vars/all/vault.yml +group_vars/*/vault.yml +**/vault.yml +!**/vault.yml.example + +# Vault password files +.vault_pass +.vault_password +*.vault_pass + +# Ansible retry files +*.retry + +# Python bytecode +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +.venv/ +venv/ +ENV/ + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.bak +*.log diff --git a/act-runner.yml b/act-runner.yml new file mode 100644 index 0000000..0fb5907 --- /dev/null +++ b/act-runner.yml @@ -0,0 +1,53 @@ +--- +# ============================================================================= +# Gitea Act Runner Deployment Playbook +# ============================================================================= +# +# Deploys and configures Gitea Act Runner on Ubuntu servers. +# +# Usage: +# ansible-playbook -i inventory/hosts.yml act-runner.yml --ask-vault-pass +# +# Dry run: +# ansible-playbook -i inventory/hosts.yml act-runner.yml --check --diff --ask-vault-pass +# +# ============================================================================= + +- name: Deploy Gitea Act Runner + hosts: runner_servers + become: true + gather_facts: true + + pre_tasks: + - name: Validate target operating system + ansible.builtin.assert: + that: + - ansible_facts['distribution'] == "Ubuntu" + - ansible_facts['distribution_major_version'] | int >= 20 + fail_msg: >- + This playbook requires Ubuntu 20.04 or later. + Detected: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} + + - name: Update apt package cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + roles: + - role: act_runner + tags: [act_runner] + + post_tasks: + - name: Display deployment summary + ansible.builtin.debug: + msg: + - "==============================================" + - "Gitea Act Runner - Deployment Complete" + - "==============================================" + - "Runner name: {{ act_runner_name }}" + - "Gitea instance: {{ gitea_instance_url }}" + - "Service status: {{ 'RUNNING' if act_runner_service_status.status.ActiveState == 'active' else 'NOT RUNNING' }}" + - "" + - "Verify in Gitea UI:" + - " {{ gitea_instance_url }}/-/admin/actions/runners" + - "==============================================" diff --git a/gitea.yml b/gitea.yml new file mode 100644 index 0000000..622cfe1 --- /dev/null +++ b/gitea.yml @@ -0,0 +1,23 @@ +--- +# ============================================================================= +# Gitea Deployment Playbook +# ============================================================================= +# +# Deploys and configures Gitea with PostgreSQL using Docker Compose. +# Includes backup, domain configuration, and security hardening. +# +# Usage: +# ansible-playbook -i inventory/hosts.yml gitea.yml --ask-vault-pass +# +# Dry run: +# ansible-playbook -i inventory/hosts.yml gitea.yml --check --diff --ask-vault-pass +# +# ============================================================================= + +- name: Deploy Gitea + hosts: gitea_servers + gather_facts: true + + roles: + - role: gitea + tags: [gitea] diff --git a/group_vars/all/vars.yml b/group_vars/all/vars.yml new file mode 100644 index 0000000..74565da --- /dev/null +++ b/group_vars/all/vars.yml @@ -0,0 +1,53 @@ +--- +# ============================================================================= +# Group Variables - All Hosts +# ============================================================================= +# +# Maps vault secrets to role variables and sets common overrides. +# Vault variables (prefixed with vault_) are stored encrypted in vault.yml. +# +# HOW TO USE: +# 1. Create the vault: ansible-vault create group_vars/all/vault.yml +# 2. Add your secrets to the vault (see vault.yml.example) +# 3. The mappings below will reference those vault variables +# +# See: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html +# ============================================================================= + +# ============================================================================= +# Gitea Server Configuration +# ============================================================================= +# Used by: roles/gitea + +# Domain configuration +gitea_domain: "{{ vault_gitea_domain }}" +gitea_ssh_domain: "{{ gitea_domain }}" +gitea_root_url: "https://{{ gitea_domain }}" + +# Database credentials +gitea_db_password: "{{ vault_gitea_db_password }}" + +# ACME/TLS configuration +gitea_acme_email: "{{ vault_gitea_acme_email | default('') }}" + +# ============================================================================= +# Act Runner Configuration +# ============================================================================= +# Used by: roles/act_runner + +# Gitea instance URL (e.g., "https://git.example.com") +gitea_instance_url: "{{ vault_gitea_instance_url }}" + +# Registration token from Gitea admin panel +# Get it from: {{ gitea_instance_url }}/-/admin/actions/runners +act_runner_token: "{{ vault_act_runner_token }}" + +# Package registry hostname (usually same as Gitea host, without https://) +gitea_registry: "{{ vault_gitea_registry }}" + +# Service account username for package registry authentication +gitea_actions_user: "{{ vault_gitea_actions_user }}" + +# Personal Access Token (PAT) for package registry +# Create at: {{ gitea_instance_url }}/user/settings/applications +gitea_packages_token: "{{ vault_gitea_packages_token }}" diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example new file mode 100644 index 0000000..fa47285 --- /dev/null +++ b/group_vars/all/vault.yml.example @@ -0,0 +1,29 @@ +--- +# ============================================================================= +# Vault Secrets Example +# ============================================================================= +# +# Copy this file and encrypt it: +# cp vault.yml.example vault.yml +# ansible-vault encrypt vault.yml +# +# Or create directly: +# ansible-vault create vault.yml +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Gitea Server Secrets +# ----------------------------------------------------------------------------- +vault_gitea_domain: "git.example.com" +vault_gitea_db_password: "your-secure-database-password" +vault_gitea_acme_email: "admin@example.com" + +# ----------------------------------------------------------------------------- +# Act Runner Secrets +# ----------------------------------------------------------------------------- +vault_gitea_instance_url: "https://git.example.com" +vault_act_runner_token: "runner-registration-token-from-gitea-admin" +vault_gitea_registry: "git.example.com" +vault_gitea_actions_user: "gitea_actions" +vault_gitea_packages_token: "personal-access-token-for-packages" diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..6533ad2 --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,37 @@ +--- +# ============================================================================= +# Ansible Inventory - nult +# ============================================================================= +# Null-T: Instant teleportation from Strugatsky's Noon Universe. +# Like Null-T, this toolkit instantly deploys infrastructure. +# +# Servers are grouped by role. A host can belong to multiple groups. +# ============================================================================= + +all: + children: + # ------------------------------------------------------------------------- + # Gitea Servers + # ------------------------------------------------------------------------- + gitea_servers: + hosts: + gitea-primary: + ansible_host: 51.250.71.151 + gitea_install_dir: /home/parf/gitea + + # ------------------------------------------------------------------------- + # Act Runner Servers + # ------------------------------------------------------------------------- + runner_servers: + hosts: + tralalero-tralala: + ansible_host: 147.45.100.33 + ansible_user: root + # act_runner_name: "custom-name" # Optional override + + # --------------------------------------------------------------------------- + # Global Variables + # --------------------------------------------------------------------------- + vars: + ansible_user: parf + ansible_become: true diff --git a/roles/act_runner/defaults/main.yml b/roles/act_runner/defaults/main.yml new file mode 100644 index 0000000..c4998e4 --- /dev/null +++ b/roles/act_runner/defaults/main.yml @@ -0,0 +1,99 @@ +--- +# ============================================================================= +# Gitea Act Runner - Role Default Variables +# ============================================================================= +# +# This file defines configurable variables for the act_runner role. +# Override these in group_vars/all.yml or inventory host_vars as needed. +# +# REQUIRED VARIABLES (must be set in vault - not defined here): +# - gitea_instance_url : URL of your Gitea instance +# - act_runner_token : Registration token from Gitea +# - gitea_packages_token : PAT for package registry access +# - gitea_registry : Package registry hostname +# - gitea_actions_user : Service account username +# +# See group_vars/vault.yml.example for how to set these secrets. +# See: https://docs.gitea.com/usage/actions/act-runner +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Act Runner Binary Configuration +# ----------------------------------------------------------------------------- + +# Version of act_runner to install. +# Check available versions at: https://dl.gitea.com/act_runner/ +# Format: semantic version string without 'v' prefix. +act_runner_version: "0.2.13" + +# Target CPU architecture for the binary download. +# Valid values: "amd64" (x86_64), "arm64" (aarch64) +act_runner_arch: "amd64" + +# Whether to verify SHA256 checksum after downloading the binary. +# STRONGLY RECOMMENDED: Leave as true for security. +act_runner_verify_checksum: true + +# Filesystem path where the act_runner binary will be installed. +# /usr/local/bin is the standard FHS location for locally installed binaries. +act_runner_bin_path: "/usr/local/bin/act_runner" + +# ----------------------------------------------------------------------------- +# System User Configuration +# ----------------------------------------------------------------------------- + +# Unix username for running the act_runner service. +# Will be created as a system user if it doesn't exist. +act_runner_user: "act_runner" + +# Unix group for the act_runner user. +act_runner_group: "act_runner" + +# Home directory for the act_runner user. +# Stores: .runner file, cache, working directories. +act_runner_home: "/home/act_runner" + +# Directory for act_runner configuration files. +# Stores: config.yaml +act_runner_config_dir: "/etc/act_runner" + +# ----------------------------------------------------------------------------- +# Node.js Configuration +# ----------------------------------------------------------------------------- + +# Node.js major version to install via NodeSource. +# Required for JavaScript-based GitHub Actions. +# Valid values: "18", "20", "22", "24" +# See: https://nodejs.org/en/about/previous-releases +act_runner_nodejs_version: "24" + +# ----------------------------------------------------------------------------- +# Runner Configuration +# ----------------------------------------------------------------------------- + +# Human-readable name for this runner instance. +# Displayed in Gitea UI under Actions > Runners. +# Default: server's hostname +act_runner_name: "{{ ansible_facts['hostname'] }}" + +# Labels determine which jobs this runner can execute. +# Format: "label-name:executor" +# Executors: +# - "host" : Run directly on the host system +# - "docker://image" : Run in Docker container +# +# Examples: +# - "ubuntu-latest:host" +# - "ubuntu-latest:docker://node:24" +# - "self-hosted:host" +act_runner_labels: + - "ubuntu-latest:host" + +# ----------------------------------------------------------------------------- +# Container Behavior +# ----------------------------------------------------------------------------- + +# Whether to always pull container images before running jobs. +# true: Ensures latest image (recommended for CI/CD) +# false: Uses cached image if available (faster) +act_runner_container_force_pull: true diff --git a/roles/act_runner/handlers/main.yml b/roles/act_runner/handlers/main.yml new file mode 100644 index 0000000..7193389 --- /dev/null +++ b/roles/act_runner/handlers/main.yml @@ -0,0 +1,26 @@ +--- +# ============================================================================= +# Gitea Act Runner - Ansible Handlers +# ============================================================================= +# +# Handlers are triggered by 'notify' in tasks and run once at the end of play. +# They provide a way to restart services only when configuration changes. +# +# Usage in tasks: +# - name: Deploy config +# template: ... +# notify: restart act_runner +# +# ============================================================================= + +# Reload systemd configuration after unit file changes. +# Required before systemctl can see updated unit files. +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +# Restart the act_runner service to apply configuration changes. +- name: Restart act_runner + ansible.builtin.systemd: + name: act_runner + state: restarted diff --git a/roles/act_runner/tasks/binary.yml b/roles/act_runner/tasks/binary.yml new file mode 100644 index 0000000..0dc2771 --- /dev/null +++ b/roles/act_runner/tasks/binary.yml @@ -0,0 +1,88 @@ +--- +# ============================================================================= +# Gitea Act Runner - Binary Installation +# ============================================================================= +# +# Downloads and installs the act_runner binary from: +# https://dl.gitea.com/act_runner/ +# +# Security: Binary integrity is verified via SHA256 checksum. +# +# ============================================================================= + +# Construct download URLs based on version and architecture. +- name: Set act_runner download URLs + ansible.builtin.set_fact: + act_runner_download_url: >- + https://dl.gitea.com/act_runner/{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-{{ act_runner_arch }} + act_runner_checksum_url: >- + https://dl.gitea.com/act_runner/{{ act_runner_version }}/act_runner-{{ act_runner_version }}-linux-{{ act_runner_arch }}.sha256 + +# Download the act_runner binary to a temporary location. +- name: Download act_runner binary + ansible.builtin.get_url: + url: "{{ act_runner_download_url }}" + dest: /tmp/act_runner + mode: '0755' + +# Download checksum file for verification (when enabled). +- name: Download act_runner checksum + ansible.builtin.get_url: + url: "{{ act_runner_checksum_url }}" + dest: /tmp/act_runner.sha256 + mode: '0644' + when: act_runner_verify_checksum + +# Read the expected checksum from the downloaded file. +- name: Read expected checksum + ansible.builtin.slurp: + src: /tmp/act_runner.sha256 + register: act_runner_expected_checksum_file + when: act_runner_verify_checksum + +# Parse the checksum (format: "checksum filename"). +- name: Parse expected checksum + ansible.builtin.set_fact: + act_runner_expected_checksum: "{{ (act_runner_expected_checksum_file.content | b64decode).split()[0] }}" + when: act_runner_verify_checksum + +# Calculate actual checksum of downloaded binary. +- name: Calculate actual checksum + ansible.builtin.stat: + path: /tmp/act_runner + checksum_algorithm: sha256 + register: act_runner_actual_checksum + when: act_runner_verify_checksum + +# Verify checksums match (fail if tampered). +- name: Verify checksum matches + ansible.builtin.assert: + that: + - act_runner_actual_checksum.stat.checksum == act_runner_expected_checksum + fail_msg: >- + Checksum verification FAILED! + Expected: {{ act_runner_expected_checksum }} + Actual: {{ act_runner_actual_checksum.stat.checksum }} + The downloaded binary may have been tampered with. + success_msg: "Checksum verified: {{ act_runner_expected_checksum }}" + when: act_runner_verify_checksum + +# Install binary to final location. +- name: Install act_runner binary + ansible.builtin.copy: + src: /tmp/act_runner + dest: "{{ act_runner_bin_path }}" + remote_src: true + owner: root + group: root + mode: '0755' + notify: Restart act_runner + +# Clean up temporary files. +- name: Clean up temporary files + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /tmp/act_runner + - /tmp/act_runner.sha256 diff --git a/roles/act_runner/tasks/config.yml b/roles/act_runner/tasks/config.yml new file mode 100644 index 0000000..5d5170c --- /dev/null +++ b/roles/act_runner/tasks/config.yml @@ -0,0 +1,56 @@ +--- +# ============================================================================= +# Gitea Act Runner - Configuration and Registration +# ============================================================================= +# +# Deploys the runner configuration and registers with Gitea. +# Registration is idempotent: only runs if .runner file doesn't exist. +# +# The .runner file contains the runner's identity after registration. +# DO NOT DELETE this file or re-registration will be required. +# +# ============================================================================= + +# Deploy configuration file from template. +- name: Deploy act_runner configuration + ansible.builtin.template: + src: config.yaml.j2 + dest: "{{ act_runner_config_dir }}/config.yaml" + owner: "{{ act_runner_user }}" + group: "{{ act_runner_group }}" + mode: '0640' # Restrictive: contains secrets + notify: Restart act_runner + +# Check if runner is already registered. +# The .runner file is created during registration and persists. +- name: Check if runner is already registered + ansible.builtin.stat: + path: "{{ act_runner_home }}/.runner" + register: act_runner_runner_file + +# Register the runner with Gitea (only if not already registered). +# This is a one-time operation that creates the .runner file. +- name: Register runner with Gitea + ansible.builtin.command: + cmd: >- + {{ act_runner_bin_path }} register + --no-interactive + --config {{ act_runner_config_dir }}/config.yaml + --instance {{ gitea_instance_url }} + --token {{ act_runner_token }} + --name {{ act_runner_name }} + --labels {{ act_runner_labels | join(',') }} + chdir: "{{ act_runner_home }}" + become: true + become_user: "{{ act_runner_user }}" + when: not act_runner_runner_file.stat.exists + register: act_runner_registration_result + changed_when: act_runner_registration_result.rc == 0 + # Don't show token in logs + no_log: true + +# Display registration result (without sensitive data). +- name: Display registration status + ansible.builtin.debug: + msg: >- + Runner registration: {{ 'NEW - registered successfully' if act_runner_registration_result.changed | default(false) else 'EXISTING - already registered' }} diff --git a/roles/act_runner/tasks/docker.yml b/roles/act_runner/tasks/docker.yml new file mode 100644 index 0000000..0871746 --- /dev/null +++ b/roles/act_runner/tasks/docker.yml @@ -0,0 +1,91 @@ +--- +# ============================================================================= +# Gitea Act Runner - Docker Installation +# ============================================================================= +# +# Installs Docker CE following the official Docker documentation: +# https://docs.docker.com/engine/install/ubuntu/ +# +# Uses DEB822 format (.sources) as per current Docker documentation. +# +# ============================================================================= + +# Remove old/conflicting Docker packages that may interfere. +- name: Remove conflicting Docker packages + ansible.builtin.apt: + name: + - docker.io + - docker-doc + - docker-compose + - docker-compose-v2 + - podman-docker + - containerd + - runc + state: absent + failed_when: false + +# Install packages required to add Docker's APT repository. +- name: Install Docker prerequisites + ansible.builtin.apt: + name: + - ca-certificates + - curl + state: present + +# Create directory for APT keyrings. +- name: Create keyrings directory + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + +# Download Docker's official GPG key. +- name: Download Docker GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + +# Get the dpkg architecture for the repository URL. +- name: Get dpkg architecture + ansible.builtin.command: dpkg --print-architecture + register: act_runner_dpkg_arch + changed_when: false + +# Add Docker's APT repository using DEB822 format. +# This is the modern format recommended by Docker documentation. +- name: Add Docker APT repository (DEB822 format) + ansible.builtin.copy: + dest: /etc/apt/sources.list.d/docker.sources + mode: '0644' + content: | + Types: deb + URIs: https://download.docker.com/linux/ubuntu + Suites: {{ ansible_facts['distribution_release'] }} + Components: stable + Architectures: {{ act_runner_dpkg_arch.stdout }} + Signed-By: /etc/apt/keyrings/docker.asc + +# Update apt cache after adding repository. +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 0 # Force update since we just added a new repo + +# Install Docker CE and related packages. +- name: Install Docker packages + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + +# Ensure Docker service is running and enabled on boot. +- name: Ensure Docker service is started and enabled + ansible.builtin.systemd: + name: docker + state: started + enabled: true diff --git a/roles/act_runner/tasks/main.yml b/roles/act_runner/tasks/main.yml new file mode 100644 index 0000000..498c5ac --- /dev/null +++ b/roles/act_runner/tasks/main.yml @@ -0,0 +1,75 @@ +--- +# ============================================================================= +# Gitea Act Runner - Main Task Orchestration +# ============================================================================= +# +# This file orchestrates the act_runner installation in the correct order. +# Each include_tasks imports a focused task file for better maintainability. +# +# Execution order matters: +# 1. Validate inputs (fail fast on missing required values) +# 2. Install Docker (required for container operations in Actions) +# 3. Install Node.js (required for JavaScript-based GitHub Actions) +# 4. Download act_runner binary (the core component) +# 5. Create system user (security: run as unprivileged user) +# 6. Configure and register (connect to Gitea instance) +# 7. Setup systemd service (enable automatic startup) +# 8. Verify installation (ensure everything works) +# +# ============================================================================= + +# Fail early if required variables are not set. +# This prevents partial installations that would be harder to debug. +- name: Validate required variables are defined + ansible.builtin.assert: + that: + - gitea_instance_url is defined + - gitea_instance_url | length > 0 + - act_runner_token is defined + - act_runner_token | length > 0 + - gitea_packages_token is defined + - gitea_packages_token | length > 0 + - gitea_registry is defined + - gitea_registry | length > 0 + - gitea_actions_user is defined + - gitea_actions_user | length > 0 + fail_msg: >- + Missing required variables. Ensure these are set in vault: + gitea_instance_url, act_runner_token, gitea_packages_token, + gitea_registry, gitea_actions_user. + See group_vars/vault.yml.example for details. + success_msg: "All required variables are defined" + +# Docker is needed even for host execution because many GitHub Actions +# use Docker internally (e.g., actions/checkout uses node in container). +- name: Install and configure Docker + ansible.builtin.include_tasks: docker.yml + +# Node.js is required for JavaScript-based GitHub Actions. +# Many popular actions (checkout, cache, upload-artifact) need Node.js. +- name: Install Node.js runtime + ansible.builtin.include_tasks: nodejs.yml + +# Download and install the act_runner binary with checksum verification. +- name: Install act_runner binary + ansible.builtin.include_tasks: binary.yml + +# Create dedicated system user for security isolation. +# The runner should not run as root. +- name: Create act_runner system user + ansible.builtin.include_tasks: user.yml + +# Deploy configuration and register with Gitea instance. +# Registration only happens if .runner file doesn't exist (idempotent). +- name: Configure and register runner + ansible.builtin.include_tasks: config.yml + +# Deploy systemd unit file for service management. +# Enables automatic startup on boot and easy service control. +- name: Setup systemd service + ansible.builtin.include_tasks: systemd.yml + +# Run verification checks to ensure installation succeeded. +# Fails the playbook if any critical component is not working. +- name: Verify installation + ansible.builtin.include_tasks: verify.yml diff --git a/roles/act_runner/tasks/nodejs.yml b/roles/act_runner/tasks/nodejs.yml new file mode 100644 index 0000000..5da7621 --- /dev/null +++ b/roles/act_runner/tasks/nodejs.yml @@ -0,0 +1,54 @@ +--- +# ============================================================================= +# Gitea Act Runner - Node.js Installation +# ============================================================================= +# +# Installs Node.js LTS via NodeSource: +# https://github.com/nodesource/distributions +# +# Node.js is required because: +# - Many GitHub Actions are written in JavaScript +# - Popular actions like checkout, cache, upload-artifact need Node.js +# - The runner itself may need Node.js for certain operations +# +# ============================================================================= + +# Install prerequisites for NodeSource setup script. +- name: Install Node.js prerequisites + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + state: present + +# Download NodeSource setup script. +# This script adds the NodeSource APT repository. +- name: Download NodeSource setup script + ansible.builtin.get_url: + url: "https://deb.nodesource.com/setup_{{ act_runner_nodejs_version }}.x" + dest: /tmp/nodesource_setup.sh + mode: '0755' + +# Execute the NodeSource setup script to configure APT repository. +# Script is safe to re-run (idempotent). +- name: Execute NodeSource setup script + ansible.builtin.command: + cmd: /tmp/nodesource_setup.sh + args: + creates: /etc/apt/sources.list.d/nodesource.list + register: act_runner_nodesource_result + changed_when: act_runner_nodesource_result.rc == 0 + +# Install Node.js from NodeSource repository. +- name: Install Node.js + ansible.builtin.apt: + name: nodejs + state: present + update_cache: true + +# Clean up setup script. +- name: Remove NodeSource setup script + ansible.builtin.file: + path: /tmp/nodesource_setup.sh + state: absent diff --git a/roles/act_runner/tasks/systemd.yml b/roles/act_runner/tasks/systemd.yml new file mode 100644 index 0000000..870d679 --- /dev/null +++ b/roles/act_runner/tasks/systemd.yml @@ -0,0 +1,37 @@ +--- +# ============================================================================= +# Gitea Act Runner - Systemd Service Setup +# ============================================================================= +# +# Creates and enables the systemd service for act_runner. +# This ensures the runner: +# - Starts automatically on boot +# - Restarts if it crashes +# - Can be controlled via systemctl +# +# ============================================================================= + +# Deploy systemd service unit file from template. +- name: Deploy systemd service file + ansible.builtin.template: + src: act_runner.service.j2 + dest: /etc/systemd/system/act_runner.service + owner: root + group: root + mode: '0644' + notify: + - Reload systemd + - Restart act_runner + +# Reload systemd to pick up the new/changed unit file. +# This is immediate (not via handler) to ensure service can be started. +- name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + +# Enable and start the act_runner service. +- name: Enable and start act_runner service + ansible.builtin.systemd: + name: act_runner + state: started + enabled: true diff --git a/roles/act_runner/tasks/user.yml b/roles/act_runner/tasks/user.yml new file mode 100644 index 0000000..c622213 --- /dev/null +++ b/roles/act_runner/tasks/user.yml @@ -0,0 +1,54 @@ +--- +# ============================================================================= +# Gitea Act Runner - System User Setup +# ============================================================================= +# +# Creates a dedicated system user for running the act_runner service. +# Running as an unprivileged user improves security by: +# - Limiting what the service can access +# - Isolating it from other services +# - Following the principle of least privilege +# +# ============================================================================= + +# Create the act_runner system group. +- name: Create act_runner group + ansible.builtin.group: + name: "{{ act_runner_group }}" + state: present + system: true + +# Create the act_runner system user. +- name: Create act_runner user + ansible.builtin.user: + name: "{{ act_runner_user }}" + group: "{{ act_runner_group }}" + # Add to docker group for container access. + groups: docker + append: true + # Use bash shell for better compatibility with actions. + shell: /bin/bash + # Home directory for runner data. + home: "{{ act_runner_home }}" + create_home: true + # System user (no login, low UID). + system: true + state: present + +# Ensure home directory has correct permissions. +- name: Set permissions on home directory + ansible.builtin.file: + path: "{{ act_runner_home }}" + state: directory + owner: "{{ act_runner_user }}" + group: "{{ act_runner_group }}" + mode: '0750' + +# Create configuration directory. +- name: Create configuration directory + ansible.builtin.file: + path: "{{ act_runner_config_dir }}" + state: directory + owner: "{{ act_runner_user }}" + group: "{{ act_runner_group }}" + mode: '0750' diff --git a/roles/act_runner/tasks/verify.yml b/roles/act_runner/tasks/verify.yml new file mode 100644 index 0000000..79ed447 --- /dev/null +++ b/roles/act_runner/tasks/verify.yml @@ -0,0 +1,80 @@ +--- +# ============================================================================= +# Gitea Act Runner - Installation Verification +# ============================================================================= +# +# Verifies that all components were installed correctly. +# Fails the playbook if any critical check doesn't pass. +# +# Verification order: +# 1. Docker daemon is running and accessible +# 2. Node.js is installed at the expected version +# 3. act_runner binary is executable and reports version +# 4. act_runner systemd service is active +# +# ============================================================================= + +# Verify Docker daemon is running and accessible. +- name: Verify Docker daemon is accessible + ansible.builtin.command: docker info + changed_when: false + register: act_runner_docker_info_result + +- name: Display Docker status + ansible.builtin.debug: + msg: "Docker is running: {{ act_runner_docker_info_result.stdout_lines[0] | default('OK') }}" + +# Verify Node.js is installed at the expected major version. +- name: Verify Node.js installation + ansible.builtin.command: node --version + changed_when: false + register: act_runner_node_version_result + +- name: Verify Node.js major version matches expected + ansible.builtin.assert: + that: + - act_runner_node_version_result.stdout is match('^v' ~ act_runner_nodejs_version ~ '\\.') + fail_msg: >- + Node.js version mismatch! + Expected: v{{ act_runner_nodejs_version }}.x + Actual: {{ act_runner_node_version_result.stdout }} + success_msg: "Node.js {{ act_runner_node_version_result.stdout }} installed correctly" + +# Verify act_runner binary is installed and executable. +- name: Verify act_runner binary + ansible.builtin.command: "{{ act_runner_bin_path }} --version" + changed_when: false + register: act_runner_version_result + +- name: Display act_runner version + ansible.builtin.debug: + msg: "act_runner version: {{ act_runner_version_result.stdout }}" + +# Verify the systemd service is running. +- name: Get act_runner service status + ansible.builtin.systemd: + name: act_runner + register: act_runner_service_status + +- name: Verify act_runner service is active + ansible.builtin.assert: + that: + - act_runner_service_status.status.ActiveState == "active" + fail_msg: >- + act_runner service is NOT running! + State: {{ act_runner_service_status.status.ActiveState }} + Check logs: journalctl -u act_runner -n 50 + success_msg: "act_runner service is running (PID: {{ act_runner_service_status.status.MainPID }})" + +# Final summary. +- name: Verification complete + ansible.builtin.debug: + msg: + - "==========================================" + - "All verification checks PASSED!" + - "==========================================" + - "Docker: running" + - "Node.js: {{ act_runner_node_version_result.stdout }}" + - "act_runner: {{ act_runner_version_result.stdout }}" + - "Service: active (PID {{ act_runner_service_status.status.MainPID }})" + - "==========================================" diff --git a/roles/act_runner/templates/act_runner.service.j2 b/roles/act_runner/templates/act_runner.service.j2 new file mode 100644 index 0000000..68336f1 --- /dev/null +++ b/roles/act_runner/templates/act_runner.service.j2 @@ -0,0 +1,71 @@ +# ============================================================================= +# Gitea Act Runner - Systemd Service Unit +# ============================================================================= +# Managed by Ansible - DO NOT EDIT MANUALLY +# +# Common commands: +# systemctl status act_runner - Check service status +# systemctl restart act_runner - Restart the service +# journalctl -u act_runner -f - Follow service logs +# +# See: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html +# ============================================================================= + +[Unit] +# Human-readable description +Description=Gitea Actions runner + +# Documentation link +Documentation=https://gitea.com/gitea/act_runner + +# Start after Docker and network are available +After=docker.service network-online.target + +# Request network-online.target to be started +Wants=network-online.target + +[Service] +# Simple type: process runs in foreground +Type=simple + +# Main command +ExecStart={{ act_runner_bin_path }} daemon --config {{ act_runner_config_dir }}/config.yaml + +# Reload command (sends HUP signal) +ExecReload=/bin/kill -s HUP $MAINPID + +# Working directory +WorkingDirectory={{ act_runner_home }} + +# No timeout for start/stop (jobs may take long) +TimeoutSec=0 + +# Wait before restarting after failure +RestartSec=10 + +# Always restart on any exit +Restart=always + +# Run as unprivileged user +User={{ act_runner_user }} +Group={{ act_runner_group }} + +# --------------------------------------------------------------------------- +# Security Hardening +# --------------------------------------------------------------------------- + +# No new privileges via setuid/setgid +NoNewPrivileges=true + +# Make /usr, /boot, /efi read-only +ProtectSystem=strict + +# Allow writes only to these paths +ReadWritePaths={{ act_runner_home }} {{ act_runner_config_dir }} + +# Private /tmp directory +PrivateTmp=true + +[Install] +# Start on normal boot +WantedBy=multi-user.target diff --git a/roles/act_runner/templates/config.yaml.j2 b/roles/act_runner/templates/config.yaml.j2 new file mode 100644 index 0000000..cd44298 --- /dev/null +++ b/roles/act_runner/templates/config.yaml.j2 @@ -0,0 +1,113 @@ +# ============================================================================= +# Gitea Act Runner - Configuration File +# ============================================================================= +# Managed by Ansible - DO NOT EDIT MANUALLY +# +# To modify settings, update the role variables and re-run the playbook. +# +# Reference: https://docs.gitea.com/usage/actions/act-runner +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Logging Configuration +# ----------------------------------------------------------------------------- +log: + # Log verbosity level. + # Valid values: trace, debug, info, warn, error, fatal + level: info + +# ----------------------------------------------------------------------------- +# Runner Core Configuration +# ----------------------------------------------------------------------------- +runner: + # Path to the runner registration state file. + # Created during 'act_runner register'. DO NOT DELETE. + file: {{ act_runner_home }}/.runner + + # Maximum number of concurrent jobs. + capacity: 1 + + # Environment variables injected into every job. + envs: + # Package registry hostname + registry: {{ gitea_registry }} + + # Service account username + actions_user: {{ gitea_actions_user }} + + # PAT for package registry authentication + PACKAGES_TOKEN: {{ gitea_packages_token }} + + # Optional file for additional environment variables. + env_file: .env + + # Maximum job duration (also limited by Gitea instance). + timeout: 3h + + # Grace period for jobs during shutdown. + shutdown_timeout: 0s + + # Skip TLS verification. WARNING: Security risk if true. + insecure: false + + # Job polling settings. + fetch_timeout: 5s + fetch_interval: 2s + + # Labels determine which jobs this runner handles. + labels: +{% for label in act_runner_labels %} + - "{{ label }}" +{% endfor %} + +# ----------------------------------------------------------------------------- +# Cache Server Configuration +# ----------------------------------------------------------------------------- +cache: + # Enable built-in cache server for actions/cache. + enabled: true + + # Cache storage directory (empty = default). + dir: "" + + # Network settings (empty = auto-detect). + host: "" + port: 0 + + # External cache server URL (empty = use built-in). + external_server: "" + +# ----------------------------------------------------------------------------- +# Container Execution Configuration +# ----------------------------------------------------------------------------- +container: + # Docker network (empty = isolated per job). + network: "" + + # Privileged mode. WARNING: Security risk if true. + privileged: false + + # Additional docker run options. + options: + + # Working directory inside containers. + workdir_parent: + + # Allowed volume mounts (empty = none, ["**"] = any). + valid_volumes: [] + + # Docker daemon (empty = auto-detect). + docker_host: "" + + # Always pull images before jobs. + force_pull: {{ act_runner_container_force_pull | lower }} + + # Rebuild images even if they exist. + force_rebuild: false + +# ----------------------------------------------------------------------------- +# Host Execution Configuration +# ----------------------------------------------------------------------------- +host: + # Working directory for host execution. + workdir_parent: diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml new file mode 100644 index 0000000..50f9b16 --- /dev/null +++ b/roles/gitea/defaults/main.yml @@ -0,0 +1,151 @@ +--- +# ============================================================================= +# Gitea Role Defaults +# ============================================================================= +# +# Default values for Gitea role variables. +# These have the LOWEST precedence and can be overridden by: +# - group_vars/all/vars.yml +# - inventory host_vars +# - command line --extra-vars +# +# Reference: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable +# Reference: https://docs.gitea.com/administration/config-cheat-sheet +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Version Configuration +# ----------------------------------------------------------------------------- +# Using major.minor pinning for automatic security patch updates. +# Reference: https://hub.docker.com/r/gitea/gitea +# Reference: https://hub.docker.com/_/postgres + +gitea_version: "1.25" +gitea_postgres_version: "17-alpine" + +# ----------------------------------------------------------------------------- +# Container Configuration +# ----------------------------------------------------------------------------- +# Service names for docker compose commands +gitea_service_name: "server" +gitea_db_service_name: "db" + +# Container names for docker exec commands (may differ from service names) +gitea_container_name: "gitea" +gitea_db_container_name: "gitea-db-1" + +# User/Group IDs for container processes +# These should match existing volume ownership +gitea_user_uid: 1002 +gitea_user_gid: 1004 + +# ----------------------------------------------------------------------------- +# Database Configuration +# ----------------------------------------------------------------------------- + +gitea_db_type: "postgres" +gitea_db_host: "db:5432" +gitea_db_name: "gitea" +gitea_db_user: "gitea" +# gitea_db_password: MUST be set via vault mapping in group_vars/all/vars.yml + +# ----------------------------------------------------------------------------- +# Network Configuration +# ----------------------------------------------------------------------------- + +# HTTP port inside container (external 443 maps to this via ACME) +gitea_http_port: 3000 + +# SSH port configuration +gitea_ssh_port: 22 +gitea_ssh_listen_port: 22 +gitea_ssh_external_port: 2222 + +# ----------------------------------------------------------------------------- +# Security Hardening - Password & Authentication +# ----------------------------------------------------------------------------- +# Reference: https://docs.gitea.com/administration/config-cheat-sheet#security-security +# Reference: https://onappsec.com/gitea-configuration-hardening/ + +# Password hashing algorithm +# Options: argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt +# argon2 is the strongest and recommended choice +gitea_password_hash_algo: "argon2" + +# Password complexity requirements +# Options: off, lower, upper, digit, spec (comma-separated) +gitea_password_complexity: "lower,upper,digit,spec" + +# Minimum password length (NIST SP 800-63B recommends 8+) +gitea_min_password_length: 12 + +# Check passwords against HaveIBeenPwned database +gitea_password_check_pwn: true + +# Two-factor authentication is enabled by default in Gitea 1.25+ +# No configuration needed - users can enable 2FA in their account settings + +# ----------------------------------------------------------------------------- +# Security Hardening - API & Features +# ----------------------------------------------------------------------------- + +# Disable Git hooks (prevents arbitrary code execution via hooks) +gitea_disable_git_hooks: true + +# Reject API tokens passed in URL query strings (header-based only) +# Prevents token leakage in server logs and browser history +gitea_disable_query_auth_token: true + +# Webhook allowed destination hosts +# Options: loopback, private, external, *, or CIDR list +gitea_webhook_allowed_hosts: "external" + +# ----------------------------------------------------------------------------- +# Security Hardening - Session & Cookies +# ----------------------------------------------------------------------------- + +# Force HTTPS for session cookies +gitea_cookie_secure: true + +# SameSite cookie policy (strict prevents CSRF) +# Options: lax, strict, none +gitea_same_site: "strict" + +# ----------------------------------------------------------------------------- +# Security Hardening - TLS +# ----------------------------------------------------------------------------- + +# Minimum TLS version (disable older vulnerable protocols) +gitea_ssl_min_version: "TLSv1.2" + +# ACME/Let's Encrypt automatic certificate provisioning +gitea_enable_acme: true +gitea_acme_accept_tos: true +gitea_acme_directory: "https" +# gitea_acme_email: SHOULD be set via vault mapping in group_vars/all/vars.yml + +# ----------------------------------------------------------------------------- +# Service Configuration +# ----------------------------------------------------------------------------- + +# Disable public registration (admin-only account creation) +gitea_disable_registration: true + +# Require sign-in to view any content +gitea_require_signin_view: false + +# LFS (Large File Storage) support +gitea_lfs_enabled: true + +# Offline mode (don't fetch external resources like Gravatar) +gitea_offline_mode: true + +# ----------------------------------------------------------------------------- +# Backup Configuration +# ----------------------------------------------------------------------------- + +# Backup directory (relative to gitea_install_dir) +gitea_backup_dir: "backups" + +# Number of backup sets to retain +gitea_backup_retention: 5 diff --git a/roles/gitea/handlers/main.yml b/roles/gitea/handlers/main.yml new file mode 100644 index 0000000..e7d4632 --- /dev/null +++ b/roles/gitea/handlers/main.yml @@ -0,0 +1,23 @@ +--- +# ============================================================================= +# Gitea Role Handlers +# ============================================================================= +# +# Handlers are triggered by 'notify' in tasks and run at the end of the play. +# They're typically used for service restarts after configuration changes. +# +# In this role, most restarts happen via docker compose in deploy.yml, +# so handlers are minimal. These are provided for ad-hoc config changes. +# ============================================================================= + +- name: Restart gitea + ansible.builtin.command: + cmd: "docker compose -f {{ gitea_install_dir }}/docker-compose.yml restart {{ gitea_container_name }}" + listen: "restart gitea" + changed_when: true + +- name: Restart gitea services + ansible.builtin.command: + cmd: "docker compose -f {{ gitea_install_dir }}/docker-compose.yml up -d --wait" + listen: "restart gitea services" + changed_when: true diff --git a/roles/gitea/tasks/backup.yml b/roles/gitea/tasks/backup.yml new file mode 100644 index 0000000..0c1ae77 --- /dev/null +++ b/roles/gitea/tasks/backup.yml @@ -0,0 +1,138 @@ +--- +# ============================================================================= +# Backup Tasks - Using Official Gitea Dump +# ============================================================================= +# +# Uses the official `gitea dump` command for comprehensive backup. +# Reference: https://docs.gitea.com/administration/backup-and-restore +# +# The dump creates a ZIP containing: +# - app.ini and custom/ directory (configuration) +# - All git repositories +# - Database SQL dump (gitea-db.sql) +# - Attachments, avatars, LFS objects +# +# Note: We run the dump while Gitea is running. The official docs recommend +# stopping Gitea for perfect consistency, but docker exec requires a running +# container. The consistency risk during the brief dump operation is minimal. +# ============================================================================= + +# Set up variables for this backup run. +# gitea_backup_timestamp: Used in filename for unique identification (e.g., 20260114T143022) +# gitea_backup_dir_path: Where backups are stored on the host filesystem +- name: Set backup variables + ansible.builtin.set_fact: + gitea_backup_timestamp: "{{ ansible_facts['date_time']['iso8601_basic_short'] }}" + gitea_backup_dir_path: "{{ gitea_install_dir }}/{{ gitea_backup_dir }}" + +# Ensure the backup directory exists on the host. +# Mode 0750: owner read/write/execute, group read/execute, others no access +- name: Ensure backup directory exists + ansible.builtin.file: + path: "{{ gitea_backup_dir_path }}" + state: directory + mode: "0750" + +# ----------------------------------------------------------------------------- +# Create Backup Using Official Gitea Dump +# ----------------------------------------------------------------------------- +# Run the official gitea dump command inside the running container. +# +# Command breakdown: +# docker exec {{ container }} - Execute command inside the container +# /usr/local/bin/gitea dump - The gitea binary with dump subcommand +# -c /data/gitea/conf/app.ini - Path to config file (tells gitea where data is) +# -w /tmp - Working directory for temporary files +# -f /tmp/gitea-backup-*.zip - Output filename for the backup archive +# +# Note: We write to /tmp inside the container first, then copy out. +# This avoids permission issues with mounted volumes. + +- name: Run gitea dump command + ansible.builtin.command: + cmd: >- + docker exec -u git {{ gitea_container_name }} + /usr/local/bin/gitea dump + -c /data/gitea/conf/app.ini + -w /tmp + -f /tmp/gitea-backup-{{ gitea_backup_timestamp }}.zip + register: gitea_dump_result + changed_when: true + +# Copy backup from container to host filesystem +# docker cp copies files between container and host. +# Format: docker cp container:/path/in/container /path/on/host +- name: Copy backup from container to host + ansible.builtin.command: + cmd: >- + docker cp + {{ gitea_container_name }}:/tmp/gitea-backup-{{ gitea_backup_timestamp }}.zip + {{ gitea_backup_dir_path }}/gitea-backup-{{ gitea_backup_timestamp }}.zip + changed_when: true + +# Clean up the backup file inside the container +# We don't want to leave large files in the container's /tmp +# failed_when: false - Don't fail if cleanup fails (backup already succeeded) +- name: Remove backup file from container + ansible.builtin.command: + cmd: >- + docker exec -u git {{ gitea_container_name }} + rm /tmp/gitea-backup-{{ gitea_backup_timestamp }}.zip + changed_when: true + failed_when: false + +# ----------------------------------------------------------------------------- +# Backup Verification +# ----------------------------------------------------------------------------- +# Verify the backup was actually created and is not empty. +# This catches cases where the command succeeded but produced no output. + +- name: Verify backup file exists + ansible.builtin.stat: + path: "{{ gitea_backup_dir_path }}/gitea-backup-{{ gitea_backup_timestamp }}.zip" + register: gitea_backup_file + +# Display backup info for operator visibility +# Size calculation: bytes / 1048576 = megabytes (1024*1024) +- name: Display backup info + ansible.builtin.debug: + msg: >- + Backup completed: {{ gitea_backup_dir_path }}/gitea-backup-{{ gitea_backup_timestamp }}.zip + Size: {{ (gitea_backup_file.stat.size / 1048576) | round(2) }}MB + when: gitea_backup_file.stat.exists | default(false) + +- name: Fail if backup file is missing or empty + ansible.builtin.fail: + msg: "Backup file is missing or empty. Cannot proceed." + when: + - not ansible_check_mode + - not gitea_backup_file.stat.exists or gitea_backup_file.stat.size == 0 + +# ----------------------------------------------------------------------------- +# Backup Rotation +# ----------------------------------------------------------------------------- +# Keep only the N most recent backups (controlled by gitea_backup_retention). +# This prevents disk space from being exhausted by old backups. + +# Find all existing backup files matching our naming pattern +- name: Find old backup files + ansible.builtin.find: + paths: "{{ gitea_backup_dir_path }}" + patterns: "gitea-backup-*.zip" + file_type: file + register: gitea_old_backup_files + +# Delete old backups, keeping only the most recent ones. +# Logic breakdown: +# gitea_old_backup_files.files | sort(attribute='mtime') - Sort by modification time (oldest first) +# [:-gitea_backup_retention] - Slice: all except last N items +# (Python slice notation: [:-5] = all but last 5) +# Example: If we have 7 backups and retention=5, this deletes the 2 oldest. +- name: Remove old backups beyond retention limit + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ (gitea_old_backup_files.files | sort(attribute='mtime'))[:-gitea_backup_retention] }}" + loop_control: + label: "{{ item.path | basename }}" + when: gitea_old_backup_files.files | length > gitea_backup_retention diff --git a/roles/gitea/tasks/config.yml b/roles/gitea/tasks/config.yml new file mode 100644 index 0000000..2f968d5 --- /dev/null +++ b/roles/gitea/tasks/config.yml @@ -0,0 +1,139 @@ +--- +# ============================================================================= +# Configuration Tasks - Update Domain Settings +# ============================================================================= +# +# Updates Gitea's app.ini configuration file with the new domain. +# +# The app.ini file lives inside the Docker volume at /data/gitea/conf/app.ini. +# We extract it, modify it on the host, then copy it back into the container. +# +# Settings that need updating for domain rename: +# [server] +# DOMAIN = git.veridion.ru (web domain) +# SSH_DOMAIN = git.veridion.ru (SSH clone URLs) +# ROOT_URL = https://git.veridion.ru/ (base URL for all links) +# +# Reference: https://docs.gitea.com/administration/config-cheat-sheet +# ============================================================================= + +# Create a temporary directory on the host for config editing. +# We'll extract app.ini here, modify it, then copy back. +# check_mode: false - tempfile is harmless, needed for subsequent tasks +- name: Create temporary directory for config editing + ansible.builtin.tempfile: + state: directory + prefix: gitea_config_ + register: gitea_config_temp_dir + check_mode: false + +# ----------------------------------------------------------------------------- +# Extract Configuration from Container +# ----------------------------------------------------------------------------- +# docker cp extracts files from a container to the host filesystem. +# Format: docker cp : +# check_mode: false - read-only extraction needed for lineinfile to evaluate changes + +- name: Extract app.ini from Gitea container + ansible.builtin.command: + cmd: "docker cp {{ gitea_container_name }}:/data/gitea/conf/app.ini {{ gitea_config_temp_dir.path }}/app.ini" + changed_when: false + check_mode: false + +# ----------------------------------------------------------------------------- +# Update Domain Settings +# ----------------------------------------------------------------------------- +# Using lineinfile module to update specific settings in app.ini. +# Each task finds a line matching the regexp and replaces it. +# +# lineinfile parameters: +# path: File to modify +# regexp: Pattern to find (uses Python regex) +# line: Replacement line +# backrefs: If true, allows using \1, \2 for captured groups (not used here) +# +# The regexp patterns: +# ^DOMAIN\s*= Matches "DOMAIN = " at start of line, with any whitespace +# ^\s*DOMAIN\s*= Would also match indented lines (not typical in app.ini) + +- name: Update DOMAIN setting in app.ini + ansible.builtin.lineinfile: + path: "{{ gitea_config_temp_dir.path }}/app.ini" + regexp: '^DOMAIN\s*=' + line: "DOMAIN = {{ gitea_domain }}" + register: gitea_domain_updated + +- name: Update SSH_DOMAIN setting in app.ini + ansible.builtin.lineinfile: + path: "{{ gitea_config_temp_dir.path }}/app.ini" + regexp: '^SSH_DOMAIN\s*=' + line: "SSH_DOMAIN = {{ gitea_ssh_domain }}" + register: gitea_ssh_domain_updated + +# ROOT_URL must include the protocol (https://) and trailing slash +- name: Update ROOT_URL setting in app.ini + ansible.builtin.lineinfile: + path: "{{ gitea_config_temp_dir.path }}/app.ini" + regexp: '^ROOT_URL\s*=' + line: "ROOT_URL = {{ gitea_root_url }}/" + register: gitea_root_url_updated + +# ----------------------------------------------------------------------------- +# Apply Security Hardening (Optional) +# ----------------------------------------------------------------------------- +# These settings enhance security. They're applied during domain update +# since we're already modifying the config. +# +# Each setting is conditional on whether the variable is defined, +# allowing operators to skip specific hardening options. + +# Password hashing: argon2 is more secure than pbkdf2 (Gitea default) +- name: Update password hash algorithm + ansible.builtin.lineinfile: + path: "{{ gitea_config_temp_dir.path }}/app.ini" + regexp: '^PASSWORD_HASH_ALGO\s*=' + line: "PASSWORD_HASH_ALGO = {{ gitea_password_hash_algo }}" + insertafter: '^\[security\]' + when: gitea_password_hash_algo is defined + +# Disable git hooks to prevent arbitrary code execution +- name: Update git hooks setting + ansible.builtin.lineinfile: + path: "{{ gitea_config_temp_dir.path }}/app.ini" + regexp: '^DISABLE_GIT_HOOKS\s*=' + line: "DISABLE_GIT_HOOKS = {{ gitea_disable_git_hooks | lower }}" + insertafter: '^\[security\]' + when: gitea_disable_git_hooks is defined + +# ----------------------------------------------------------------------------- +# Copy Updated Configuration Back to Container +# ----------------------------------------------------------------------------- +# docker cp can also copy from host to container. +# Format: docker cp : + +- name: Copy updated app.ini back to container + ansible.builtin.command: + cmd: "docker cp {{ gitea_config_temp_dir.path }}/app.ini {{ gitea_container_name }}:/data/gitea/conf/app.ini" + changed_when: true + when: gitea_domain_updated.changed or gitea_ssh_domain_updated.changed or gitea_root_url_updated.changed + +# ----------------------------------------------------------------------------- +# Cleanup +# ----------------------------------------------------------------------------- +# Remove the temporary directory we created. +# check_mode: false - clean up the temp dir we created with check_mode: false + +- name: Remove temporary config directory + ansible.builtin.file: + path: "{{ gitea_config_temp_dir.path }}" + state: absent + check_mode: false + +# Display summary of changes for operator visibility +- name: Display configuration changes + ansible.builtin.debug: + msg: | + Configuration updated: + DOMAIN = {{ gitea_domain }} + SSH_DOMAIN = {{ gitea_ssh_domain }} + ROOT_URL = {{ gitea_root_url }}/ diff --git a/roles/gitea/tasks/deploy.yml b/roles/gitea/tasks/deploy.yml new file mode 100644 index 0000000..2659675 --- /dev/null +++ b/roles/gitea/tasks/deploy.yml @@ -0,0 +1,126 @@ +--- +# ============================================================================= +# Deploy Tasks - Pull Images and Restart Services +# ============================================================================= +# +# Performs the actual deployment: +# 1. Pull the new Docker images +# 2. Restart services with --wait flag (waits for healthchecks) +# 3. Verify the deployment +# +# This task relies on healthchecks defined in docker-compose.yml: +# - PostgreSQL: pg_isready checks database is accepting connections +# - Gitea: curl to /api/healthz checks web server is responding +# +# The --wait flag makes docker compose block until all services are healthy. +# This is cleaner than manually polling with Ansible loops. +# +# Database migrations happen AUTOMATICALLY when Gitea starts with a new +# version. The start_period in Gitea's healthcheck allows time for this. +# +# Reference: https://docs.gitea.com/installation/upgrade-from-gitea +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Pull New Docker Images +# ----------------------------------------------------------------------------- +# Pull images before stopping services to minimize downtime. +# This downloads the new image layers while the old containers still run. + +- name: Pull new Docker images + ansible.builtin.command: + cmd: "docker compose -f {{ gitea_install_dir }}/docker-compose.yml pull" + register: gitea_docker_pull + changed_when: "'Pulled' in gitea_docker_pull.stdout or 'Downloaded' in gitea_docker_pull.stdout" + +- name: Display image pull results + ansible.builtin.debug: + msg: "{{ gitea_docker_pull.stdout_lines | default(['Images already up to date']) }}" + +# ----------------------------------------------------------------------------- +# Restart Services with Healthcheck Wait +# ----------------------------------------------------------------------------- +# docker compose up -d --wait: +# -d : Detached mode (run in background) +# --wait : Block until all services are healthy (based on healthcheck) +# +# This single command: +# 1. Stops old containers if config changed +# 2. Creates new containers with new images +# 3. Waits for healthchecks to pass +# +# Timeout is set high (5 minutes) to allow for: +# - Database startup +# - Gitea migrations (can take time on version upgrades) +# - ACME certificate issuance (30-60 seconds for new domain) + +- name: Restart services and wait for healthy + ansible.builtin.command: + cmd: "docker compose -f {{ gitea_install_dir }}/docker-compose.yml up -d --wait" + chdir: "{{ gitea_install_dir }}" + register: gitea_compose_up + changed_when: true + # 5 minute timeout for migrations + ACME certificate + timeout: 300 + +- name: Display compose output + ansible.builtin.debug: + msg: "{{ gitea_compose_up.stdout_lines | default(['Services started']) }}" + +# ----------------------------------------------------------------------------- +# Verify Deployment +# ----------------------------------------------------------------------------- +# Additional verification beyond healthchecks. + +# Check container health status +# check_mode: false - read-only, shows current state during dry run +- name: Verify container health status + ansible.builtin.command: + cmd: "docker compose -f {{ gitea_install_dir }}/docker-compose.yml ps --format json" + register: gitea_compose_status + changed_when: false + check_mode: false + +- name: Display container status + ansible.builtin.debug: + msg: "Container status: {{ gitea_compose_status.stdout }}" + +# Check Gitea logs for migration activity +# check_mode: false - read-only, shows current logs during dry run +- name: Check Gitea logs for errors or migrations + ansible.builtin.command: + cmd: "docker logs --tail 30 {{ gitea_container_name }}" + register: gitea_logs + changed_when: false + check_mode: false + +- name: Display recent Gitea logs + ansible.builtin.debug: + msg: "{{ gitea_logs.stdout_lines[-10:] | default(['No logs available']) }}" + +# ----------------------------------------------------------------------------- +# Final Summary +# ----------------------------------------------------------------------------- + +- name: Deployment summary + ansible.builtin.debug: + msg: | + ============================================ + DEPLOYMENT COMPLETE + ============================================ + Domain: https://{{ gitea_domain }}/ + SSH: ssh://git@{{ gitea_domain }}:{{ gitea_ssh_external_port }}/ + Gitea: {{ gitea_version }} + PostgreSQL: {{ gitea_postgres_version }} + + Healthchecks passed - services are running. + + Verify manually: + 1. Login at https://{{ gitea_domain }}/ + 2. Clone a repo via HTTPS and SSH + 3. Check Settings > Admin for version info + + If act-runner was configured, update its config: + vault_gitea_instance_url: https://{{ gitea_domain }} + vault_gitea_registry: {{ gitea_domain }} + ============================================ diff --git a/roles/gitea/tasks/main.yml b/roles/gitea/tasks/main.yml new file mode 100644 index 0000000..cb6746e --- /dev/null +++ b/roles/gitea/tasks/main.yml @@ -0,0 +1,39 @@ +--- +# ============================================================================= +# Gitea Role - Main Task Orchestration +# ============================================================================= +# +# This file controls the execution order of all tasks. +# Tasks are designed to be idempotent and safe to run multiple times. +# +# IMPORTANT: Database migrations are handled AUTOMATICALLY by Gitea on startup. +# The backup task MUST run before any changes to allow rollback. +# +# Reference: https://docs.gitea.com/installation/upgrade-from-gitea +# ============================================================================= + +- name: Include preflight checks + ansible.builtin.include_tasks: preflight.yml + tags: + - preflight + - always + +- name: Include backup tasks + ansible.builtin.include_tasks: backup.yml + tags: + - backup + +- name: Include configuration tasks + ansible.builtin.include_tasks: config.yml + tags: + - config + +- name: Include upgrade tasks + ansible.builtin.include_tasks: upgrade.yml + tags: + - upgrade + +- name: Include deployment tasks + ansible.builtin.include_tasks: deploy.yml + tags: + - deploy diff --git a/roles/gitea/tasks/preflight.yml b/roles/gitea/tasks/preflight.yml new file mode 100644 index 0000000..c852986 --- /dev/null +++ b/roles/gitea/tasks/preflight.yml @@ -0,0 +1,144 @@ +--- +# ============================================================================= +# Preflight Checks +# ============================================================================= +# +# Validates prerequisites before making any changes. +# Fails fast with clear error messages if requirements are not met. +# +# Reference: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/assert_module.html +# ============================================================================= + +- name: Verify required variables are defined + ansible.builtin.assert: + that: + - gitea_domain is defined + - gitea_domain | length > 0 + - gitea_db_password is defined + - gitea_db_password | length > 0 + - gitea_install_dir is defined + fail_msg: >- + Required variables missing. Ensure vault.yml contains: + vault_gitea_domain, vault_gitea_db_password. + Ensure inventory contains: gitea_install_dir. + quiet: true + +- name: Check if Docker is installed + ansible.builtin.command: + cmd: docker --version + register: gitea_docker_check + changed_when: false + check_mode: false + failed_when: gitea_docker_check.rc != 0 + +- name: Verify Docker daemon is running + ansible.builtin.command: + cmd: docker info + register: gitea_docker_info + changed_when: false + check_mode: false + failed_when: gitea_docker_info.rc != 0 + +- name: Check if Gitea install directory exists + ansible.builtin.stat: + path: "{{ gitea_install_dir }}" + register: gitea_dir_stat + +- name: Verify Gitea install directory exists + ansible.builtin.assert: + that: + - gitea_dir_stat.stat.exists + - gitea_dir_stat.stat.isdir + fail_msg: "Gitea install directory not found: {{ gitea_install_dir }}" + quiet: true + +- name: Check if docker-compose.yml exists + ansible.builtin.stat: + path: "{{ gitea_install_dir }}/docker-compose.yml" + register: gitea_compose_stat + +- name: Verify docker-compose.yml exists + ansible.builtin.assert: + that: + - gitea_compose_stat.stat.exists + fail_msg: "docker-compose.yml not found in {{ gitea_install_dir }}" + quiet: true + +# Find the mount point containing gitea_install_dir using df command. +# This is more reliable than substring matching on ansible_mounts. +# check_mode: false - df is read-only, safe to run even in --check mode +- name: Find mount point for install directory + ansible.builtin.command: + cmd: "df --output=target {{ gitea_install_dir }}" + register: gitea_df_result + changed_when: false + check_mode: false + +# Parse mount point from df output (first line is header, second is mount) +- name: Parse mount point from df output + ansible.builtin.set_fact: + gitea_mount_point: "{{ gitea_df_result.stdout_lines[-1] | trim }}" + +# Look up full mount info (size_available, etc.) from gathered facts +- name: Get mount info from ansible_facts + ansible.builtin.set_fact: + gitea_install_mount: "{{ ansible_facts['mounts'] | selectattr('mount', 'equalto', gitea_mount_point) | first }}" + +# Check available space: 2GB = 2 * 1024^3 = 2147483648 bytes +- name: Verify sufficient disk space (minimum 2GB) + ansible.builtin.assert: + that: + - gitea_install_mount.size_available > 2147483648 + fail_msg: >- + Insufficient disk space on {{ gitea_install_mount.mount }}. + Available: {{ (gitea_install_mount.size_available / 1073741824) | round(2) }}GB. + Minimum required: 2GB. + quiet: true + +- name: Check if Gitea container is running + ansible.builtin.command: + cmd: docker ps --filter "name={{ gitea_container_name }}" --format "{{ '{{' }}.Names{{ '}}' }}" + register: gitea_container_check + changed_when: false + check_mode: false + +- name: Display Gitea container status + ansible.builtin.debug: + msg: "Gitea container status: {{ 'running' if gitea_container_name in gitea_container_check.stdout else 'not running' }}" + +- name: Check if database container is running + ansible.builtin.command: + cmd: docker ps --filter "name={{ gitea_db_container_name }}" --format "{{ '{{' }}.Names{{ '}}' }}" + register: gitea_db_container_check + changed_when: false + check_mode: false + +- name: Display database container status + ansible.builtin.debug: + msg: "Database container status: {{ 'running' if gitea_db_container_name in gitea_db_container_check.stdout else 'not running' }}" + +# Verify DNS is configured before proceeding. +# ACME certificate issuance will fail without valid DNS. +- name: Check DNS resolution for domain + ansible.builtin.command: + cmd: "dig +short {{ gitea_domain }}" + register: gitea_dns_check + changed_when: false + check_mode: false + failed_when: false + +- name: Display DNS resolution result + ansible.builtin.debug: + msg: "DNS for {{ gitea_domain }} resolves to: {{ gitea_dns_check.stdout_lines | default(['UNRESOLVED']) | join(', ') }}" + +# Fail if DNS doesn't resolve (can be skipped with gitea_skip_gitea_dns_check=true) +- name: Verify DNS resolves for domain + ansible.builtin.fail: + msg: >- + DNS for {{ gitea_domain }} does not resolve. + ACME certificate issuance will fail without valid DNS. + Ensure A record points to this server before proceeding. + To skip this check, set gitea_skip_gitea_dns_check=true. + when: + - gitea_dns_check.stdout | length == 0 + - not (gitea_skip_gitea_dns_check | default(false) | bool) diff --git a/roles/gitea/tasks/upgrade.yml b/roles/gitea/tasks/upgrade.yml new file mode 100644 index 0000000..339bed9 --- /dev/null +++ b/roles/gitea/tasks/upgrade.yml @@ -0,0 +1,67 @@ +--- +# ============================================================================= +# Upgrade Tasks - Deploy New docker-compose.yml +# ============================================================================= +# +# Deploys the templated docker-compose.yml with the new Gitea version. +# +# This task uses Ansible's template module to generate docker-compose.yml +# from a Jinja2 template. The template allows us to: +# - Update the Gitea image version +# - Update the PostgreSQL image version +# - Apply configuration from Ansible variables +# +# The template is stored at: roles/gitea/templates/docker-compose.yml.j2 +# +# After this task, the docker-compose.yml on the server will reference +# the new image versions, but containers aren't restarted yet. +# That happens in deploy.yml. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Backup Current docker-compose.yml +# ----------------------------------------------------------------------------- +# Even though we have a full gitea dump backup, it's useful to have +# the docker-compose.yml readily available for quick rollback. + +- name: Backup current docker-compose.yml before upgrade + ansible.builtin.copy: + src: "{{ gitea_install_dir }}/docker-compose.yml" + dest: "{{ gitea_install_dir }}/docker-compose.yml.backup" + remote_src: true + mode: "0640" + +# ----------------------------------------------------------------------------- +# Deploy New docker-compose.yml from Template +# ----------------------------------------------------------------------------- +# The template module: +# 1. Reads the Jinja2 template (docker-compose.yml.j2) +# 2. Substitutes all {{ variable }} placeholders with actual values +# 3. Writes the result to the destination path +# +# Key variables used in the template: +# gitea_version - Docker image tag (e.g., "1.25") +# postgres_version - PostgreSQL image tag (e.g., "17-alpine") +# gitea_db_password - Database password (from vault) +# gitea_user_uid/gid - Container user/group IDs +# gitea_http_port - Internal HTTP port (3000) +# gitea_ssh_port - Internal SSH port (22) +# gitea_ssh_external_port - External SSH port (2222) + +- name: Deploy docker-compose.yml from template + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ gitea_install_dir }}/docker-compose.yml" + mode: "0640" + # Validate the YAML syntax before deploying + validate: "docker compose -f %s config --quiet" + register: gitea_compose_deployed + +# Display what changed for operator visibility +- name: Display upgrade info + ansible.builtin.debug: + msg: | + docker-compose.yml updated: + Gitea version: {{ gitea_version }} + PostgreSQL version: {{ gitea_postgres_version }} + Template changed: {{ gitea_compose_deployed.changed }} diff --git a/roles/gitea/templates/docker-compose.yml.j2 b/roles/gitea/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..a2a0ac3 --- /dev/null +++ b/roles/gitea/templates/docker-compose.yml.j2 @@ -0,0 +1,72 @@ +# Gitea with PostgreSQL - Docker Compose +# +# Based on: https://docs.gitea.com/installation/install-with-docker +# Healthchecks from: https://docs.gitea.com/installation/install-on-kubernetes +# https://github.com/go-gitea/gitea/pull/35513 +# +# Generated by Ansible - do not edit manually. + +networks: + gitea: + external: false + +volumes: + gitea: + driver: local + postgres: + driver: local + +services: + db: + image: postgres:{{ gitea_postgres_version }} + container_name: {{ gitea_db_container_name }} + restart: always + environment: + - POSTGRES_USER={{ gitea_db_user }} + - POSTGRES_PASSWORD={{ gitea_db_password }} + - POSTGRES_DB={{ gitea_db_name }} + networks: + - gitea + volumes: + - postgres:/var/lib/postgresql/data + # PostgreSQL readiness check + healthcheck: + test: ["CMD", "pg_isready", "-U", "{{ gitea_db_user }}", "-d", "{{ gitea_db_name }}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + server: + image: docker.io/gitea/gitea:{{ gitea_version }} + container_name: {{ gitea_container_name }} + restart: always + environment: + - USER_UID={{ gitea_user_uid }} + - USER_GID={{ gitea_user_gid }} + - GITEA__database__DB_TYPE={{ gitea_db_type }} + - GITEA__database__HOST={{ gitea_db_host }} + - GITEA__database__NAME={{ gitea_db_name }} + - GITEA__database__USER={{ gitea_db_user }} + - GITEA__database__PASSWD={{ gitea_db_password }} + networks: + - gitea + volumes: + - /home/git/.ssh/:/data/git/.ssh + - gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "443:{{ gitea_http_port }}" + - "127.0.0.1:{{ gitea_ssh_external_port }}:{{ gitea_ssh_port }}" + depends_on: + db: + condition: service_healthy + # Gitea health endpoint (per K8s docs and PR #35513) + # start_period allows time for database migrations on upgrade + healthcheck: + test: ["CMD", "curl", "-fSs", "http://127.0.0.1:{{ gitea_http_port }}/api/healthz"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..4fe6d83 --- /dev/null +++ b/site.yml @@ -0,0 +1,23 @@ +--- +# ============================================================================= +# Site Playbook - Deploy All Services +# ============================================================================= +# +# Deploys all services in the correct order. +# +# Usage: +# ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass +# +# Deploy specific service: +# ansible-playbook -i inventory/hosts.yml site.yml --tags gitea --ask-vault-pass +# ansible-playbook -i inventory/hosts.yml site.yml --tags act_runner --ask-vault-pass +# +# ============================================================================= + +- name: Deploy Gitea + ansible.builtin.import_playbook: gitea.yml + tags: [gitea] + +- name: Deploy Act Runner + ansible.builtin.import_playbook: act-runner.yml + tags: [act_runner]