Add /docs/ directory with user and technical documentation extracted from README, AGENTS.md, and source code. Add scripts/sync_wiki.py to sync docs to Gitea wiki via API. Add scripts/doc_coverage.py to check CLI commands, modules, and CI scripts are documented. Add sync-wiki.yml workflow for auto-sync on merge and release. Slim down README.md to lean entry point. 28 new unit tests, 100% coverage maintained.
Closes GRM-36
The release script now runs lint and tests before committing or tagging. If either fails, the release is aborted. Also fixes test_cli_version to use __version__ dynamically.
Closes GRM-35
Parallel molecule execution within a single runner caused conflicts
(shared temp directories, Docker network collisions). Rewrote
molecule_ci_guard.py to run pairs sequentially while still polling
the Gitea API for cross-runner cancellation.
Each pair now gets its own subprocess with proper environment setup
(MOLECULE_PLATFORM_NAME/IMAGE/COMMAND), and output streams directly
to CI logs for debugging.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 23:53:13 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Gitea Actions does not implement fail-fast/max-parallel for matrix jobs,
so a failing runner does not stop the others. Added molecule_ci_guard.py
which polls the Gitea API in a background thread. If any other molecule
runner reports failure, the current runner kills its molecule subprocess
and exits early.
CI returns to a 3-runner matrix; each runner executes its assigned pairs
in parallel via run_molecule_parallel.py, guarded by molecule_ci_guard.py.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 23:37:52 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The initial wait loop used proc.wait(timeout=0.5) which could raise
subprocess.TimeoutExpired and crash the runner. Added a try/except and
increased timeout to 5s so the runner polls correctly without crashing.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 23:30:36 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Added scripts/run_molecule_parallel.py to run a runner's assigned
(scenario, platform) pairs in parallel. If any subprocess fails, the
remaining ones are terminated with SIGTERM/SIGKILL and the runner
exits immediately. This gives fast feedback without continuing to run
tests that are guaranteed to fail for the same reason.
CI workflow now calls this script per matrix runner. Added fail-fast and
max-parallel for best-effort cancellation across runners.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-20 23:19:25 +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
- _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
- 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
- Create ui.py with say() helper that routes messages to both click.echo()
(console/stdout, user-facing) and logging.getLogger('grm') (file audit trail)
- Update executor.py: replace logger.info() with say() for start, status, done
messages; use say(level=ERROR, err=True) before raising AnsibleError
- Update report.py: replace logger.info() with say() for operation report lines
- Update all unit tests to patch say() instead of using capsys or get_logger
- Add test_ui.py with coverage for say() calling both click.echo and logging
- 117 tests, 100% coverage, pyright clean, ruff clean
- Create logging_config.py with get_logger() providing dual handlers:
- Console handler (stderr) controlled by GRM_LOG_LEVEL env var (default INFO)
- File handler (~/.local/state/grm/logs/grm.log) capturing everything at DEBUG
- Replace all print() calls in executor.py and report.py with logger.info()/error()
- Add error logging before raising AnsibleError in executor.run()
- Add GRM_LOG_LEVEL to README configuration table and logging documentation
- Update all unit tests to mock logger instead of using capsys
- 114 tests, 100% coverage, pyright clean, ruff clean
- Create StepTracker context manager in new report.py module
- Track each step of lifecycle operations (install, update, start, stop, enable,
disable, status, remove) with begin/done markers
- On success: report lists all completed steps with ✓ icons
- On failure: report shows failed step (✗), completed steps before failure (✓),
and pending steps that never started (○)
- Add translations for report header, status labels, and registry step names
in all 5 supported languages (EN/BG/DE/RU/ZH)
- 105 tests, 100% coverage, pyright clean, ruff clean
- Add start, stop, enable, disable, status, remove methods to RunnerManager
- Add corresponding CLI subcommands: grm start/stop/enable/disable/status/remove
- Add i18n translations for lifecycle commands across all supported languages
- Add comprehensive unit tests for lifecycle methods and CLI commands
- Fix environment variable leakage in CLI tests for GITEA_URL