Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d2eaa3291 | ||
|
|
8176a62885 | ||
|
|
443756a508 | ||
|
|
340222e041 | ||
|
|
179e47bbb2 | ||
|
|
68d16577b3 | ||
|
|
38607d9f29 | ||
|
|
90139b306b | ||
|
|
dd475bec0d | ||
|
|
0f0ada3576 | ||
|
|
185e41c49e | ||
|
|
103741b3ab | ||
|
|
b770d1debf | ||
|
|
2f11489be0 | ||
|
|
8e9e58fedb | ||
|
|
70d985ebaa | ||
|
|
66f071b676 |
@@ -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
|
||||
@@ -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+'
|
||||
|
||||
@@ -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}'
|
||||
|
||||
@@ -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?
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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+(?:[.?!]|$)'
|
||||
|
||||
@@ -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}\))[^)]+\)'
|
||||
|
||||
@@ -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,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)'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why GRM?
|
||||
|
||||
|
||||
@@ -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
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
+3
-2
@@ -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
@@ -1,3 +1,3 @@
|
||||
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
||||
|
||||
__version__ = "0.18.2"
|
||||
__version__ = "0.18.7"
|
||||
|
||||
Reference in New Issue
Block a user