# Gitea Runner Manager (GRM) A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts. By default, runners are deployed as **Docker containers** using the official `gitea/runner` image. A traditional **binary** deployment mode is also available. > **Pronunciation note:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM" in Latin letters) — the Bulgarian word for **thunder**. Wherever there are clouds, there may be thunders. This is an open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian). ## Features - **Simple and focused** — no unnecessary features. - **Secure** — no hardcoded secrets, uses scoped tokens. - **Idempotent** — can be run multiple times safely. - **Flexible** — accepts a plain IP address or hostname, and allows specifying the SSH user and private key. ## Supported Operating Systems - Arch Linux - Ubuntu 22.04 / 24.04 / 26.04 - Debian 12 / 13 ## Prerequisites - **SSH key authentication** — The remote host must be reachable via SSH using the user specified with `--user` and the private key specified with `--key`. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail. - **Sudo privileges** — The specified user must have passwordless or password-prompted (`--ask-become-pass`) sudo access on the remote host. Runner installation requires root privileges for tasks such as installing packages, creating systemd services, and managing Docker. ## Quick Start ### Developer Setup ```bash git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git cd gitea-runner-manager pyenv install 3.11.11 pyenv local 3.11.11 make setup ``` ### Configure Gitea Credentials ```bash cp .env.example .env # Edit .env: # GITEA_URL=https://git.example.com # GITEA_REGISTRATION_TOKEN=your-registration-token ``` `GITEA_REGISTRATION_TOKEN` is the runner registration token obtained from your Gitea instance (Admin → Actions → Runners → Create Registration Token). ### Install a Runner Using the CLI: ```bash # Docker mode (default) — deploys gitea_runner as a container grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner # Binary mode — downloads and installs the gitea_runner binary with systemd grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --mode binary ``` If the remote user requires a sudo password: ```bash grm install 192.168.1.10 --user ubuntu --ask-become-pass ``` Using Make: ```bash # Docker mode (default) make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner # Binary mode make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner MODE=binary # With sudo password prompt: make install HOST=192.168.1.10 USER=ubuntu ASK_BECOME_PASS=1 ``` ### Verify Runner Check Gitea admin UI under **Actions → Runners**. The runner should appear as **Online**. ### View Logs ```bash # Binary mode logs sudo journalctl -u gitea-runner- -f # Docker mode logs docker logs gitea-runner- -f ``` ## Architecture GRM consists of two layers: 1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, and delegates to Ansible via the `ansible-playbook` subprocess. 2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that installs Docker, configures the runner (binary or container), creates systemd services, and registers the runner with Gitea. ``` grm install └── RunnerManager.install() └── ansible-playbook ansible/install-runner.yml └── role: gitea-runner ├── docker.yml (Docker installation) ├── docker_mode.yml (container deployment) ├── binary_mode.yml (binary + systemd deployment) ├── prune.yml (Docker prune timer) └── integration_test.yml (validate online status) ``` ## Configuration All tunable values are exposed as Ansible variables in `ansible/roles/gitea-runner/defaults/main.yml`: | Variable | Default | Description | |----------|---------|-------------| | `runner_mode` | `docker` | Deployment mode: `docker` or `binary` | | `gitea_runner_version` | `1.0.8` | Docker image tag / binary version | | `gitea_runner_docker_image` | `gitea/runner` | Docker image name | | `runner_labels` | `ubuntu-latest:docker://runner-images:ubuntu-22.04` | Runner labels | | `skip_runner_registration` | `false` | Skip API registration (useful for tests) | | `gitea_runner_data_dir` | `/var/lib/gitea-runner` | Runtime data directory | | `gitea_runner_config_dir` | `/etc/gitea-runner` | Config directory (binary mode) | | `gitea_runner_binary_path` | `/usr/local/bin/gitea_runner` | Binary install path | | `gitea_runner_prune_until` | `24h` | Prune resources older than this | | `gitea_runner_prune_schedule` | `daily` | systemd timer schedule | | `gitea_runner_prune_label` | `gitea-runner=true` | Docker label for pruning | | `gitea_runner_service_restart_sec` | `5` | systemd RestartSec value | | `gitea_runner_service_user` | `{{ ansible_user \| default('root') }}` | Service user | | `gitea_runner_log_level` | `info` | Runner log level | | `gitea_runner_container_label` | `gitea-runner=true` | Container label | | `docker_gpg_key_path` | `/etc/apt/keyrings/docker.asc` | Docker GPG key path | Override any variable by passing it to the CLI with `--extra-vars` or by setting it in your Ansible inventory. ## Development ### Project Structure ``` . ├── src/gitea_runner_manager/ # Python CLI source │ ├── cli.py # Click commands │ ├── runner_manager.py # Ansible orchestration │ └── exceptions.py # Custom exceptions ├── ansible/ │ ├── roles/gitea-runner/ # Main Ansible role │ │ ├── defaults/main.yml # Default variables │ │ ├── tasks/ # Task files │ │ ├── templates/ # Jinja2 templates │ │ └── molecule/ # Test scenarios │ ├── install-runner.yml # Install playbook │ └── update-runner.yml # Update playbook ├── tests/ # Unit tests ├── Makefile # Build & test automation └── pyproject.toml # Python project metadata ``` ### Setup Development Environment ```bash make setup # Creates venv, installs deps, sets up hooks source .venv/bin/activate ``` ### Running Linters ```bash make lint # Python (ruff + pyright) make ansible-lint # Ansible make makefile-lint # Makefile ``` ## Testing ### Unit Tests ```bash make test-unit ``` Runs pytest with 100% coverage requirement. ### Molecule Tests ```bash make molecule ``` Runs two scenarios: - **default** — Docker mode installation in an Ubuntu 22.04 container - **binary** — Binary mode installation in an Ubuntu 22.04 container Both scenarios test idempotence (second run produces zero changes). ### Full Test Suite ```bash make test-all # Runs unit tests + linters + molecule ``` ## Troubleshooting ### "Event loop is closed" warning This is a harmless cleanup traceback from Molecule's Docker driver when the test process is interrupted. It does not indicate a test failure. ### Runner appears offline after installation - Check that the `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` environment variables are correct. - Verify the runner container or service is running: `docker ps` or `systemctl status gitea-runner-`. - Check logs for registration errors. ### Docker mode: container won't start - Ensure Docker is installed and running on the host. - Verify the Docker socket is accessible: `docker version`. ### Binary mode: systemd service fails - Check the service status: `systemctl status gitea-runner-`. - Verify the binary exists at `gitea_runner_binary_path`. - Ensure the service user is in the `docker` group. ## Makefile Targets | Target | Description | |--------|-------------| | `setup` | Full environment setup | | `install` | Installs a runner on a host (`MODE=docker` or `binary`) | | `update` | Updates a runner on a host (`MODE=docker` or `binary`) | | `lint` | Runs Python linters | | `ansible-lint` | Runs `ansible-lint` | | `test-unit` | Runs unit tests with coverage | | `molecule` | Runs Ansible Molecule tests | | `test-all` | Runs all tests | ## License GPL-3.0