Compare commits

..
Author SHA1 Message Date
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> cea84b1d89 fix: disable systemd-oomd memory pressure kill for runner users
The default systemd user service configuration (10-oomd-user-service-defaults.conf)
sets ManagedOOMMemoryPressure=kill with ManagedOOMMemoryPressureLimit=50%.
Under parallel DinD load (6 molecule tests), memory pressure exceeds 50%
for 30 seconds, causing systemd-oomd to SIGKILL molecule containers
(rc=137). This was the root cause of recurring "container is not running"
and "Failed to create temporary directory" errors in CI.

Fix: Create a per-user systemd override that sets:
- ManagedOOMMemoryPressure=auto (don't kill on pressure)
- ManagedOOMMemoryPressureLimit=100% (never trigger)
- OOMScoreAdjust=-500 (very unlikely to be OOM-killed)

The runner users run Docker-in-Docker workloads that legitimately use
large amounts of memory. systemd-oomd's aggressive memory pressure
killing is inappropriate for CI runner workloads.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 23:09:46 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 645d87a3cf feat: add debug logging to healthcheck (image count, disk, docker df)
Logs image count and docker system df output at each healthcheck run
to diagnose "No such image" failures. When the gentle prune removes
images, the log shows the image count dropping to 0, making the root
cause immediately visible instead of requiring forensics.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 19:11:11 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> e58f4c9da4 fix: stop gentle prune from removing CI runner images
The gentle prune (70% disk threshold) was running
docker image prune -af --filter "until=1h" every 2 minutes.
This removed the CI runner image (ci-full:latest, ~2.3GB)
between jobs, causing "No such image" errors when act_runner
tried to create job containers with forcePull=false.

The host disk normally sits at 72-74%, above the 70% warn
threshold, so the gentle prune was running constantly. With
the CI image unused between jobs, it was pruned after 1 hour.

Fix: remove docker image prune from the gentle path. Only
prune containers, volumes, and networks at 70%. Images are
only pruned at the 75% critical threshold when disk is
genuinely full.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 18:50:41 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 9c786ec4df revert: restore original healthcheck thresholds (2min, 70%, 75%)
The 1min interval + 70% critical threshold caused more problems than
it solved. The host disk normally sits at ~74% — just under the
original 75% critical threshold. Lowering to 70% triggered aggressive
docker system prune -af --volumes every single minute, which:

1. Wiped all cached images, forcing all 6 parallel slots to re-pull
   2-3GB each simultaneously — increasing disk pressure, not reducing it
2. Caused network prune to remove networks mid-job → "network not found"
   errors in Gitea Actions

Reverting to 2min/70%warn/75%critical. Images stay cached, only gentle
until=1h pruning runs at 70%, and full prune only triggers at 75%
(above the normal 74% baseline).

The network prune --filter "until=1h" fix from the previous commit is
kept — that was a genuine bug fix.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 11:54:41 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> d57d6a3591 fix: use until=1h filter for network prune in critical path
The critical disk prune path was doing `docker network prune -f`
without a filter, which removes ALL unused networks. This races with
Gitea Actions jobs that are mid-startup: the runner creates a network,
then the healthcheck prunes it before the container attaches, causing
"network not found" errors.

Use `--filter "until=1h"` (same as the warn path) to only remove
networks older than 1 hour.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 10:34:45 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c7323d5f10 fix: remove userland-proxy=false (incompatible with rootless Docker)
userland-proxy: false requires bridge-nf-call-iptables which is not
available in rootless Docker. This caused dockerd to fail with:
"cannot restrict inter-container communication or run without the
userland proxy: stat /proc/sys/net/bridge/bridge-nf-call-iptables:
no such file or directory"

Keep log-opts (max-size=10m, max-file="3") which works correctly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 09:43:35 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 8047ea6061 fix: quote max-file as string in daemon.json
Docker's daemon.json parser expects max-file as a string, not an
integer. The unquoted Jinja template rendered "max-file": 3 which
caused dockerd to fail with "cannot unmarshal number into Go struct
field LogConfig.log-opts of type string".

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 09:37:36 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 3f22910a7d fix: reduce healthcheck interval to 1min and disk thresholds
Runners are consistently running out of disk during parallel molecule
tests. The 2min healthcheck interval is too coarse — a single molecule
scenario can fill 10+ GB in 2 minutes. Reduce to 1min and lower disk
thresholds (60% warn, 70% critical) so pruning kicks in earlier.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 09:04:24 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c192929e34 fix: tune rootless Docker daemon for parallel DinD reliability
Add userland-proxy=false and log-opts (10m, 3 files) to the rootless
Docker daemon.json. The userland proxy adds overhead under parallel
container creation and can become a bottleneck during CI molecule
tests. Log rotation prevents disk exhaustion from container logs under
heavy DinD load, which caused the daemon to become unresponsive
(context deadline exceeded on /_ping).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 01:19:13 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 20aefa57c0 fix: remove molecule-tests from auto-merge needs to prevent skip cascade
CI / validate (pull_request) Successful in 1m45s
CI / molecule-tests (1) (pull_request) Skipped
CI / molecule-tests (2) (pull_request) Skipped
CI / molecule-tests (3) (pull_request) Skipped
CI / molecule-tests (4) (pull_request) Skipped
CI / auto-merge (pull_request) Successful in 1m11s
Gitea Actions skips dependent jobs of skipped jobs without evaluating
if: conditions. When molecule-tests is skipped (ansible-changed=false),
auto-merge was also skipped because it had molecule-tests in needs.
Remove molecule-tests from needs and rely on validate success only.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:31 +02:00
Emil Simeonov 811d07bfe1 docs: trigger CI re-run after PR title fix 2026-08-10 00:43:31 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 3c25fa6fdc ci: wire devx lint checks into CI and pre-commit hooks
- Add 5 lint checks to make lint-all: check-ansible-no-log,
  check-ansible-no-state-absent-on-db, check-ansible-patterns,
  check-jinja-expr, check-ansible-set-fact-to-json
- Add corresponding pre-commit hooks with file-pattern triggers
- Bump devx pin from v0.48.1 to v0.50.0

Closes GRM-161

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:31 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 9a33287258 fix(healthcheck): lower critical prune threshold to 75%
At 76%, molecule containers fail with "container is not running" because
overlay2 runs out of space under parallel DinD load. The gentle prune
(until=1h) at 75% was insufficient — runners stayed at 76% because
nothing was older than 1h on busy runners.

Lower the critical threshold to 75% so the full prune (no until filter)
triggers immediately when disk reaches the level where tests fail. Also
lower the gentle prune threshold to 70% to start cleanup earlier.

This matches the infra CI disk gate which skips tests at 75%+.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:31 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7af29f4e3c feat(healthcheck): add two-tier disk prune with critical threshold
The healthcheck previously used a single 75% threshold with an
until=1h filter. On busy runners with constant CI jobs, nothing
is older than 1h, so the prune barely frees anything — disk stays
at ~76% and the CI disk gate (80%) skips all molecule tests.

Two-tier approach:
- 75-79%: gentle prune (until=1h) — same as before
- 80%+:   full prune (no until filter) — removes ALL stopped
          containers and unused images regardless of age, plus
          builder cache and networks

This ensures that when disk pressure is critical, the healthcheck
actually reclaims space instead of skipping everything that's
recently created.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:31 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 1c8edb55d8 fix(ci): use pushd/popd instead of molecule --cwd
Molecule doesn't support --cwd flag. Use pushd/popd to change
directory before running molecule test/destroy.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:20 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 26a32dcf28 fix(ci): use shell: bash for molecule test step
The inline molecule runner uses bashisms (here-strings `<<<`,
parameter expansion) that dash/sh doesn't support. Gitea Actions
defaults to sh, causing "Syntax error: redirection unexpected".

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:20 +02:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> aec957f06f refactor: remove molecule_ci_guard — adds complexity, no value
The cross-runner cancellation guard was killing healthy molecule slots
when unhealthy slots failed, defeating fail-fast: false. Replace with
direct molecule test execution.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 00:43:20 +02:00
9 changed files with 79 additions and 26 deletions
-6
View File
@@ -2,12 +2,6 @@
All notable changes to this project will be documented in this file.
## [0.21.0] - 2026-08-09
### Features
- *(healthcheck)* Add two-tier disk prune with critical threshold
## [0.20.0] - 2026-08-09
### Features
+6 -6
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/python.svg)](https://www.python.org/downloads/)
## Why GRM?
+12 -3
View File
@@ -36,9 +36,10 @@ gitea_runner_prune_label: "gitea-runner=true"
gitea_runner_service_restart_sec: "5"
# Health check configuration
# 2min interval — catches hung daemons before multiple CI jobs fail between checks.
# The previous 5min interval was too coarse: a stuck daemon could fail 3+ molecule
# jobs in the window between healthcheck runs.
# 2min interval — catches hung daemons before multiple CI jobs fail between
# checks. The 1min interval caused excessive pruning which removed cached
# images, forcing all 6 parallel slots to re-pull simultaneously and
# actually increasing disk pressure.
gitea_runner_healthcheck_interval: "2min"
gitea_runner_healthcheck_boot_delay: "2min"
gitea_runner_healthcheck_disk_threshold: 70
@@ -47,6 +48,10 @@ gitea_runner_healthcheck_disk_threshold: 70
# This removes all stopped containers and unused images regardless of age.
# At 75%+, molecule containers fail with "container is not running" because
# overlay2 runs out of space under parallel DinD load.
# IMPORTANT: keep at 75 (not lower) — the host disk normally sits at ~74%.
# Lowering to 70 triggers full prune every cycle, wiping cached images and
# forcing all parallel slots to re-pull simultaneously, which increases
# disk pressure rather than reducing it.
gitea_runner_healthcheck_disk_critical: 75
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
@@ -76,6 +81,10 @@ gitea_runner_docker_shutdown_timeout: 30
gitea_runner_docker_max_concurrent_downloads: 3
gitea_runner_docker_max_concurrent_uploads: 3
gitea_runner_docker_default_nofile: 65536
# Log file size limits — under parallel DinD load, container logs can fill
# disk and cause the daemon to become unresponsive. Limit log size per container.
gitea_runner_docker_max_log_size: "10m"
gitea_runner_docker_max_log_files: 3
# Admin token for runner deregistration via Gitea API.
# If not set, falls back to registration_token (which likely lacks admin scope).
@@ -10,3 +10,7 @@
- ansible_facts is defined
- ansible_facts['service_mgr'] | default('') == 'systemd'
- gitea_runner_docker_rootless_setup
- name: Reload systemd user daemon
ansible.builtin.systemd:
daemon_reload: true
@@ -180,6 +180,10 @@
"features": {
"containerd-snapshotter": false
},
"log-opts": {
"max-size": "{{ gitea_runner_docker_max_log_size }}",
"max-file": "{{ gitea_runner_docker_max_log_files }}"
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
@@ -283,6 +287,10 @@
"features": {
"containerd-snapshotter": false
},
"log-opts": {
"max-size": "{{ gitea_runner_docker_max_log_size }}",
"max-file": "{{ gitea_runner_docker_max_log_files }}"
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
@@ -69,3 +69,31 @@
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
mode: "0755"
- name: Disable systemd-oomd memory pressure kill for runner user
when: gitea_runner_systemd_available.stat.exists
block:
- name: Ensure user service override directory exists
ansible.builtin.file:
path: "/etc/systemd/system/user@{{ gitea_runner_uid }}.service.d"
state: directory
owner: root
group: root
mode: "0755"
- name: Disable ManagedOOMMemoryPressure for runner user
ansible.builtin.copy:
content: |
[Service]
ManagedOOMMemoryPressure=auto
ManagedOOMMemoryPressureLimit=100%
OOMScoreAdjust=-500
dest: "/etc/systemd/system/user@{{ gitea_runner_uid }}.service.d/oomd-override.conf"
owner: root
group: root
mode: "0644"
notify: Reload systemd user daemon
- name: Reload systemd daemon for oomd override
ansible.builtin.systemd:
daemon_reload: true
@@ -223,6 +223,10 @@ fi
# 3. Check disk space — prune aggressively if below threshold
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
# Debug: log image count and sizes before pruning decisions
img_count=$(timeout 10 docker images -q 2>/dev/null | wc -l || echo "?")
img_size=$(timeout 10 docker system df --format '{% raw %}{{.Type}} {{.Size}} {{.Reclaimable}}{% endraw %}' 2>/dev/null | grep "Images" || echo "unknown")
echo "INFO: disk=${disk_pct}% images=${img_count} | ${img_size}"
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_critical }} ]]; then
echo "CRITICAL: Disk usage at ${disk_pct}% (>= {{ gitea_runner_healthcheck_disk_critical }}%), full prune"
# Critical level: remove ALL stopped containers (no age filter) and ALL
@@ -234,7 +238,10 @@ if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_critical }} ]]; then
| awk '{print $1}' \
| xargs -r docker rm -f 2>/dev/null || true
docker system prune -af --volumes || true
docker network prune -f || true
# Prune networks older than 1 hour only — removing ALL networks can
# race with Gitea Actions jobs that are mid-startup (network created
# but container not yet attached), causing "network not found" errors.
docker network prune -f --filter "until=1h" || true
docker builder prune -af || true
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
echo "INFO: Disk usage after full prune: ${disk_pct}%"
@@ -251,9 +258,12 @@ elif [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
| grep -E '(hour|day|week|month|year)s? ago' \
| awk '{print $1}' \
| xargs -r docker rm -f 2>/dev/null || true
# Prune images and containers older than 1h (until filter is NOT
# supported with --volumes, so prune volumes separately without a filter).
docker image prune -af --filter "until=1h" 2>/dev/null || true
# Prune containers and volumes older than 1h. Do NOT prune images here —
# the CI runner image (ci-full:latest, ~2.3GB) is unused between jobs and
# would be removed by image prune, forcing a re-pull on every CI job
# (84+ seconds per pull, and the pull can fail silently causing
# "No such image" errors). Images are only pruned at the critical
# threshold (75%) when disk is genuinely full.
docker container prune -f --filter "until=1h" 2>/dev/null || true
docker volume prune -f 2>/dev/null || true
# Prune networks older than 1 hour to avoid removing networks that
+6 -6
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/a9578eaecfc5bba4f295c16f9045be6f094d637d/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/918ffe7df0c91464112f012126e3fd60a0dfa2d5/python.svg)](https://www.python.org/downloads/)
## Overview
+1 -1
View File
@@ -1,3 +1,3 @@
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
__version__ = "0.21.0"
__version__ = "0.20.0"