commit 6982bcf37202acb50b62df1941bd1d838bc3a4d1 Author: Mark Date: Sat Jan 10 16:01:06 2026 +0100 Initial commit: Ansible playbook for Gitea Act Runner deployment Automated deployment of act_runner on Ubuntu 20.04+ servers: - Docker CE installation (DEB822 format) - Node.js 24.x via NodeSource - act_runner binary with SHA256 verification - systemd service with security hardening - CI: ansible-lint via Gitea Actions Co-Authored-By: Claude Opus 4.5 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/.gitea/workflows/lint.yml b/.gitea/workflows/lint.yml new file mode 100644 index 0000000..a9bc2d6 --- /dev/null +++ b/.gitea/workflows/lint.yml @@ -0,0 +1,139 @@ +# ============================================================================= +# Gitea Actions Workflow - Ansible Lint +# ============================================================================= +# +# This workflow validates Ansible playbooks and roles using ansible-lint. +# Runs automatically on push and pull request events. +# +# WORKFLOW LOCATION: +# Gitea Actions requires workflow files in `.gitea/workflows/` directory. +# This differs from GitHub Actions which uses `.github/workflows/`. +# See: https://docs.gitea.com/usage/actions/quickstart +# +# RUNNER CONFIGURATION: +# - `runs-on: ubuntu-latest` matches our runner label from act_runner config +# - Jobs execute on self-hosted runners registered with this repository's Gitea +# - Requires runner with `ubuntu-latest:host` label (host execution mode) +# +# WHAT THIS WORKFLOW DOES: +# 1. Checks out the repository code +# 2. Installs uv package manager +# 3. Syncs dependencies from pyproject.toml +# 4. Runs ansible-lint against the playbook +# +# MANUAL TRIGGER: +# You can manually trigger this workflow from Gitea UI: +# Repository > Actions > Select workflow > "Run workflow" +# +# DOCUMENTATION: +# - Gitea Actions: https://docs.gitea.com/usage/actions/overview +# - Ansible Lint: https://ansible.readthedocs.io/projects/lint/ +# - uv: https://docs.astral.sh/uv/ +# - Workflow Syntax: https://docs.gitea.com/usage/actions/workflow-syntax +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Workflow Metadata +# ----------------------------------------------------------------------------- + +# Human-readable name displayed in Gitea Actions UI. +# Shown in repository's Actions tab and in commit status checks. +name: Ansible Lint + +# ----------------------------------------------------------------------------- +# Trigger Events +# ----------------------------------------------------------------------------- +# Define when this workflow should run. +# See: https://docs.gitea.com/usage/actions/workflow-syntax#on + +on: + # Run on every push to any branch. + # This catches issues before they're merged. + push: + # Optionally limit to specific branches: + # branches: + # - master + # - main + + # Run on pull requests targeting any branch. + # Provides feedback before merge. + pull_request: + # Optionally limit to PRs targeting specific branches: + # branches: + # - master + # - main + + # Allow manual triggering from Gitea UI. + # Useful for re-running after fixing external issues (e.g., runner problems). + workflow_dispatch: + +# ----------------------------------------------------------------------------- +# Jobs +# ----------------------------------------------------------------------------- +# A workflow consists of one or more jobs that run in parallel by default. +# Use `needs:` to create dependencies between jobs if sequential execution +# is required. + +jobs: + # --------------------------------------------------------------------------- + # Lint Job + # --------------------------------------------------------------------------- + # Validates Ansible code quality and best practices. + lint: + # Human-readable name shown in Gitea UI for this job. + name: Ansible Lint Check + + # Runner label to execute this job. + # Must match a label registered with act_runner. + # Our runner uses: `ubuntu-latest:host` (see roles/act_runner/defaults/main.yml) + runs-on: ubuntu-latest + + # ------------------------------------------------------------------------- + # Job Steps + # ------------------------------------------------------------------------- + # Steps run sequentially within a job. + # Each step can run commands or use pre-built actions. + steps: + # ----------------------------------------------------------------------- + # Step 1: Checkout Repository + # ----------------------------------------------------------------------- + # Clone the repository to the runner's workspace. + # Uses the official checkout action maintained by GitHub/Gitea. + # See: https://github.com/actions/checkout + - name: Checkout repository + uses: actions/checkout@v4 + # Default behavior: + # - Clones the commit that triggered the workflow + # - For PRs, clones the merge commit + # - Shallow clone (depth=1) for faster checkout + + # ----------------------------------------------------------------------- + # Step 2: Install uv + # ----------------------------------------------------------------------- + # Install uv - a fast Python package manager from Astral. + # Replaces pip/virtualenv with a single, faster tool. + # See: https://docs.astral.sh/uv/ + - name: Install uv + uses: astral-sh/setup-uv@v5 + + # ----------------------------------------------------------------------- + # Step 3: Sync Dependencies and Run Linter + # ----------------------------------------------------------------------- + # uv sync reads pyproject.toml and creates a virtual environment + # with all dependencies (ansible, ansible-lint). + # uv run executes commands within that environment. + - name: Run ansible-lint + run: | + # Install dependencies from pyproject.toml. + uv sync + + # Run ansible-lint on the main playbook. + # Exit code 0 = success (no issues found) + # Exit code non-zero = linting errors found (fails the workflow) + # + # The linter uses .ansible-lint config file for: + # - Profile: production (strictest rules) + # - Excluded paths: .git/, .gitignore + # - Offline mode: true (no network calls) + uv run ansible-lint playbook.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1fa448 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# ============================================================================= +# Gitignore - Gitea Act Runner Ansible Playbook +# ============================================================================= + +# Ansible Vault - NEVER commit actual vault files +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 +__pycache__/ +*.py[cod] +*.pyo +.Python +*.egg-info/ +.eggs/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Temp files +*.tmp +*.temp +*.log + +# SSH keys (if accidentally placed here) +*.pem +*.key +id_rsa* diff --git a/README.md b/README.md new file mode 100644 index 0000000..3765ce0 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Gitea Act Runner - Ansible Playbook + +Deploys [Gitea Act Runner](https://docs.gitea.com/usage/actions/act-runner) on Ubuntu 20.04+ servers. + +## Usage + +```bash +# Install dependencies +uv sync + +# Configure inventory and vault (see examples) +ansible-vault create group_vars/all/vault.yml + +# Deploy +uv run ansible-playbook -i inventory/hosts.yml playbook.yml --ask-vault-pass +``` + +## References + +- [Act Runner Docs](https://docs.gitea.com/usage/actions/act-runner) +- [Gitea Actions](https://docs.gitea.com/usage/actions/overview) diff --git a/group_vars/all/vars.yml b/group_vars/all/vars.yml new file mode 100644 index 0000000..35b68cb --- /dev/null +++ b/group_vars/all/vars.yml @@ -0,0 +1,79 @@ +--- +# ============================================================================= +# Group Variables - All Hosts +# ============================================================================= +# +# This file maps vault secrets to role variables and sets common overrides. +# +# VARIABLE RESOLUTION ORDER (lowest to highest priority): +# 1. roles/act_runner/defaults/main.yml (role defaults) +# 2. group_vars/all.yml (this file) +# 3. inventory host_vars (per-host overrides) +# 4. command line --extra-vars (highest priority) +# +# Vault variables (prefixed with vault_) are stored encrypted in vault.yml. +# Role variables are defined in roles/act_runner/defaults/main.yml. +# +# HOW TO USE: +# 1. Create the vault: ansible-vault create group_vars/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 +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Vault Secret Mappings +# ----------------------------------------------------------------------------- +# These map encrypted vault variables to the role's expected variable names. +# This indirection allows vault structure to differ from role expectations. +# +# REQUIRED: These must be set in group_vars/vault.yml + +# URL of your Gitea instance (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 }}" + +# ----------------------------------------------------------------------------- +# Role Variable Overrides (Optional) +# ----------------------------------------------------------------------------- +# Uncomment and modify to override role defaults for all hosts. +# See roles/act_runner/defaults/main.yml for all available variables. + +# Act Runner Version +# Check available versions: https://dl.gitea.com/act_runner/ +# act_runner_version: "0.2.13" + +# Node.js Version +# Valid values: "18", "20", "22", "24" +# See: https://nodejs.org/en/about/previous-releases +# nodejs_version: "24" + +# Runner Labels +# Define what jobs this runner can handle. +# Format: "label:executor" where executor is "host" or "docker://image" +# act_runner_labels: +# - "ubuntu-latest:host" +# - "self-hosted:host" + +# Concurrent Jobs +# How many jobs can run simultaneously on each runner. +# Higher values = more parallelism but more resource usage. +# runner_capacity: 1 + +# Container Settings +# Whether to always pull images before running (recommended: true) +# container_force_pull: true diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example new file mode 100644 index 0000000..3699f7c --- /dev/null +++ b/group_vars/all/vault.yml.example @@ -0,0 +1,94 @@ +--- +# ============================================================================= +# Ansible Vault - Encrypted Secrets (EXAMPLE) +# ============================================================================= +# +# This is an EXAMPLE file showing what should go in your encrypted vault. +# DO NOT commit actual secrets to version control. +# +# HOW TO CREATE THE REAL VAULT: +# 1. Copy this file's contents +# 2. Run: ansible-vault create group_vars/vault.yml +# 3. Paste and edit with your actual values +# 4. Save and exit +# +# HOW TO MANAGE THE VAULT: +# - Edit: ansible-vault edit group_vars/vault.yml +# - View: ansible-vault view group_vars/vault.yml +# - Rekey: ansible-vault rekey group_vars/vault.yml +# +# SECURITY NOTES: +# - Never commit the vault password to version control +# - Store vault password in a secure location (password manager, etc.) +# - Consider using --vault-password-file for automation +# - Add vault.yml to .gitignore (this example file is safe to commit) +# +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Gitea Instance Configuration +# ----------------------------------------------------------------------------- + +# URL of your Gitea instance (including https://). +# This is where the runner will connect to pick up jobs. +# Example: "https://git.example.com" +vault_gitea_instance_url: "https://git.karmaxplan.ru" + +# ----------------------------------------------------------------------------- +# Runner Registration Token +# ----------------------------------------------------------------------------- + +# Registration token from Gitea for authenticating new runners. +# +# HOW TO GET THIS TOKEN: +# 1. Log into Gitea as admin +# 2. Go to: Site Administration > Actions > Runners +# URL: {{ vault_gitea_instance_url }}/-/admin/actions/runners +# 3. Click "Create new Runner" +# 4. Copy the displayed token +# +# IMPORTANT: +# - Tokens are SINGLE-USE: one token = one runner registration +# - Generate a new token for each server you deploy to +# - Token expires if not used within a certain time +vault_act_runner_token: "" + +# ----------------------------------------------------------------------------- +# Package Registry Configuration +# ----------------------------------------------------------------------------- +# These values enable the runner to authenticate with Gitea's package registry. +# This is needed if your workflows push/pull container images or packages. + +# Hostname of the Gitea package registry. +# Usually the same as your Gitea instance hostname (without https://). +vault_gitea_registry: "git.karmaxplan.ru" + +# Service account username for package registry operations. +# Best practice: Create a dedicated "actions" user in Gitea for CI/CD. +# This user should have appropriate permissions for your repositories. +vault_gitea_actions_user: "actions" + +# Personal Access Token (PAT) for package registry authentication. +# +# HOW TO CREATE THE PAT: +# 1. Log into Gitea as the service account (e.g., "actions") +# 2. Go to: User Settings > Applications +# URL: {{ vault_gitea_instance_url }}/user/settings/applications +# 3. Under "Generate New Token", enter a name (e.g., "act-runner-packages") +# 4. Select scopes: +# - read:package (required for pulling images) +# - write:package (required for pushing images) +# 5. Click "Generate Token" +# 6. Copy the token immediately (it won't be shown again) +# +# SECURITY NOTES: +# - This token grants package access - keep it secret +# - Rotate tokens periodically +# - Use the minimum required scopes +vault_gitea_packages_token: "" + +# ----------------------------------------------------------------------------- +# Optional: Sudo Password (if using non-root user with password sudo) +# ----------------------------------------------------------------------------- +# Uncomment if your ansible_user requires a password for sudo. +# vault_sudo_password: "" diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..78ddc1b --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,99 @@ +--- +# ============================================================================= +# Ansible Inventory - Gitea Act Runner Servers +# ============================================================================= +# +# This file defines which servers the playbook will deploy to. +# Each host should be reachable via SSH with the specified ansible_user. +# +# HOW TO USE: +# 1. Replace the example hosts with your actual server IPs/hostnames +# 2. Set the correct SSH user (ansible_user) +# 3. Optionally override variables per-host +# +# Host-specific variables can override group defaults. +# Example: Different runner names, labels, or capacity per server. +# +# EXAMPLE CONFIGURATIONS: +# +# Single server: +# all: +# hosts: +# runner: +# ansible_host: 10.0.0.50 +# vars: +# ansible_user: root +# +# Multiple servers with custom names: +# all: +# hosts: +# prod-runner-01: +# ansible_host: 10.0.0.50 +# act_runner_name: "production-01" +# prod-runner-02: +# ansible_host: 10.0.0.51 +# act_runner_name: "production-02" +# vars: +# ansible_user: deploy +# ansible_become: true +# +# See: https://docs.ansible.com/ansible/latest/inventory_guide/ +# ============================================================================= + +all: + hosts: + # ------------------------------------------------------------------------- + # Define each runner server below + # ------------------------------------------------------------------------- + # The key (e.g., runner-01) is the inventory_hostname. + # Replace with your actual server details. + + tralalero-tralala: + # IP address or hostname for SSH connection. + # REQUIRED: Change this to your server's address. + ansible_host: 147.45.100.33 + + # Optional: Override runner name for this specific host. + # Default: Uses the server's actual hostname (ansible_hostname fact). + # Uncomment and modify if you want a custom name in Gitea UI. + # act_runner_name: "production-runner-01" + + # Optional: Different labels for this runner. + # Default: ["ubuntu-latest:host"] (from role defaults) + # Uncomment to customize what jobs this runner can handle. + # act_runner_labels: + # - "ubuntu-latest:host" + # - "self-hosted:host" + + # Uncomment and configure additional servers as needed: + # runner-02: + # ansible_host: 192.168.1.11 + # act_runner_name: "production-runner-02" + + # --------------------------------------------------------------------------- + # Variables applied to all hosts in this inventory + # --------------------------------------------------------------------------- + vars: + # SSH user for connecting to servers. + # This user must either be root, or have sudo privileges. + # REQUIRED: Change this to match your server access. + ansible_user: root + + # --------------------------------------------------------------------------- + # Alternative: Use a non-root user with sudo + # --------------------------------------------------------------------------- + # If you connect as a regular user that has sudo access, uncomment these: + # ansible_user: deploy + # ansible_become: true + # ansible_become_method: sudo + # ansible_become_password: "{{ vault_sudo_password }}" # If password required + + # --------------------------------------------------------------------------- + # SSH Configuration (optional) + # --------------------------------------------------------------------------- + # Uncomment if you need to specify SSH key or port: + # ansible_ssh_private_key_file: ~/.ssh/id_rsa + # ansible_port: 22 + + # Disable host key checking for new servers (not recommended for production): + # ansible_ssh_common_args: '-o StrictHostKeyChecking=no' diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..0ebe16c --- /dev/null +++ b/playbook.yml @@ -0,0 +1,97 @@ +--- +# ============================================================================= +# Gitea Act Runner - Deployment Playbook +# ============================================================================= +# +# This playbook deploys and configures Gitea Act Runner on Ubuntu servers. +# +# USAGE: +# # Standard deployment (interactive vault password prompt): +# ansible-playbook -i inventory/hosts.yml playbook.yml --ask-vault-pass +# +# # Dry run (preview changes without applying): +# ansible-playbook -i inventory/hosts.yml playbook.yml --check --diff --ask-vault-pass +# +# # Deploy to specific hosts only: +# ansible-playbook -i inventory/hosts.yml playbook.yml --limit runner-01 --ask-vault-pass +# +# PREREQUISITES: +# - Ansible 2.15+ on control machine +# - SSH access to target servers (root or sudo user) +# - Vault password for encrypted secrets (group_vars/vault.yml) +# - Target servers running Ubuntu 20.04 or later +# +# WHAT THIS PLAYBOOK DOES: +# 1. Validates target OS is supported (Ubuntu 20.04+) +# 2. Updates apt package cache +# 3. Installs Docker CE +# 4. Installs Node.js LTS via NodeSource +# 5. Downloads and installs act_runner binary +# 6. Creates act_runner system user +# 7. Deploys configuration and registers with Gitea +# 8. Sets up systemd service for automatic startup +# 9. Verifies all components are working +# +# DOCUMENTATION: +# - Gitea Actions: https://docs.gitea.com/usage/actions/overview +# - Act Runner: https://docs.gitea.com/usage/actions/act-runner +# - Ansible: https://docs.ansible.com/ansible/latest/ +# +# ============================================================================= + +- name: Deploy Gitea Act Runner + hosts: all + become: true + gather_facts: true + + # --------------------------------------------------------------------------- + # Pre-tasks: Validation and preparation before role execution + # --------------------------------------------------------------------------- + pre_tasks: + # Fail early if the target OS is not supported. + # This prevents confusing errors later in the playbook. + - 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'] }} + success_msg: >- + Target OS validated: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} + + # Update apt cache before installing packages. + # cache_valid_time prevents unnecessary updates on repeated runs. + - name: Update apt package cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 # Skip update if cache is less than 1 hour old + + # --------------------------------------------------------------------------- + # Roles: Main installation logic + # --------------------------------------------------------------------------- + roles: + - role: act_runner + tags: + - act_runner + + # --------------------------------------------------------------------------- + # Post-tasks: Summary and verification + # --------------------------------------------------------------------------- + post_tasks: + # Display deployment summary for operator confirmation. + # Uses variables registered during verification 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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ff9c04 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +# ============================================================================= +# Project Configuration - Gitea Act Runner Ansible Playbook +# ============================================================================= +# Dependency management for uv (https://docs.astral.sh/uv/) +# +# Usage: +# uv sync # Install dependencies +# uv run ansible-lint # Run linter +# ============================================================================= + +[project] +name = "act-runner-gitea" +version = "0.1.0" +description = "Ansible playbook for Gitea Act Runner deployment" +requires-python = ">=3.10" + +dependencies = [ + "ansible>=2.15", + "ansible-lint>=6.0", +] 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: