Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30cc68bfe3 | ||
|
|
d6549de2e0 | ||
|
|
dc4bb0936d | ||
|
|
52477e558a | ||
|
|
28214de583 | ||
|
|
7fd023d192 | ||
|
|
2ffedaa793 | ||
|
|
e7b2d4e6af | ||
|
|
bda2af91bc | ||
|
|
c72dc97f63 | ||
|
|
db9fb6f162 | ||
|
|
d102453960 | ||
|
|
42409d9e47 | ||
|
|
91e880f05c | ||
|
|
2d2eaa3291 | ||
|
|
8176a62885 | ||
|
|
443756a508 | ||
|
|
340222e041 | ||
|
|
179e47bbb2 | ||
|
|
68d16577b3 | ||
|
|
38607d9f29 | ||
|
|
90139b306b | ||
|
|
dd475bec0d | ||
|
|
0f0ada3576 | ||
|
|
185e41c49e | ||
|
|
103741b3ab | ||
|
|
b770d1debf | ||
|
|
2f11489be0 |
@@ -1,47 +0,0 @@
|
||||
name: 'Notify on failure'
|
||||
description: 'Create a Gitea issue when a CI workflow fails (calls devx.ci.notify_failure)'
|
||||
|
||||
# Composite action for the common "Notify on failure" step pattern.
|
||||
# Replaces the repeated inline:
|
||||
# - name: Notify on failure
|
||||
# if: failure()
|
||||
# env:
|
||||
# CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
# run: |
|
||||
# . .venv/bin/activate 2>/dev/null || true
|
||||
# export PATH="$HOME/.local/bin:$PATH"
|
||||
# python3 -m devx.ci.notify_failure \
|
||||
# --repo "${{ github.repository }}" \
|
||||
# --run-id "${{ github.run_id }}" \
|
||||
# --workflow "ci/validate" \
|
||||
# --commit "${{ github.sha }}" \
|
||||
# --auto-login
|
||||
#
|
||||
# Gitea 1.27 notes:
|
||||
# - `if: failure()` is evaluated in the calling workflow's context and
|
||||
# propagates correctly to composite action steps.
|
||||
# - `secrets` are not accessible here; the calling workflow's top-level
|
||||
# `env:` CI_GITEA_API_TOKEN is used via `${{ env.* }}`.
|
||||
|
||||
inputs:
|
||||
workflow:
|
||||
description: 'Workflow/job name used in the Gitea issue title (e.g., ci/validate)'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
shell: bash
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ env.CI_GITEA_API_TOKEN }}
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "${{ inputs.workflow }}" \
|
||||
--commit "${{ github.sha }}" \
|
||||
--auto-login
|
||||
@@ -1,89 +0,0 @@
|
||||
name: 'Quality checks'
|
||||
description: 'Run lint, unit tests with coverage, test speed, docs, translations, and security scan'
|
||||
|
||||
# Composite action for the 6-step quality check sequence used by the
|
||||
# devx validate job. Replaces the inline block:
|
||||
# - Lint all
|
||||
# - Unit tests with 100% coverage
|
||||
# - Check unit test speed
|
||||
# - Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||
# - Translation completeness check
|
||||
# - Dependency security scan
|
||||
#
|
||||
# Each step activates the venv defensively (`. .venv/bin/activate 2>/dev/null
|
||||
# || true`) so the action works whether or not the setup step created a
|
||||
# venv at the repo root (pre-built CI images symlink /opt/venv to .venv).
|
||||
#
|
||||
# Gitea 1.27 notes:
|
||||
# - Every `run` step needs explicit `shell:`.
|
||||
# - Inputs are string-typed; numeric thresholds are passed through as
|
||||
# strings to `devx.tools.check_test_speed`.
|
||||
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package name for doc version checks (e.g., devx, grm). Empty = no DEVX_DOC_VERSIONS_PKG override.'
|
||||
required: false
|
||||
default: ''
|
||||
test-speed-max:
|
||||
description: 'Max total test seconds (passed to check_test_speed --max-seconds)'
|
||||
required: false
|
||||
default: '15'
|
||||
test-speed-max-single:
|
||||
description: 'Max single test seconds (passed to check_test_speed --max-single-seconds)'
|
||||
required: false
|
||||
default: '0.5'
|
||||
translations-file:
|
||||
description: 'Path to translations.json (empty = default location src/devx/translations.json)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Lint all
|
||||
shell: bash
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
make lint-all
|
||||
- name: Unit tests with 100% coverage
|
||||
shell: bash
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
make pytest-cov
|
||||
- name: Check unit test speed
|
||||
shell: bash
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.tools.check_test_speed \
|
||||
--max-seconds "${{ inputs.test-speed-max }}" \
|
||||
--max-single-seconds "${{ inputs.test-speed-max-single }}"
|
||||
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||
shell: bash
|
||||
env:
|
||||
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||
DEVX_VALE_LEVEL: warning
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
if [ -n "${{ inputs.package }}" ]; then
|
||||
export DEVX_DOC_VERSIONS_PKG="${{ inputs.package }}"
|
||||
fi
|
||||
make devx-docs-check
|
||||
- name: Translation completeness check
|
||||
shell: bash
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
if [ -n "${{ inputs.translations-file }}" ]; then
|
||||
python3 -m devx.ci.check_translations --translations "${{ inputs.translations-file }}"
|
||||
else
|
||||
python3 -m devx.ci.check_translations
|
||||
fi
|
||||
- name: Dependency security scan
|
||||
shell: bash
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
# Install pip in venv if missing (needed by pip-audit)
|
||||
.venv/bin/python -m ensurepip 2>/dev/null || true
|
||||
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
||||
pip-audit --desc --skip-editable 2>&1 || true
|
||||
@@ -1,37 +0,0 @@
|
||||
name: 'Set up environment'
|
||||
description: 'Set up CI environment with venv and PATH (calls make setup-image)'
|
||||
|
||||
# Composite action for the common "Set up environment" step pattern.
|
||||
# Replaces the repeated inline:
|
||||
# - name: Set up environment
|
||||
# env:
|
||||
# CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
# run: make setup-image
|
||||
#
|
||||
# Gitea 1.27 notes:
|
||||
# - Every `run` step needs explicit `shell:`.
|
||||
# - Composite actions cannot access `secrets` directly; they read from
|
||||
# the `env:` context which the calling workflow must populate.
|
||||
# - The calling workflow's top-level `env:` block (CI_GITEA_API_TOKEN,
|
||||
# CI_GITEA_USERNAME) is visible here via `${{ env.* }}`.
|
||||
|
||||
inputs:
|
||||
extras:
|
||||
description: 'Extra pip install groups passed to make setup-image (e.g., ci,lint,release)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Set up environment
|
||||
shell: bash
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ env.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ env.CI_GITEA_USERNAME }}
|
||||
run: |
|
||||
if [ -n "${{ inputs.extras }}" ]; then
|
||||
make setup-image EXTRAS="${{ inputs.extras }}"
|
||||
else
|
||||
make setup-image
|
||||
fi
|
||||
+59
-29
@@ -18,11 +18,7 @@ jobs:
|
||||
# Saves ~5x checkout+setup overhead vs 6 separate jobs.
|
||||
validate:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
credentials:
|
||||
username: ${{ vars.CI_GITEA_USERNAME }}
|
||||
password: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
@@ -36,14 +32,45 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.gitea/actions/setup-env
|
||||
with:
|
||||
extras: "ci,lint"
|
||||
- uses: ./.gitea/actions/quality-checks
|
||||
with:
|
||||
package: grm
|
||||
test-speed-max: "4"
|
||||
translations-file: src/grm/translations.json
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
# --- quality steps ---
|
||||
- name: Lint all
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
make lint-all
|
||||
- name: Unit tests with 100% coverage
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
make pytest-cov
|
||||
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||
env:
|
||||
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||
DEVX_DOC_VERSIONS_PKG: grm
|
||||
DEVX_VALE_LEVEL: warning
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
make devx-docs-check
|
||||
- name: Translation completeness check
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.check_translations --translations src/grm/translations.json
|
||||
- name: Check unit test speed
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
- name: Dependency security scan
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
# Install pip in venv if missing (needed by pip-audit)
|
||||
.venv/bin/python -m ensurepip 2>/dev/null || true
|
||||
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
||||
pip-audit --desc --skip-editable 2>&1 || true
|
||||
- name: Workflow dry-run validation
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -113,19 +140,24 @@ jobs:
|
||||
--owner "${{ github.repository_owner }}" \
|
||||
--repo "${{ github.event.repository.name }}" \
|
||||
--github-output
|
||||
- uses: ./.gitea/actions/notify-failure
|
||||
with:
|
||||
workflow: "ci/validate"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure --auto-login \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "ci/validate" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
molecule-tests:
|
||||
needs: [validate]
|
||||
if: needs.validate.outputs.ansible-changed == 'true'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
credentials:
|
||||
username: ${{ vars.CI_GITEA_USERNAME }}
|
||||
password: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -234,11 +266,7 @@ jobs:
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.validate.result == 'success'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
credentials:
|
||||
username: ${{ vars.CI_GITEA_USERNAME }}
|
||||
password: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
@@ -248,9 +276,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
- uses: ./.gitea/actions/setup-env
|
||||
with:
|
||||
extras: "ci"
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Post approval review
|
||||
env:
|
||||
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
||||
|
||||
@@ -36,11 +36,7 @@ env:
|
||||
jobs:
|
||||
detect-and-configure:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
credentials:
|
||||
username: ${{ vars.CI_GITEA_USERNAME }}
|
||||
password: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
@@ -53,9 +49,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: ./.gitea/actions/setup-env
|
||||
with:
|
||||
extras: "ci"
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Ensure branch protection and labels
|
||||
env:
|
||||
DEVX_REPO_NAME: grm
|
||||
@@ -89,19 +87,24 @@ jobs:
|
||||
--base "HEAD~1" \
|
||||
--head "HEAD" \
|
||||
--github-output
|
||||
- uses: ./.gitea/actions/notify-failure
|
||||
with:
|
||||
workflow: "post-merge/detect-and-configure"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure --auto-login \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/detect-and-configure" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
release-and-maintain:
|
||||
needs: [detect-and-configure]
|
||||
if: always() && needs.detect-and-configure.result == 'success'
|
||||
runs-on: docker
|
||||
container:
|
||||
image: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
credentials:
|
||||
username: ${{ vars.CI_GITEA_USERNAME }}
|
||||
password: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
tag: ${{ steps.release-tag.outputs.tag }}
|
||||
@@ -114,22 +117,20 @@ jobs:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
- uses: ./.gitea/actions/setup-env
|
||||
with:
|
||||
extras: "ci,lint"
|
||||
- name: Configure git
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "grm-ci-bot"
|
||||
git config user.email "grm-ci-bot@oblachno.fyi"
|
||||
git remote set-url origin "https://grm-ci-bot:${CI_GITEA_API_TOKEN}@git.oblachno.oblachno.fyi/oblachno-oss/grm.git"
|
||||
# --- release + publish (only if not a release commit) ---
|
||||
- name: Run release
|
||||
id: release-tag
|
||||
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
@@ -176,6 +177,15 @@ jobs:
|
||||
git fetch origin master
|
||||
git reset --hard origin/master
|
||||
python3 -m devx.ci.push_badges
|
||||
- uses: ./.gitea/actions/notify-failure
|
||||
with:
|
||||
workflow: "post-merge/release-and-maintain"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure --auto-login \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/release-and-maintain" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
+3
-21
@@ -2,29 +2,11 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
## [0.21.0] - 2026-08-09
|
||||
|
||||
### CI
|
||||
### Features
|
||||
|
||||
- Convert `ci.yml` and `post-merge.yml` to use composite actions
|
||||
(`setup-env`, `quality-checks`, `notify-failure`) copied from devx,
|
||||
reducing workflow duplication
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix `register.yml` premature service start: removed step that ran
|
||||
`systemctl --user start gitea-runner` before the systemd unit file was
|
||||
created by `service.yml` (included after `register.yml`). First-time
|
||||
installs were failing with "Unit gitea-runner.service not found".
|
||||
- Fix `ci.yml` auto-merge IndentationError: replaced broken inline Python
|
||||
polling script with `devx.ci.wait_for_checks` (the inline Python had
|
||||
YAML run-block indentation leaking into `python3 -c` string).
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Bump devx from v0.50.0 to v0.50.5 (adds `wait_for_checks` tool,
|
||||
consolidated Ansible checks, tenacity retry in `install_tools`,
|
||||
increased download retry attempts/backoff)
|
||||
- *(healthcheck)* Add two-tier disk prune with critical threshold
|
||||
|
||||
## [0.20.0] - 2026-08-09
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -36,10 +36,9 @@ gitea_runner_prune_label: "gitea-runner=true"
|
||||
gitea_runner_service_restart_sec: "5"
|
||||
|
||||
# Health check configuration
|
||||
# 2min interval — catches hung daemons before multiple CI jobs fail between
|
||||
# checks. The 1min interval caused excessive pruning which removed cached
|
||||
# images, forcing all 6 parallel slots to re-pull simultaneously and
|
||||
# actually increasing disk pressure.
|
||||
# 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: 70
|
||||
@@ -48,10 +47,6 @@ gitea_runner_healthcheck_disk_threshold: 70
|
||||
# This removes all stopped containers and unused images regardless of age.
|
||||
# At 75%+, molecule containers fail with "container is not running" because
|
||||
# overlay2 runs out of space under parallel DinD load.
|
||||
# IMPORTANT: keep at 75 (not lower) — the host disk normally sits at ~74%.
|
||||
# Lowering to 70 triggers full prune every cycle, wiping cached images and
|
||||
# forcing all parallel slots to re-pull simultaneously, which increases
|
||||
# disk pressure rather than reducing it.
|
||||
gitea_runner_healthcheck_disk_critical: 75
|
||||
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
|
||||
|
||||
@@ -81,10 +76,6 @@ gitea_runner_docker_shutdown_timeout: 30
|
||||
gitea_runner_docker_max_concurrent_downloads: 3
|
||||
gitea_runner_docker_max_concurrent_uploads: 3
|
||||
gitea_runner_docker_default_nofile: 65536
|
||||
# Log file size limits — under parallel DinD load, container logs can fill
|
||||
# disk and cause the daemon to become unresponsive. Limit log size per container.
|
||||
gitea_runner_docker_max_log_size: "10m"
|
||||
gitea_runner_docker_max_log_files: 3
|
||||
|
||||
# Admin token for runner deregistration via Gitea API.
|
||||
# If not set, falls back to registration_token (which likely lacks admin scope).
|
||||
@@ -99,6 +90,22 @@ gitea_runner_remove_user: true
|
||||
gitea_runner_log_level: "info"
|
||||
gitea_runner_container_label: "gitea-runner=true"
|
||||
gitea_runner_file: ".runner"
|
||||
# force_pull: when false (default), the runner reuses locally cached images
|
||||
# instead of pulling on every job. Pre-cached images (via the pre-cache timer
|
||||
# or pre_pull_images task) eliminate registry thundering-herd when all runners
|
||||
# start jobs simultaneously.
|
||||
gitea_runner_force_pull: false
|
||||
# Container options passed to `docker run` for CI job containers.
|
||||
# Mounts the host rootless Docker socket as /run/host-docker.sock so
|
||||
# start_docker.py inside the container can detect and use the host daemon
|
||||
# (full disk, no nested DinD) instead of starting an inner dockerd.
|
||||
gitea_runner_container_options: "-v /run/user/{{ gitea_runner_uid }}/docker.sock:/run/host-docker.sock"
|
||||
# Volumes allowed in CI job containers (validated by the runner against
|
||||
# container.options and job-level volumes). Must include the host Docker
|
||||
# socket mount target.
|
||||
gitea_runner_valid_volumes:
|
||||
- "/run/host-docker.sock"
|
||||
- "/run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||
|
||||
# Containerd version pinning — Docker 28.x vendors containerd v2.1.x internally.
|
||||
# containerd.io >= 2.3 ships a shim that returns a protobuf BootstrapResult which
|
||||
@@ -144,3 +151,11 @@ gitea_runner_docker_ipv6_cidr: "fd00:dead:beef::/48"
|
||||
# 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: []
|
||||
|
||||
# Pre-cache timer: periodically pulls the runner container image so it stays
|
||||
# fresh in the local Docker cache. This prevents thundering-herd registry
|
||||
# timeouts when all runners start CI jobs simultaneously with empty caches.
|
||||
# Runs every 6 hours (aligned with prune schedule). Set to empty string to
|
||||
# disable the timer.
|
||||
gitea_runner_pre_cache_schedule: "*-*-* 00/6:30:00"
|
||||
gitea_runner_pre_cache_images: "{{ gitea_runner_pre_pull_images }}"
|
||||
|
||||
@@ -10,7 +10,3 @@
|
||||
- ansible_facts is defined
|
||||
- ansible_facts['service_mgr'] | default('') == 'systemd'
|
||||
- gitea_runner_docker_rootless_setup
|
||||
|
||||
- name: Reload systemd user daemon
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
@@ -112,4 +112,5 @@
|
||||
- "'status=removing' in healthcheck_script.content | b64decode"
|
||||
- "'status=stopping' in healthcheck_script.content | b64decode"
|
||||
- "gitea_runner_healthcheck_disk_threshold | string in healthcheck_script.content | b64decode"
|
||||
- "gitea_runner_healthcheck_disk_critical | string in healthcheck_script.content | b64decode"
|
||||
fail_msg: "Healthcheck script template is missing expected content"
|
||||
|
||||
@@ -40,22 +40,24 @@
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload systemd user daemon for healthcheck timer
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user daemon-reload
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
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
|
||||
|
||||
- name: Enable and start healthcheck user timer
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user enable --now runner-healthcheck.timer
|
||||
ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer
|
||||
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
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
- name: Include validation
|
||||
ansible.builtin.include_tasks: validate.yml
|
||||
|
||||
- name: Include service setup
|
||||
ansible.builtin.include_tasks: service.yml
|
||||
|
||||
- name: Include registration
|
||||
ansible.builtin.include_tasks: register.yml
|
||||
when: not gitea_runner_skip_registration
|
||||
|
||||
- name: Include service setup
|
||||
ansible.builtin.include_tasks: service.yml
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
- name: Include pre-pull images
|
||||
ansible.builtin.include_tasks: pre_pull_images.yml
|
||||
|
||||
- name: Include pre-cache timer
|
||||
ansible.builtin.include_tasks: pre_cache.yml
|
||||
|
||||
- name: Include integration test
|
||||
ansible.builtin.include_tasks: integration_test.yml
|
||||
when: not gitea_runner_skip_registration
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
# Periodic timer that pre-pulls CI runner images into the local Docker cache.
|
||||
# Prevents thundering-herd registry timeouts when all runners start jobs
|
||||
# simultaneously with empty/stale caches. Runs every 6 hours (configurable).
|
||||
# The prune timer removes dangling images but NOT tagged ones, so pre-pulled
|
||||
# images persist between runs.
|
||||
|
||||
- name: Create docker-pull-images user service file
|
||||
ansible.builtin.template:
|
||||
src: docker-pull-images.service.j2
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-pull-images.service"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
register: gitea_runner_pre_cache_service
|
||||
|
||||
- name: Create docker-pull-images user timer file
|
||||
ansible.builtin.template:
|
||||
src: docker-pull-images.timer.j2
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-pull-images.timer"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
register: gitea_runner_pre_cache_timer
|
||||
|
||||
- name: Reload systemd user daemon for pre-cache timer
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
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_pre_cache_service is changed or gitea_runner_pre_cache_timer is changed
|
||||
- gitea_runner_pre_cache_schedule | length > 0
|
||||
- gitea_runner_pre_cache_images | length > 0
|
||||
|
||||
- name: Enable and start docker-pull-images user timer
|
||||
ansible.builtin.command: systemctl --user enable --now docker-pull-images.timer
|
||||
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_pre_cache_schedule | length > 0
|
||||
- gitea_runner_pre_cache_images | length > 0
|
||||
|
||||
- name: Disable and stop docker-pull-images timer (no images or schedule)
|
||||
ansible.builtin.command: systemctl --user disable --now docker-pull-images.timer
|
||||
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
|
||||
failed_when: false
|
||||
when:
|
||||
- gitea_runner_systemd_available.stat.exists
|
||||
- gitea_runner_docker_rootless_setup
|
||||
- gitea_runner_pre_cache_schedule | length == 0 or gitea_runner_pre_cache_images | length == 0
|
||||
@@ -18,11 +18,12 @@
|
||||
register: gitea_runner_prune_timer
|
||||
|
||||
- name: Reload systemd user daemon for prune timer
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user daemon-reload
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
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
|
||||
@@ -30,11 +31,12 @@
|
||||
- gitea_runner_prune_service is changed or gitea_runner_prune_timer is changed
|
||||
|
||||
- name: Enable and start docker-prune user timer
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user enable --now docker-prune.timer
|
||||
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
|
||||
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
|
||||
|
||||
@@ -44,12 +44,13 @@
|
||||
('already exists' not in gitea_runner_register_output.stdout | default(''))
|
||||
timeout: 60
|
||||
|
||||
- name: Restart runner service after (re-)registration
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user restart gitea-runner
|
||||
- name: Ensure runner service is running after registration
|
||||
ansible.builtin.command: systemctl --user start 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
|
||||
|
||||
@@ -143,10 +143,6 @@
|
||||
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
||||
{% endif %}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
mode: "0644"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
@@ -184,10 +180,6 @@
|
||||
"features": {
|
||||
"containerd-snapshotter": false
|
||||
},
|
||||
"log-opts": {
|
||||
"max-size": "{{ gitea_runner_docker_max_log_size }}",
|
||||
"max-file": "{{ gitea_runner_docker_max_log_files }}"
|
||||
},
|
||||
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||
"ipv6": true,
|
||||
"ip6tables": true,
|
||||
@@ -220,20 +212,22 @@
|
||||
- not gitea_runner_rootless_docker_check.stat.exists
|
||||
|
||||
- name: Start rootless Docker daemon (systemd user service)
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user start docker
|
||||
ansible.builtin.command: systemctl --user start docker
|
||||
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 }}/bus"
|
||||
changed_when: true
|
||||
when: gitea_runner_docker_rootless_setup
|
||||
|
||||
- name: Enable rootless Docker daemon (systemd user service)
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user enable docker
|
||||
ansible.builtin.command: systemctl --user enable docker
|
||||
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 }}/bus"
|
||||
changed_when: true
|
||||
when: gitea_runner_docker_rootless_setup
|
||||
|
||||
@@ -256,10 +250,6 @@
|
||||
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
||||
{% endif %}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=10
|
||||
mode: "0644"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
@@ -267,11 +257,12 @@
|
||||
when: gitea_runner_docker_rootless_setup
|
||||
|
||||
- name: Reload systemd user daemon if network config changed
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user daemon-reload
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
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 }}/bus"
|
||||
changed_when: true
|
||||
when:
|
||||
- gitea_runner_docker_rootless_setup
|
||||
@@ -292,10 +283,6 @@
|
||||
"features": {
|
||||
"containerd-snapshotter": false
|
||||
},
|
||||
"log-opts": {
|
||||
"max-size": "{{ gitea_runner_docker_max_log_size }}",
|
||||
"max-file": "{{ gitea_runner_docker_max_log_files }}"
|
||||
},
|
||||
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||
"ipv6": true,
|
||||
"ip6tables": true,
|
||||
@@ -313,11 +300,12 @@
|
||||
when: gitea_runner_docker_rootless_setup
|
||||
|
||||
- name: Restart rootless Docker if config changed
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user restart docker
|
||||
ansible.builtin.command: systemctl --user restart docker
|
||||
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 }}/bus"
|
||||
changed_when: true
|
||||
when:
|
||||
- gitea_runner_docker_rootless_setup
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
register: gitea_runner_service_file
|
||||
|
||||
- name: Reload systemd user daemon
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user daemon-reload
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
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
|
||||
@@ -21,11 +22,12 @@
|
||||
- gitea_runner_service_file is changed
|
||||
|
||||
- name: Restart gitea-runner if service file changed
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user restart gitea-runner
|
||||
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
|
||||
@@ -33,11 +35,12 @@
|
||||
- gitea_runner_service_file is changed
|
||||
|
||||
- name: Enable and start gitea-runner user service
|
||||
ansible.builtin.command: >
|
||||
runuser -u {{ gitea_runner_service_user }} --
|
||||
env XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus
|
||||
systemctl --user enable --now gitea-runner
|
||||
ansible.builtin.command: systemctl --user enable --now 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
|
||||
|
||||
@@ -69,31 +69,3 @@
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Disable systemd-oomd memory pressure kill for runner user
|
||||
when: gitea_runner_systemd_available.stat.exists
|
||||
block:
|
||||
- name: Ensure user service override directory exists
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/user@{{ gitea_runner_uid }}.service.d"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Disable ManagedOOMMemoryPressure for runner user
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
[Service]
|
||||
ManagedOOMMemoryPressure=auto
|
||||
ManagedOOMMemoryPressureLimit=100%
|
||||
OOMScoreAdjust=-500
|
||||
dest: "/etc/systemd/system/user@{{ gitea_runner_uid }}.service.d/oomd-override.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify: Reload systemd user daemon
|
||||
|
||||
- name: Reload systemd daemon for oomd override
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
@@ -11,7 +11,13 @@ Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
# consuming disk and memory. We stop+rm everything first, then prune the rest.
|
||||
# Exclude CI job containers (name starts with GITEA-ACTIONS-TASK) — removing
|
||||
# them kills the active CI job and causes "RWLayer is unexpectedly nil" errors.
|
||||
ExecStart=/bin/sh -c 'docker ps -a --format "{% raw %}{{.ID}} {{.Names}}{% endraw %}" 2>/dev/null | grep -v "GITEA-ACTIONS-TASK" | awk "{print $1}" | xargs -r docker rm -f 2>/dev/null || true'
|
||||
# Only remove containers older than 1 hour (grep for "hour/day/week/month/year
|
||||
# ago" in RunningFor) to avoid killing molecule test containers that CI jobs
|
||||
# are actively using.
|
||||
ExecStart=/bin/sh -c 'docker ps -a --format "{% raw %}{{.ID}} {{.Names}} {{.RunningFor}}{% endraw %}" 2>/dev/null | grep -v "GITEA-ACTIONS-TASK" | grep -E "(hour|day|week|month|year)s? ago" | awk "{print $1}" | xargs -r docker rm -f 2>/dev/null || true'
|
||||
ExecStart=/usr/bin/docker system prune -af --filter "until={{ gitea_runner_prune_until }}" --volumes
|
||||
ExecStart=/usr/bin/docker network prune -f
|
||||
# Prune networks older than the prune-until threshold to avoid removing
|
||||
# networks that molecule tests are actively creating (e.g. 'traefik' network
|
||||
# created during molecule create phase before containers are attached).
|
||||
ExecStart=/usr/bin/docker network prune -f --filter "until={{ gitea_runner_prune_until }}"
|
||||
ExecStart=/usr/bin/docker builder prune -f
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Pre-pull Docker images for CI runner cache
|
||||
After=docker.service
|
||||
Wants=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
# Pull each image quietly. docker pull exits 0 if image is already up-to-date,
|
||||
# so this is idempotent. Errors are non-fatal (image may already be cached).
|
||||
{% for image in gitea_runner_pre_cache_images %}
|
||||
ExecStart=/usr/bin/docker pull -q {{ image }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Periodic Docker image pre-cache for CI runner
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{ gitea_runner_pre_cache_schedule }}
|
||||
Persistent=true
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -9,3 +9,13 @@ runner:
|
||||
container:
|
||||
label: "{{ gitea_runner_container_label }}"
|
||||
docker_host: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||
force_pull: {{ gitea_runner_force_pull | lower }}
|
||||
{% if gitea_runner_container_options | length > 0 %}
|
||||
options: "{{ gitea_runner_container_options }}"
|
||||
{% endif %}
|
||||
{% if gitea_runner_valid_volumes | length > 0 %}
|
||||
valid_volumes:
|
||||
{% for volume in gitea_runner_valid_volumes %}
|
||||
- "{{ volume }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -48,6 +48,14 @@ fi
|
||||
# runner (e.g., server restore, runner record deleted, Gitea restart with
|
||||
# token salt change), the runner logs "unregistered runner" every few seconds.
|
||||
# A service restart will not fix this; re-registration is required.
|
||||
#
|
||||
# Detection method: query the Gitea API to verify the runner's UUID still
|
||||
# exists. This is more reliable than parsing journal logs (which requires
|
||||
# journal access permissions that runner users may not have — see the
|
||||
# 2026-08-08 incident where journalctl --user returned "No journal files
|
||||
# were opened due to insufficient permissions" for all runner users,
|
||||
# causing the healthcheck to always report "OK: runner healthy" even
|
||||
# though all runners were unregistered).
|
||||
{% if gitea_runner_auto_recover_api_token %}
|
||||
# Auto-recovery is enabled: fetch a new registration token from the Gitea API
|
||||
# and re-register the runner automatically. A cooldown prevents infinite loops.
|
||||
@@ -58,10 +66,50 @@ GITEA_URL="{{ gitea_url }}"
|
||||
RUNNER_NAME="{{ gitea_runner_name }}"
|
||||
RUNNER_LABELS="{{ gitea_runner_labels }}"
|
||||
BINARY="{{ gitea_runner_binary_path }}"
|
||||
RUNNER_FILE="{{ gitea_runner_data_dir }}/.runner"
|
||||
{% endif %}
|
||||
recent_errors=$(journalctl --user -u gitea-runner.service --since "5 minutes ago" --no-pager -q 2>/dev/null | grep -c "unregistered runner" || true)
|
||||
if [[ "$recent_errors" -ge 3 ]]; then
|
||||
echo "CRITICAL: runner is unregistered in Gitea (re-login failed $recent_errors times in 5 minutes)."
|
||||
runner_unregistered=0
|
||||
|
||||
# Primary detection: query the Gitea API to check if the runner's ID
|
||||
# still exists in Gitea's runner list. This works regardless of journal
|
||||
# permissions.
|
||||
{% if gitea_runner_auto_recover_api_token %}
|
||||
if [[ -f "$GITEA_API_TOKEN_FILE" && -f "$RUNNER_FILE" ]]; then
|
||||
API_TOKEN=$(cat "$GITEA_API_TOKEN_FILE" 2>/dev/null || true)
|
||||
RUNNER_ID=$(python3 -c "import json; print(json.load(open('$RUNNER_FILE')).get('id',''))" 2>/dev/null || true)
|
||||
if [[ -n "$API_TOKEN" && -n "$RUNNER_ID" ]]; then
|
||||
# List all runners and check if our ID is present
|
||||
runner_found=$(curl -sf --connect-timeout 5 --max-time 10 \
|
||||
-H "Authorization: token $API_TOKEN" \
|
||||
"${GITEA_URL}/api/v1/admin/actions/runners" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
runners = data if isinstance(data, list) else data.get('runners', [])
|
||||
ids = [str(r.get('id', '')) for r in runners]
|
||||
print('1' if '$RUNNER_ID' in ids else '0')
|
||||
except Exception:
|
||||
print('0')
|
||||
" 2>/dev/null || echo "0")
|
||||
if [[ "$runner_found" != "1" ]]; then
|
||||
runner_unregistered=1
|
||||
echo "CRITICAL: runner ID $RUNNER_ID not found in Gitea (unregistered)."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
{% endif %}
|
||||
|
||||
# Fallback detection: check journal logs (if accessible)
|
||||
if [[ "$runner_unregistered" -eq 0 ]]; then
|
||||
recent_errors=$(journalctl --user -u gitea-runner.service --since "5 minutes ago" --no-pager -q 2>/dev/null | grep -c "unregistered runner" || true)
|
||||
if [[ "$recent_errors" -ge 3 ]]; then
|
||||
runner_unregistered=1
|
||||
echo "CRITICAL: runner is unregistered in Gitea (re-login failed $recent_errors times in 5 minutes)."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$runner_unregistered" -ge 1 ]]; then
|
||||
{% if gitea_runner_auto_recover_api_token %}
|
||||
# Check cooldown — skip if we recently attempted recovery
|
||||
if [[ -f "$COOLDOWN_FILE" ]]; then
|
||||
@@ -135,14 +183,32 @@ if [[ "$recent_errors" -ge 3 ]]; then
|
||||
systemctl --user start gitea-runner.service
|
||||
sleep 3
|
||||
|
||||
# Verify recovery — check if unregistered errors stopped
|
||||
new_errors=$(journalctl --user -u gitea-runner.service --since "10 seconds ago" --no-pager -q 2>/dev/null | grep -c "unregistered runner" || true)
|
||||
if [[ "$new_errors" -eq 0 ]]; then
|
||||
echo "OK: runner recovered and no longer reporting unregistered errors"
|
||||
# Clear cooldown on success
|
||||
rm -f "$COOLDOWN_FILE" 2>/dev/null || true
|
||||
# Verify recovery — query the Gitea API to confirm the new ID is registered
|
||||
NEW_ID=$(python3 -c "import json; print(json.load(open('$RUNNER_FILE')).get('id',''))" 2>/dev/null || true)
|
||||
if [[ -n "$NEW_ID" ]]; then
|
||||
new_found=$(curl -sf --connect-timeout 5 --max-time 10 \
|
||||
-H "Authorization: token $API_TOKEN" \
|
||||
"${GITEA_URL}/api/v1/admin/actions/runners" 2>/dev/null \
|
||||
| python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
runners = data if isinstance(data, list) else data.get('runners', [])
|
||||
ids = [str(r.get('id', '')) for r in runners]
|
||||
print('1' if '$NEW_ID' in ids else '0')
|
||||
except Exception:
|
||||
print('0')
|
||||
" 2>/dev/null || echo "0")
|
||||
if [[ "$new_found" == "1" ]]; then
|
||||
echo "OK: runner recovered and registered with new ID $NEW_ID"
|
||||
# Clear cooldown on success
|
||||
rm -f "$COOLDOWN_FILE" 2>/dev/null || true
|
||||
else
|
||||
echo "WARN: runner re-registered but ID not found in Gitea API. Will retry after cooldown."
|
||||
exit 3
|
||||
fi
|
||||
else
|
||||
echo "WARN: runner still showing unregistered errors after re-registration. Will retry after cooldown."
|
||||
echo "WARN: could not read new .runner file after re-registration. Will retry after cooldown."
|
||||
exit 3
|
||||
fi
|
||||
{% else %}
|
||||
@@ -157,18 +223,43 @@ fi
|
||||
|
||||
# 3. Check disk space — prune aggressively if below threshold
|
||||
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"
|
||||
# Force-remove stale containers (including running ones from failed molecule tests).
|
||||
# "docker container prune -f" only removes stopped containers, so running
|
||||
# containers from crashed CI jobs accumulate and consume disk/memory.
|
||||
# Exclude CI job containers (name starts with GITEA-ACTIONS-TASK).
|
||||
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_critical }} ]]; then
|
||||
echo "CRITICAL: Disk usage at ${disk_pct}% (>= {{ gitea_runner_healthcheck_disk_critical }}%), full prune"
|
||||
# Critical level: remove ALL stopped containers (no age filter) and ALL
|
||||
# unused images/volumes. The until=1h gentle prune is insufficient here.
|
||||
# Stop+rm stale non-CI containers regardless of age (failed molecule tests
|
||||
# from the last 59 minutes also consume disk).
|
||||
docker ps -a --format '{% raw %}{{.ID}} {{.Names}}{% endraw %}' 2>/dev/null \
|
||||
| grep -v 'GITEA-ACTIONS-TASK' \
|
||||
| awk '{print $1}' \
|
||||
| xargs -r docker rm -f 2>/dev/null || true
|
||||
docker system prune -af --filter "until=1h" --volumes || true
|
||||
docker system prune -af --volumes || true
|
||||
docker network prune -f || true
|
||||
docker builder prune -af || true
|
||||
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||
echo "INFO: Disk usage after full prune: ${disk_pct}%"
|
||||
elif [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
|
||||
echo "WARN: Disk usage at ${disk_pct}%, pruning runner resources (until=1h)"
|
||||
# Force-remove stale containers (including running ones from failed molecule tests)
|
||||
# that are older than 1 hour. "docker container prune -f" only removes stopped
|
||||
# containers, so running containers from crashed CI jobs accumulate and consume
|
||||
# disk/memory. Exclude CI job containers (name starts with GITEA-ACTIONS-TASK).
|
||||
# Only remove containers older than 1 hour to avoid killing molecule test
|
||||
# containers that CI jobs are actively using.
|
||||
docker ps -a --format '{% raw %}{{.ID}} {{.Names}} {{.RunningFor}}{% endraw %}' 2>/dev/null \
|
||||
| grep -v 'GITEA-ACTIONS-TASK' \
|
||||
| grep -E '(hour|day|week|month|year)s? ago' \
|
||||
| awk '{print $1}' \
|
||||
| xargs -r docker rm -f 2>/dev/null || true
|
||||
# Prune images and containers older than 1h (until filter is NOT
|
||||
# supported with --volumes, so prune volumes separately without a filter).
|
||||
docker image prune -af --filter "until=1h" 2>/dev/null || true
|
||||
docker container prune -f --filter "until=1h" 2>/dev/null || true
|
||||
docker volume prune -f 2>/dev/null || true
|
||||
# Prune networks older than 1 hour to avoid removing networks that
|
||||
# molecule tests are actively creating (e.g. 'traefik' network created
|
||||
# during molecule create phase before containers are attached).
|
||||
docker network prune -f --filter "until=1h" || 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
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
||||
|
||||
__version__ = "0.20.0"
|
||||
__version__ = "0.21.0"
|
||||
|
||||
Reference in New Issue
Block a user