Rootless Docker requires newuidmap/newgidmap kernel support which
doesn't work in nested Docker containers (Operation not permitted).
Added docker_rootless_setup variable (default true) to skip the
daemon startup steps. Set to false in all molecule converge playbooks
so tests verify package installation, user creation, service file
rendering, and config without requiring a working rootless daemon.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 22:37:31 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Three changes:
1. CI: add set -e and fail-fast: true to stop on first molecule failure
instead of continuing (all pairs fail for same reason anyway)
2. Docker APT repo: use copy module to write sources.list directly
instead of apt_repository module which wasn't picking up the repo
3. Fix arch mapping: ansible_facts returns x86_64, Docker repo needs amd64
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 22:31:08 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ansible_facts['architecture'] returns x86_64 but Docker APT repo
expects amd64. Added docker_apt_arch mapping. Also added debug tasks
to show apt sources and apt-cache search results for docker-ce.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 22:24:02 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The apt_repository update_cache option wasn't reliably picking up the
new Docker APT repo. Split into separate apt update step. Also moved
the long repo string to a default variable to satisfy yaml line-length
lint rule.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 22:15:28 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The geerlingguy Docker containers don't include curl or gpg, which
are needed by the rootless Docker role to download and dearmor the
Docker APT repository GPG key. Added these prerequisites to the
molecule common prepare playbook.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 22:07:55 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The deb822_repository module isn't available in the CI Ansible
collection. Reverted to apt_repository but now properly dearmors
the GPG key using gpg --dearmor before referencing it in signed-by.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 21:46:33 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The apt_repository module with signed-by wasn't working because the
downloaded GPG key wasn't properly dearmored. The deb822_repository
module handles GPG key download and dearmoring automatically.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 21:35:19 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The rootless_docker.yml task was trying to apt install docker-ce
without first adding the Docker APT repository, causing package not
found errors on Debian/Ubuntu containers.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 21:23:54 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Rootless Docker requires loginctl enable-linger and systemctl --user,
which need systemd as PID 1 inside the container. Updated all platform
entries to use /lib/systemd/systemd (or /usr/lib/systemd/systemd for
Arch) as the container command instead of sleep infinity.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 21:12:43 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The regex only matched "N passed in X.XXs" but pytest can output
"N passed, M warnings in X.XXs". Updated regex to handle both.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 21:06:41 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Three major improvements:
1. Rootless Docker refactor: Removes docker/binary modes, unifies to
rootless Docker with per-runner system users. Each runner gets its
own rootless Docker daemon, systemd user service, and isolated
environment. Simplifies CLI (removes --mode option), Ansible role
(single code path), and molecule scenarios (removes binary scenario).
2. Auto-merge fix: Fixes status check context mismatch in branch
protection (was requiring "lint", "unit-tests", "molecule-tests" but
actual contexts are "CI / quality", "CI / molecule-tests*"). Adds
retry/wait logic to auto_merge.py that polls commit statuses for up
to 15 minutes before attempting merge, eliminating the chicken-and-egg
problem where auto-merge would fail because CI hadn't completed yet.
3. Molecule platform matrix: Adds OS platform matrix to CI — all 6
scenarios now run on all 4 supported OSes (ubuntu-2204, ubuntu-2404,
debian-12, archlinux) = 24 test pairs distributed across 3 parallel
runners. Updates distribute_molecule.py to distribute (scenario,
platform) pairs. Updates Makefile with molecule-all target for
local multi-platform testing.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Add shared config.py with API URLs, regexes, timeouts, pagination
- Add GiteaClient and VikunjaClient in api_clients.py with pooled sessions
- Add APIError exception for unified HTTP error handling
- Refactor all scripts to use shared modules and http.HTTPStatus
- Rewrite unit tests to mock clients and use HTTPStatus constants
- Add tests for api_clients and config modules
- Achieve 100% test coverage
- Replace argparse/print/sys.exit with click commands and ClickException
- Translate all user-facing messages via _()
- Add friendly Oops! / Nice! prompts
- Wrap HTTP errors in all scripts with user-friendly translated messages
- Update all unit tests to use CliRunner and expect ClickException
- Add 100% branch coverage for new HTTP error handling branches
- Add missing translation keys to i18n.py
- Fix pre-commit hook to use venv Python for validate_commit_msg.py
The lifecycle scenario runs in a Docker container without systemd
as PID 1. The side_effect and verify playbooks used systemd module
operations unconditionally, causing failures like:
System has not been booted with systemd as init system
Add a systemd availability check (/run/systemd/system stat) to both
playbooks and conditionally skip systemd tasks when running in
environments without systemd (e.g. Molecule Docker containers).
The multi-instance scenario verify playbook expects config files at
/etc/gitea-runner/<name>/config.yaml (binary mode path). Without
runner_mode set, the role defaulted to docker mode, which places the
config in /var/lib/gitea-runner/<name>/config.yaml instead.
Add runner_mode: binary to both converge plays so config placement
matches the verify assertions.
The binary_mode.yml task file did not create gitea_runner_data_dir
when runner registration was skipped (as in molecule tests). This
caused the binary scenario verify playbook to fail because the
data directory assertion expected it to exist.
Add an explicit directory creation step before config creation,
mirroring the docker_mode.yml structure.
The molecule target iterates through 7 scenarios. The previous
relative cd into ansible/roles/gitea-runner failed after the first
iteration because the shell was already inside that directory.
Using $(CURDIR) ensures each loop iteration starts from the project
root.
- Add CONTRIBUTING.md with branch naming, commit format, and PR workflow
- Add TROUBLESHOOTING.md with common issues and solutions
- Update README.md with CI badge and commit convention section
- Update pyproject.toml with pythonpath and coverage settings for scripts
Replace include_tasks with include_role + tasks_from in disable, remove,
start, enable, stop, and status playbooks. include_tasks does not load
role defaults, causing undefined variable errors (e.g. gitea_runner_data_dir)
when deregistering or registering runners.
- _resolve_runner now returns gitea_url from registry so disable/remove
can reuse the URL stored at install time without requiring env vars.
- Added --url option to install, disable, and remove CLI commands.
- remove(force=True) no longer requires gitea_url or token.
- Moved _parse_status outside the for loop in list_runners.
- Updated all translations and tests to match.
- docker inspect -f "{{.State.Status}}" used Go template braces that
conflicted with Ansible Jinja2 templating in the shell module.
Ansible tried to parse {{.State.Status}} as a Jinja2 variable (which
starts with a dot, making it invalid), causing a local template error.
The outer except Exception caught this immediately, so the fallback
loop never reached the legacy container name or systemctl checks.
- Replaced with: docker inspect <name> | python3 -c JSON parsing,
which avoids any brace syntax and uses python3 (already required by
Ansible on managed nodes).
- Added per-iteration try/except inside the fallback loop so a failure
on one container name continues to the next fallback instead of
aborting the entire check.
- Added tests for fallback behavior and binary mode exception path.
128 tests, 100% coverage, ruff + pyright clean
- systemd template for docker mode now uses Type=oneshot + RemainAfterExit=yes
so that systemctl is-active returns active when the container is running.
Previously docker run -d exited immediately, causing systemd to mark the
service as inactive even though the container was still up.
- grm list now tries multiple container name fallbacks for docker mode:
1. gitea-runner-{name} (current naming)
2. gitea-runner-{host} (legacy installs where name defaulted to host)
3. systemctl is-active gitea-runner@{name} (for installs with fixed template)
- All tests pass, 100% coverage, ruff + pyright clean
- Docker mode runners now check container status via docker inspect
instead of systemctl is-active, avoiding false unknown when systemd
service is missing or stderr output is discarded
- Binary mode still uses systemctl is-active with stderr suppressed
- Both modes now show a translated context message before the check so
users know which host/user each BECOME password prompt belongs to
- Better ansible output filtering: strip CHANGED/FAILED/UNREACHABLE
header lines and separator noise
- Map Docker container states (running/exited/dead) to systemd vocabulary
- All new user-facing messages fully translated (en/bg/de/ru/zh)
- 125 tests, 100% coverage, pyright clean, ruff clean
- run_ad_hoc() now accepts ask_become_pass and check parameters
- list_runners() passes ask_become_pass=True so --ask-become-pass is
added when running in a TTY, matching playbook behavior
- list_runners() passes check=False so systemctl is-active non-zero
exit codes (inactive=3, unknown=4) don't raise exceptions; the
actual status string is parsed from stdout instead
- TTY guard prevents --ask-become-pass from hanging in non-interactive
environments (CI, scripts)
- 123 tests, 100% coverage, pyright clean, ruff clean
- Change --ask-become-pass from opt-in to opt-out across all commands
(install, update, start, stop, enable, disable, status, remove)
- Use Click toggle pattern: --ask-become-pass/--no-ask-become-pass with
default=True so users are always prompted for sudo unless they
explicitly opt out
- Update i18n translations for both help texts
- Update all CLI tests to expect ask_become_pass=True as default and
add test for --no-ask-become-pass
- Update README: remove --ask-become-pass from examples, document
--no-ask-become-pass for passwordless-sudo setups
- 120 tests, 100% coverage, pyright clean, ruff clean
- run_ad_hoc() now raises AnsibleError on non-zero exit, surfacing
stderr instead of silently returning empty stdout
- list_runners() passes become=True to run_ad_hoc since systemctl
is-active requires root privileges
- Add i18n translations for ad-hoc failure messages
- Add unit test for run_ad_hoc failure case
- Update list_runners test to expect become=True
- 119 tests, 100% coverage, pyright clean, ruff clean
- Add force parameter to RunnerManager.remove() — skips remote Ansible
playbook and only removes the local registry entry
- Add --force/-f CLI flag to grm remove command
- Add translations for --force help text across all 5 languages
- Add unit tests for force skip and CLI flag propagation
- 118 tests, 100% coverage, pyright clean, ruff clean
- Remove console StreamHandler from get_logger() — say() already handles
console output via click.echo(); having both caused every message to
appear twice
- Move GRM_LOG_LEVEL filtering into ui.say() via _console_level() so
console verbosity is still user-controllable while the log file always
captures everything at DEBUG
- Remove [GRM] prefix from say() calls — no longer needed without
duplicate logger output, giving cleaner user-facing messages
- Update test_logging_config.py: remove console handler tests and
_level_from_env tests (now in test_ui.py), expect 1 handler only
- Add test_ui.py coverage for _console_level and say() level filtering
- Update README to document single-path console output via click.echo
- 116 tests, 100% coverage, pyright clean, ruff clean