Compare commits

...
17 Commits
Author SHA1 Message Date
grm-ci-bot 2d2eaa3291 release: v0.18.7 [skip ci] 2026-08-06 09:30:18 +00:00
kiretoandemo 8176a62885 GRM-159: fix: move StartLimit to [Unit] and make prune timer reload conditional
Post-merge / detect-and-configure (push) Successful in 3m49s
Post-merge / release-and-maintain (push) Successful in 3m43s
Co-authored-by: kireto <kireto@oblachno.com>
2026-08-06 09:23:29 +00:00
gitea-actions-bot 443756a508 chore: update badge URLs to commit 36201d0d [skip ci] 2026-08-06 00:00:05 +00:00
grm-ci-bot 340222e041 release: v0.18.6 [skip ci] 2026-08-05 23:59:22 +00:00
kiretoandemo 179e47bbb2 GRM-158: fix: pre-configure daemon.json before rootless setuptool + add DBUS_SESSION_BUS_ADDRESS
Post-merge / detect-and-configure (push) Successful in 1m2s
Post-merge / release-and-maintain (push) Successful in 1m38s
Co-authored-by: kireto <kireto@oblachno.com>
2026-08-05 23:57:25 +00:00
gitea-actions-bot 68d16577b3 chore: update badge URLs to commit 169df915 [skip ci] 2026-08-05 20:21:20 +00:00
grm-ci-bot 38607d9f29 release: v0.18.5 [skip ci] 2026-08-05 20:20:43 +00:00
kiretoandemo 90139b306b GRM-157: fix: pin Docker 28.x + disable containerd snapshotter + tune prune/disk
Post-merge / detect-and-configure (push) Successful in 1m8s
Post-merge / release-and-maintain (push) Successful in 1m31s
Co-authored-by: kireto <kireto@oblachno.com>
2026-08-05 20:18:38 +00:00
gitea-actions-bot dd475bec0d chore: update badge URLs to commit 83a5b577 [skip ci] 2026-08-05 13:53:01 +00:00
grm-ci-bot 0f0ada3576 release: v0.18.4 [skip ci] 2026-08-05 13:52:28 +00:00
emo 185e41c49e GRM-156: fix: harden rootless Docker daemon resilience on CI runners
Post-merge / detect-and-configure (push) Successful in 1m9s
Post-merge / release-and-maintain (push) Successful in 1m15s
2026-08-05 13:50:34 +00:00
gitea-actions-bot 103741b3ab chore: update badge URLs to commit 7136903b [skip ci] 2026-08-04 14:04:34 +00:00
grm-ci-bot b770d1debf release: v0.18.3 [skip ci] 2026-08-04 14:03:59 +00:00
gitea-adminandemo 2f11489be0 GRM-2: fix: switch default network driver to slirp4netns (pasta TCP RST bug)
Post-merge / detect-and-configure (push) Successful in 1m8s
Post-merge / release-and-maintain (push) Successful in 1m22s
Co-authored-by: oblachno Admin <admin@oblachno.oblachno.fyi>
2026-08-04 14:01:56 +00:00
gitea-actions-bot 8e9e58fedb chore: update badge URLs to commit 69e62973 [skip ci] 2026-07-17 02:03:11 +00:00
emil 70d985ebaa GRM-155: chore: bump devx to v0.47.2
Post-merge / detect-and-configure (push) Waiting to run
Post-merge / release-and-maintain (push) Waiting to run
2026-07-17 02:00:23 +00:00
gitea-actions-bot 66f071b676 chore: update badge URLs to commit 7ee78d01 [skip ci] 2026-07-16 17:42:23 +00:00
36 changed files with 463 additions and 107 deletions
+12
View File
@@ -0,0 +1,12 @@
extends: existence
message: "Don't attribute human qualities to software or hardware ('%s')."
link: https://developers.google.com/style/anthropomorphism
level: suggestion
ignorecase: true
# Limited to the two verbs the guide itself names. Broader lists (wants, knows,
# thinks) can't tell a software subject from a human one: on a 950-file corpus
# they produced 8 false positives ('the customer wants', 'your audience knows')
# for every 2 real ones.
tokens:
- sees
- tells
+7 -2
View File
@@ -1,8 +1,13 @@
extends: existence
message: "'%s' should be in lowercase."
link: 'https://developers.google.com/style/colons'
nonword: true
level: warning
scope: sentence
# The match is the word itself, not ': X', and `nonword` is off. Both are
# required for a project Vocab to work: Vale compares accept.txt entries
# against the matched text, and `nonword: true` opts out of that entirely.
# So a proper noun after a colon can be exempted by adding it to accept.txt.
# The guide's other exemption, notice labels, is handled by the lookbehinds;
# headings are already excluded by `scope: sentence`. See issue #20.
tokens:
- '(?<!:[^ ]+?):\s[A-Z]'
- '(?<!Note: )(?<!Caution: )(?<!Warning: )(?<!Success: )(?<=:\s)[A-Z]\w+'
+1 -1
View File
@@ -6,4 +6,4 @@ level: error
nonword: true
tokens:
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)|May|Jun(?:e)|Jul(?:y)|Aug(?:ust)|Sep(?:tember)?|Oct(?:ober)|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
+14
View File
@@ -0,0 +1,14 @@
extends: existence
message: "Avoid the unverifiable claim '%s'."
link: https://developers.google.com/style/excessive-claims
level: suggestion
ignorecase: true
# The guide also names 'never', 'always', and 'ensure', but in technical writing
# those are usually legitimate instructions ('never commit secrets') rather than
# product claims: they accounted for 125 of 142 hits on a 950-file corpus.
# 'best practices' is a fixed term, not a superlative.
tokens:
- 'best(?! practices?)'
- simplest
- fastest
- guarantees?
+6 -4
View File
@@ -3,11 +3,13 @@ message: "Avoid first-person pronouns such as '%s'."
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
ignorecase: true
level: warning
nonword: true
# The 'I' tokens use lookaround rather than consuming the surrounding
# whitespace. Matching ' I ' made the alert span cover both spaces, which shows
# up as a too-wide underline in editors, and read as "such as ' I '". Dropping
# `nonword` also lets a project Vocab apply, which it can't when set. See PR #50.
tokens:
- (?:^|\s)I\s
- (?:^|\s)I,\s
- \bI'm\b
- '(?<=^|\s)I(?=[\s,])'
- "\\bI'm\\b"
- \bme\b
- \bmy\b
- \bmine\b
+5 -2
View File
@@ -4,8 +4,11 @@ link: "https://developers.google.com/style/capitalization#capitalization-in-titl
level: warning
scope: heading
match: $sentence
indicators:
- ":"
# No `indicators: [":"]` here. That makes Vale require a capital after a colon,
# which is the Microsoft convention this rule was originally copied from. This
# guide says the opposite: "the first word after a colon is generally
# lowercase" (developers.google.com/style/colons), and Colons.yml enforces
# exactly that. See issue #58.
exceptions:
- Azure
- CLI
+13
View File
@@ -0,0 +1,13 @@
extends: existence
message: "Avoid the jargon '%s'."
link: https://developers.google.com/style/jargon
level: suggestion
ignorecase: true
# The guide also cites 'solution', 'support', and 'workload' as overloaded
# terms, but those have ordinary technical meanings and accounted for every hit
# on a 950-file corpus, so only the unambiguous figurative terms are listed.
tokens:
- break-glass
- camel ?case
- out-of-the-box
- swim ?lane
+6 -2
View File
@@ -6,6 +6,10 @@ level: error
nonword: true
action:
name: replace
# The delimiter is a lookahead so the replacement doesn't swallow the comma or
# space that follows (issue #18). `$` is included so the abbreviation is still
# caught at the end of a heading, table cell, or block, which accounted for 8
# of 10 occurrences on a 950-file corpus.
swap:
'\b(?:eg|e\.g\.)(?=[\s,;])': for example
'\b(?:ie|i\.e\.)(?=[\s,;])': that is
'\b(?:eg|e\.g\.)(?=[\s,;]|$)': for example
'\b(?:ie|i\.e\.)(?=[\s,;]|$)': that is
+22 -1
View File
@@ -3,5 +3,26 @@ message: "Use the Oxford comma in '%s'."
link: 'https://developers.google.com/style/commas'
scope: sentence
level: warning
nonword: true
# List items may be several words long, not just one. Four guards keep the
# false-positive rate down:
#
# 1. The comma can't be the one closing a fronted subordinate clause
# ('When your alarm rings, you turn it off and tumble out of bed.') --
# that comma separates clauses, not list items. Only the first comma of
# such a sentence is exempt, so 'When it rains, apples, pears or bananas
# get wet.' is still caught.
# 2. The item can't open with a clause-introducer (', which ...',
# ', specifically ...').
# 3. The item can't open with a subject pronoun followed by a verb, which
# marks a compound predicate rather than a list ('..., you walk to the
# fridge and get a snack.'). A pronoun directly followed by 'and'/'or'
# is a real list item, so ', you and me.' still matches.
# 4. Neither item may contain an auxiliary verb, which is another compound
# predicate signal (', it has some downsides and is officially
# discouraged.').
#
# The trailing anchor allows end-of-scope so list fragments ('Apples, pears
# or bananas') are still caught.
tokens:
- '(?:[^,]+,){1,}\s\w+\s(?:and|or)'
- '(?<!^(?i:when|whenever|while|if|unless|until|although|though|because|since|after|before|once|whereas|whether|as)\b[^,]{0,80}),\s(?!(?:which|who|whom|whose|that|where|when|while|because|since|although|though|if|unless|so|but|and|or|however|therefore|thus|specifically|especially|namely|then|take|see|note|consider|make|use|either|neither)\b)(?!(?i:i|you|we|they|he|she|it)\s+(?!(?:and|or)\b))(?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+ (?:and|or) (?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+(?:[.?!]|$)'
+9 -1
View File
@@ -3,5 +3,13 @@ message: "Use parentheses judiciously."
link: 'https://developers.google.com/style/parentheses'
nonword: true
level: suggestion
# `[^)]` rather than `.+`: a greedy match ran from the first '(' on a line to
# the last ')', so 'Text (one) and more (two).' produced a single alert
# covering everything between them. See issue #30.
# A bare 3-5 letter acronym is skipped: Acronyms.yml requires acronyms to be
# defined as 'Spelled Out Term (ACRONYM)', so flagging those parentheses would
# put the two rules in direct conflict. The acronym has to be the whole
# parenthetical — '(NASA rocket program)' is an ordinary aside and still
# flags. Length matches the {3,5} in Acronyms.yml. See PR #59.
tokens:
- '\(.+\)'
- '\((?![A-Z]{3,5}\))[^)]+\)'
+13
View File
@@ -0,0 +1,13 @@
extends: existence
message: "Avoid time-based words like '%s' in product documentation."
link: https://developers.google.com/style/timeless-documentation
level: suggestion
ignorecase: true
# The guide also names 'now' and 'new', but both have common senses that aren't
# time-anchored ('create a new project'): adding them took a 950-file corpus of
# technical documentation from 14 hits to 117. 'recently' is left out too — every
# hit in that corpus was the UI idiom 'recently used'.
tokens:
- currently
- latest
- soon
+4 -2
View File
@@ -4,5 +4,7 @@ link: "https://developers.google.com/style/units-of-measure"
nonword: true
level: error
tokens:
- \b\d+(?:B|kB|MB|GB|TB)
- \b\d+(?:ns|ms|s|min|h|d)
- '\b\d+(?:B|kB|MB|GB|TB)\b'
- '\b\d+(?:ns|ms|min|h|d)\b'
# Seconds are split out so a decade ('1990s') isn't read as a unit.
- '\b\d+s\b(?<!\b(?:19|20)\d\ds\b)'
+3 -54
View File
@@ -2,79 +2,28 @@ extends: substitution
message: "Use '%s' instead of '%s'."
link: "https://developers.google.com/style/word-list"
level: warning
# Case matters here: each key's own capitalization is what's being corrected,
# so ignorecase would make these match their own replacements. The rest of the
# word list lives in WordListCase.yml.
ignorecase: false
action:
name: replace
swap:
"(?:API Console|dev|developer) key": API key
"(?:cell ?phone|smart ?phone)": phone|mobile phone
"(?:dev|developer|APIs) console": API console
"(?:e-mail|Email|E-mail)": email
"(?:file ?path|path ?name)": path
"(?:kill|terminate|abort)": stop|exit|cancel|end
"(?:OAuth ?2|Oauth)": OAuth 2.0
"(?:ok|Okay)": OK|okay
"(?:WiFi|wifi)": Wi-Fi
'[\.]+apk': APK
'3\-D': 3D
'Google (?:I\-O|IO)': Google I/O
"tap (?:&|and) hold": touch & hold
"un(?:check|select)": clear
above: preceding
account name: username
action bar: app bar
admin: administrator
Ajax: AJAX
a\.k\.a|aka: or|also known as
Android device: Android-powered device
android: Android
API explorer: APIs Explorer
application: app
approx\.: approximately
authN: authentication
authZ: authorization
autoupdate: automatically update
cellular data: mobile data
cellular network: mobile network
chapter: documents|pages|sections
check box: checkbox
CLI: command-line tool
click on: click|click in
Cloud: Google Cloud Platform|GCP
Container Engine: Kubernetes Engine
content type: media type
curated roles: predefined roles
data are: data is
Developers Console: Google API Console|API Console
disabled?: turn off|off
ephemeral IP address: ephemeral external IP address
fewer data: less data
file name: filename
firewalls: firewall rules
functionality: capability|feature
Google account: Google Account
Google accounts: Google Accounts
Googling: search with Google
grayed-out: unavailable
HTTPs: HTTPS
in order to: to
ingest: import|load
k8s: Kubernetes
long press: touch & hold
network IP address: internal IP address
omnibox: address bar
open-source: open source
overview screen: recents screen
regex: regular expression
SHA1: SHA-1|HAS-SHA1
sign into: sign in to
sign-?on: single sign-on
static IP address: static external IP address
stylesheet: style sheet
synch: sync
tablename: table name
tablet: device
touch: tap
url: URL
vs\.: versus
World Wide Web: web
+68
View File
@@ -0,0 +1,68 @@
extends: substitution
message: "Use '%s' instead of '%s'."
link: "https://developers.google.com/style/word-list"
level: warning
# The case-insensitive half of the word list, so sentence-initial use is caught
# ('Touch the screen', not only 'touch the screen'). Entries that must stay
# case-sensitive are in WordList.yml.
ignorecase: true
action:
name: replace
swap:
"(?:API Console|dev|developer) key": API key
"(?:cell ?phone|smart ?phone)": phone|mobile phone
"(?:dev|developer|APIs) console": API console
"(?:e-mail|Email|E-mail)": email
"(?:file ?path|path ?name)": path
"(?:kill|terminate|abort)": stop|exit|cancel|end
# Longest form first: with the shortest alternative leading, 'OAuth 2' matched
# only 'OAuth', so applying the suggestion produced 'OAuth 2.0 2'. The rule is
# already case-insensitive, so the inline (?i) is redundant. See issue #41.
'\bOauth2\.0\b|\bOAuth ?2\b(?!\.0)|\bOauth\b(?! ?2)': OAuth 2.0
"(?:ok|Okay)": OK|okay
"(?:WiFi|wifi)": Wi-Fi
'[\.]+apk': APK
'3\-D': 3D
'Google (?:I\-O|IO)': Google I/O
"tap (?:&|and) hold": touch & hold
"un(?:check|select)": clear
above: preceding
account name: username
action bar: app bar
admin: administrator
a\.k\.a|aka: or|also known as
application: app
approx\.: approximately
autoupdate: automatically update
cellular data: mobile data
cellular network: mobile network
chapter: documents|pages|sections
check box: checkbox
click on: click|click in
content type: media type
curated roles: predefined roles
data are: data is
disabled?: turn off|off
ephemeral IP address: ephemeral external IP address
fewer data: less data
file name: filename
firewalls: firewall rules
functionality: capability|feature
grayed-out: unavailable
in order to: to
ingest: import|load
long press: touch & hold
network IP address: internal IP address
omnibox: address bar
open-source: open source
overview screen: recents screen
regex: regular expression
sign into: sign in to
'(?<!single )sign-?on': single sign-on
static IP address: static external IP address
stylesheet: style sheet
synch: sync
tablename: table name
tablet: device
'touch(?! ?(?:&|and) hold)': tap
vs\.: versus
+30
View File
@@ -2,6 +2,36 @@
All notable changes to this project will be documented in this file.
## [0.18.7] - 2026-08-06
### Bug Fixes
- Move StartLimit to [Unit] and make prune timer reload conditional
## [0.18.6] - 2026-08-05
### Bug Fixes
- Pre-configure daemon.json before rootless setuptool + add DBUS_SESSION_BUS_ADDRESS
## [0.18.5] - 2026-08-05
### Bug Fixes
- Pin Docker 28.x + disable containerd snapshotter + tune prune/disk
## [0.18.4] - 2026-08-05
### Bug Fixes
- Harden rootless Docker daemon resilience on CI runners
## [0.18.3] - 2026-08-04
### Bug Fixes
- Switch default network driver to slirp4netns (pasta TCP RST bug)
## [0.18.2] - 2026-07-16
### Bug Fixes
+1 -1
View File
@@ -88,7 +88,7 @@ setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
if [ -n "$$_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \
if [ -n "$$_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; git config --global url."https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/".insteadOf "https://git.oblachno.oblachno.fyi/"; fi; \
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
+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/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/python.svg)](https://www.python.org/downloads/)
## Why GRM?
+37 -6
View File
@@ -18,18 +18,40 @@ gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
# Prune configuration
gitea_runner_prune_until: "24h"
gitea_runner_prune_schedule: "daily"
# Every 6 hours — daily is insufficient for CI runners that build dozens
# of images per day. Accumulation between daily runs can trigger Docker
# daemon instability (containerd snapshotter GC holds locks, blocking
# container operations).
gitea_runner_prune_schedule: "*-*-* 00/6:00:00"
gitea_runner_prune_label: "gitea-runner=true"
# Service configuration
gitea_runner_service_restart_sec: "5"
# Health check configuration
gitea_runner_healthcheck_interval: "5min"
# 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.
gitea_runner_healthcheck_interval: "2min"
gitea_runner_healthcheck_boot_delay: "2min"
gitea_runner_healthcheck_disk_threshold: 85
gitea_runner_healthcheck_disk_threshold: 75
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
# Docker daemon resilience settings (applied to daemon.json).
# live-restore: containers survive daemon restarts — prevents stuck container
# states when the healthcheck restarts a hung daemon.
# shutdown-timeout: grace period (seconds) for containers to stop on daemon
# shutdown/restart. Default 15s is too short for DinD containers with nested
# processes (molecule tests). 30s gives SIGTERM time to propagate.
# max-concurrent-downloads/uploads: limits parallel transfers to reduce daemon
# memory pressure when multiple CI jobs pull images simultaneously.
# default-ulimits: prevents FD exhaustion in container processes.
gitea_runner_docker_live_restore: true
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
# Admin token for runner deregistration via Gitea API.
# If not set, falls back to registration_token (which likely lacks admin scope).
# Set this to a token with admin scope to enable automatic runner cleanup on removal.
@@ -64,9 +86,18 @@ gitea_runner_rootless_scripts_ref: "v28.5.1"
# that dockerd-rootless-setuptool.sh (which derives BIN from its own dirname) finds them co-located.
gitea_runner_rootless_scripts_install_dir: "/usr/bin"
# Rootless Docker network driver: "pasta" (IPv6 support) or "slirp4netns" (IPv4 only)
# pasta has proper outgoing IPv6 support; slirp4netns does not (known limitation).
gitea_runner_docker_rootless_net_driver: "pasta"
# Rootless Docker network driver: "slirp4netns" (default) or "pasta" (IPv6 support)
# slirp4netns is the default because pasta has a TCP proxy bug that sends RST
# packets with wrong sequence numbers, breaking TCP connections from Docker
# containers to external hosts. slirp4netns doesn't have IPv6 support.
# See: https://bugs.passt.top/show_bug.cgi?id=52
gitea_runner_docker_rootless_net_driver: "slirp4netns"
# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet)
gitea_runner_docker_ipv6_cidr: "fd00:dead:beef::/48"
# Pre-pull Docker images that CI runners need (avoids pulling on every CI run).
# The runner container image (ci-full) is large (~3.3GB) and the healthcheck's
# disk-space prune only removes dangling images, so pre-pulled tagged images persist.
# Set to [] to skip pre-pulling. Images are pulled as the runner user via rootless Docker.
gitea_runner_pre_pull_images: []
@@ -49,6 +49,9 @@
- "'Type=oneshot' in prune_service.content | b64decode"
- "'docker system prune' in prune_service.content | b64decode"
- "'docker volume prune' in prune_service.content | b64decode"
- "'docker container prune' in prune_service.content | b64decode"
- "'docker network prune' in prune_service.content | b64decode"
- "'docker builder prune' in prune_service.content | b64decode"
fail_msg: "Prune service template is missing expected directives"
- name: Read rendered prune timer template
@@ -99,8 +102,26 @@
ansible.builtin.assert:
that:
- "'docker info' in healthcheck_script.content | b64decode"
- "'timeout 10 docker info' in healthcheck_script.content | b64decode"
- "'systemctl --user restart docker.service' in healthcheck_script.content | b64decode"
- "'systemctl --user restart gitea-runner.service' in healthcheck_script.content | b64decode"
- "'docker system prune' in healthcheck_script.content | b64decode"
- "'docker container prune' in healthcheck_script.content | b64decode"
- "'docker network prune' in healthcheck_script.content | b64decode"
- "'status=removing' in healthcheck_script.content | b64decode"
- "'status=stopping' in healthcheck_script.content | b64decode"
- "'docker rm -f' in healthcheck_script.content | b64decode"
- "gitea_runner_healthcheck_disk_threshold | string in healthcheck_script.content | b64decode"
fail_msg: "Healthcheck script template is missing expected content"
- name: Assert healthcheck script does NOT use aggressive prune (-af)
ansible.builtin.assert:
that:
- "'prune -af' not in healthcheck_script.content | b64decode"
- "'image prune -af' not in healthcheck_script.content | b64decode"
- "'system prune -af' not in healthcheck_script.content | b64decode"
- "'volume prune -af' not in healthcheck_script.content | b64decode"
fail_msg: >-
Healthcheck script uses 'prune -af' which removes ALL images
(including tagged runner images like ci-full). Use 'prune -f'
(dangling only) to preserve tagged images.
@@ -29,6 +29,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
when:
- gitea_runner_file_stat.stat.exists | default(false) | bool
@@ -29,6 +29,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
@@ -40,6 +41,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
@@ -18,14 +18,18 @@
else {} }}
when: gitea_runner_file_stat.stat.exists | default(false) | bool
- name: Verify runner user service active
- name: Wait for runner user service to be active
ansible.builtin.command: systemctl --user is-active gitea-runner
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
register: gitea_runner_service_check
changed_when: false
retries: 10
delay: 2
until: gitea_runner_service_check.stdout | default('') | trim == 'active'
when:
- gitea_runner_systemd_available.stat.exists
- gitea_runner_docker_rootless_setup
@@ -17,6 +17,9 @@
- name: Include healthcheck setup
ansible.builtin.include_tasks: healthcheck.yml
- name: Include pre-pull images
ansible.builtin.include_tasks: pre_pull_images.yml
- name: Include integration test
ansible.builtin.include_tasks: integration_test.yml
when: not gitea_runner_skip_registration
@@ -0,0 +1,28 @@
---
# Pre-pull Docker images that CI runners need to avoid pulling them on
# every CI run. The runner container image (ci-full) is large (~3.3GB)
# and pulling it on every run causes timeouts and disk pressure.
#
# The healthcheck script's disk-space prune only removes dangling images
# (not tagged ones), so pre-pulled images persist between CI runs.
#
# Set gitea_runner_pre_pull_images to a list of image refs to pull, or
# empty list to skip pre-pulling.
- name: Pre-pull Docker images for CI runner
ansible.builtin.command: "docker pull {{ item }}"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
register: gitea_runner_pre_pull_result
changed_when: "'Status: Downloaded' in gitea_runner_pre_pull_result.stdout or 'Status: Downloaded' in gitea_runner_pre_pull_result.stderr"
retries: 3
delay: 5
until: gitea_runner_pre_pull_result is success
loop: "{{ gitea_runner_pre_pull_images }}"
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_pre_pull_images | length > 0
@@ -6,6 +6,7 @@
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
mode: "0644"
register: gitea_runner_prune_service
- name: Create docker-prune user timer file
ansible.builtin.template:
@@ -14,6 +15,7 @@
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
mode: "0644"
register: gitea_runner_prune_timer
- name: Reload systemd user daemon for prune timer
ansible.builtin.command: systemctl --user daemon-reload
@@ -21,10 +23,12 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
- gitea_runner_docker_rootless_setup
- gitea_runner_prune_service is changed or gitea_runner_prune_timer is changed
- name: Enable and start docker-prune user timer
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
@@ -32,6 +36,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
@@ -26,6 +26,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
when: not gitea_runner_registered.stat.exists
register: gitea_runner_register_output
@@ -31,6 +31,11 @@
- ansible_facts['os_family'] == 'Debian'
- gitea_runner_docker_apt_repo is changed
# Install Docker packages from the upstream Docker APT repository.
# We do NOT pin to 28.x because recent Ubuntu releases (e.g. 26.04/plucky)
# may not have 28.x packages in the Docker repo, and Docker 29 is safe
# for rootless mode when the daemon.json disables the containerd snapshotter
# and sets a conservative default nofile ulimit (see daemon.json tasks below).
- name: Install rootless Docker dependencies (Debian/Ubuntu)
ansible.builtin.apt:
name:
@@ -45,6 +50,7 @@
- docker-compose-plugin
- rsync
state: present
register: gitea_runner_docker_install
when: ansible_facts['os_family'] == 'Debian'
- name: Update pacman cache (Arch Linux)
@@ -133,8 +139,57 @@
content: |
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
{% endif %}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
# Write daemon.json BEFORE the setuptool starts dockerd, so Docker 29
# starts with containerd snapshotter disabled from the very first boot.
# Without this, Docker 29 uses containerd snapshots by default, which
# causes instability in rootless mode.
- name: Ensure Docker config directory exists (pre-setup)
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.config/docker"
state: directory
mode: "0755"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Pre-configure rootless Docker daemon.json (disable containerd snapshotter)
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
content: |
{
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
"default-ulimits": {
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
},
"features": {
"containerd-snapshotter": false
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
"dns": ["10.0.2.3", "8.8.8.8"]
{% else %}
"ipv6": false,
"dns": ["8.8.8.8", "1.1.1.1"]
{% endif %}
}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
@@ -150,6 +205,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
DOCKERD_ROOTLESS_ROOTLESSKIT_NET: "{{ gitea_runner_docker_rootless_net_driver }}"
when:
- gitea_runner_docker_rootless_setup
@@ -161,6 +217,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when: gitea_runner_docker_rootless_setup
@@ -170,6 +227,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when: gitea_runner_docker_rootless_setup
@@ -182,14 +240,16 @@
group: "{{ gitea_runner_service_user }}"
when: gitea_runner_docker_rootless_setup
- name: Configure rootless Docker to use pasta with IPv6
- name: Configure rootless Docker network driver
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
content: |
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
{% endif %}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
@@ -202,19 +262,36 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_docker_network_override is changed
- name: Configure rootless Docker daemon with IPv6 enabled
- name: Configure rootless Docker daemon
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
content: |
{
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
"default-ulimits": {
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
},
"features": {
"containerd-snapshotter": false
},
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}"
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
"dns": ["10.0.2.3", "8.8.8.8"]
{% else %}
"ipv6": false,
"dns": ["8.8.8.8", "1.1.1.1"]
{% endif %}
}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
@@ -228,6 +305,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
@@ -240,6 +318,7 @@
environment:
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
register: gitea_runner_docker_ready
until: gitea_runner_docker_ready.rc == 0
retries: 10
@@ -6,6 +6,7 @@
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
mode: "0644"
register: gitea_runner_service_file
- name: Reload systemd user daemon
ansible.builtin.command: systemctl --user daemon-reload
@@ -13,10 +14,25 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
- gitea_runner_docker_rootless_setup
- gitea_runner_service_file is changed
- name: Restart gitea-runner if service file changed
ansible.builtin.command: systemctl --user restart gitea-runner
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
- gitea_runner_docker_rootless_setup
- gitea_runner_service_file is changed
- name: Enable and start gitea-runner user service
ansible.builtin.command: systemctl --user enable --now gitea-runner
@@ -24,6 +40,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
changed_when: true
when:
- gitea_runner_systemd_available.stat.exists
@@ -8,6 +8,7 @@
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
when:
- gitea_runner_systemd_available.stat.exists | default(false) | bool
- gitea_runner_docker_rootless_setup
@@ -21,6 +21,7 @@
environment:
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
register: gitea_runner_docker_version_output
changed_when: false
when: gitea_runner_docker_rootless_setup
@@ -7,3 +7,6 @@ Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
ExecStart=/usr/bin/docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
ExecStart=/usr/bin/docker volume prune -f --filter "label={{ gitea_runner_prune_label }}"
ExecStart=/usr/bin/docker container prune -f
ExecStart=/usr/bin/docker network prune -f
ExecStart=/usr/bin/docker builder prune -f
@@ -3,6 +3,8 @@ Description=Gitea Actions Runner (rootless)
After=docker.service
Requires=docker.service
PartOf=docker.service
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
Type=simple
@@ -14,8 +16,6 @@ ExecStop=/bin/kill -TERM $MAINPID
TimeoutStopSec=30
Restart=always
RestartSec={{ gitea_runner_service_restart_sec }}
StartLimitIntervalSec=300
StartLimitBurst=10
[Install]
WantedBy=default.target
@@ -7,18 +7,29 @@ DOCKER_HOST="unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR="/run/user/{{ gitea_runner_uid }}"
export DOCKER_HOST XDG_RUNTIME_DIR
# 1. Check Docker daemon responsiveness
if ! docker info >/dev/null 2>&1; then
echo "ERROR: Docker daemon not responding at ${DOCKER_HOST}"
# 1. Check Docker daemon responsiveness (with timeout — a bare `docker info` can
# hang indefinitely on a stuck daemon, blocking the healthcheck itself).
if ! timeout 10 docker info >/dev/null 2>&1; then
echo "ERROR: Docker daemon not responding at ${DOCKER_HOST} (timed out after 10s)"
systemctl --user restart docker.service
sleep 3
if ! docker info >/dev/null 2>&1; then
if ! timeout 10 docker info >/dev/null 2>&1; then
echo "CRITICAL: Docker daemon still down after restart"
exit 1
fi
echo "RECOVERED: Docker daemon restarted successfully"
fi
# 1b. Clean up stuck containers — containers in "removing" or "stopping" state
# for too long cause "cannot kill container: did not receive an exit event"
# errors in molecule destroy phases. Force-remove them so subsequent CI jobs
# don't inherit the stuck state.
stuck_containers=$(timeout 10 docker ps -a --filter "status=removing" --filter "status=stopping" --format '{% raw %}{{.ID}}{% endraw %}' 2>/dev/null || true)
if [[ -n "$stuck_containers" ]]; then
echo "WARN: Found stuck containers (removing/stopping), force-cleaning"
echo "$stuck_containers" | xargs -r docker rm -f 2>/dev/null || true
fi
# 2. Check gitea-runner service is active
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
if [[ "$runner_state" != "active" ]]; then
@@ -37,10 +48,13 @@ fi
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
echo "WARN: Disk usage at ${disk_pct}%, pruning all runner resources"
docker system prune -af --filter "label={{ gitea_runner_prune_label }}" --filter "until=1h" || true
docker volume prune -af --filter "label={{ gitea_runner_prune_label }}" || true
# Also prune dangling images (no label)
docker image prune -af || true
docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until=1h" || true
docker volume prune -f --filter "label={{ gitea_runner_prune_label }}" || true
# Only prune dangling (untagged) images — keep tagged runner images (ci-full, ci-quality)
docker image prune -f || true
# Clean up stopped containers and dangling networks that accumulate from failed jobs
docker container prune -f || true
docker network prune -f || true
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
echo "INFO: Disk usage after prune: ${disk_pct}%"
fi
+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/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/36201d0d5738f90a181519b2d56262d36fd04005/python.svg)](https://www.python.org/downloads/)
## Overview
+3 -2
View File
@@ -32,10 +32,11 @@ version = {attr = "grm.__version__"}
ci = [
"pytest==9.1.1",
"pytest-cov==7.1.0",
"pytest-xdist==3.8.0",
"build==1.5.1",
"twine==6.2.0",
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.1",
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.8",
]
# Lint and type-checking tools (validate job)
lint = [
@@ -55,7 +56,7 @@ molecule = [
dev = [
"grm[ci,lint,molecule]",
# Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr)
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.1",
"devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.8",
# Non-Python dev dependency: checkmake (Makefile linter)
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
]
+1 -1
View File
@@ -1,3 +1,3 @@
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
__version__ = "0.18.2"
__version__ = "0.18.7"