# Architecture GRM consists of two layers: 1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. 2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea. ## Component Tree ``` grm install └── RunnerManager.install() └── ansible-playbook ansible/install-runner.yml └── role: gitea-runner ├── user_setup.yml (create per-runner system user + lingering) ├── rootless_docker.yml (rootless Docker setup under runner user) ├── install_runner.yml (download binary, config, register, service) ├── prune.yml (Docker prune timer) └── integration_test.yml (validate service is active) ``` The Ansible role task execution order (from `AGENTS.md`): ``` main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test ``` - `install_runner.yml` handles: download, config, validate, register, service - `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates) - `systemctl --user` tasks must be guarded by `docker_rootless_setup` - Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files) ## Per-Runner Isolation Each runner runs as a systemd user service under a dedicated system user (`grm-`). Each instance has fully isolated resources: - **User**: `grm-` (dedicated system user with lingering enabled) - **Home**: `/home/grm-/` - **Data**: `/var/lib/gitea-runner//` - **Config**: `/etc/gitea-runner//` - **Service**: `gitea-runner.service` (systemd user service) - **Docker socket**: `/run/user//docker.sock` (rootless, per-runner) ## Component Interactions ```mermaid flowchart TD CLI["Python CLI
src/gitea_runner_manager/
(Click)"] RM["RunnerManager
runner_manager.py"] EXEC["Executor
executor.py"] REG["Registry
registry.py
~/.local/share/grm/runners.json"] ANS["ansible-playbook subprocess"] ROLE["Ansible Role
ansible/roles/gitea-runner/"] USER["user_setup.yml
create system user + lingering"] DOCKER["rootless_docker.yml
rootless Docker setup"] INSTALL["install_runner.yml
download, config, register, service"] PRUNE["prune.yml
Docker prune timer"] TEST["integration_test.yml
validate service active"] GITEA["Gitea instance
registration + API"] SYSTEMD["systemd user service
gitea-runner.service"] CLI --> RM RM --> REG RM --> EXEC EXEC -->|subprocess| ANS ANS --> ROLE ROLE --> USER ROLE --> DOCKER ROLE --> INSTALL ROLE --> PRUNE ROLE --> TEST INSTALL -->|register| GITEA INSTALL --> SYSTEMD DOCKER --> SYSTEMD ``` ## Additional Components From `AGENTS.md`, the project also includes: - **CI Scripts** (`scripts/`) — Automation for auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications - **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits ## Python Modules The Python CLI layer (`src/gitea_runner_manager/`) consists of the following modules: | Module | Description | |--------|-------------| | `cli.py` | Click-based CLI entry point — defines all commands (install, update, start, stop, enable, disable, status, remove, list) | | `runner_manager.py` | Ansible orchestration + registry integration — delegates to executor and manages runner lifecycle | | `executor.py` | Ansible subprocess execution — runs `ansible-playbook` with extra-vars via temp JSON files | | `registry.py` | Local JSON runner registry at `~/.local/share/grm/runners.json` — stores connection metadata | | `i18n.py` | Internationalization translations (en, bg, de, ru, zh) | | `exceptions.py` | Custom exceptions (`GRMError`, `APIError`) | | `api_clients.py` | Gitea and Vikunja API client classes for CI automation scripts | | `config.py` | Configuration constants (API URLs, repo owner/name, project IDs) — overridable via environment variables |