Compare commits

...
37 Commits
Author SHA1 Message Date
grm-ci-bot 7e18d285ab release: v0.18.2 [skip ci] 2026-07-16 17:41:52 +00:00
emil f6ba60bda6 GRM-154: fix: load tun module and pre-configure systemd override for Arch rootless Docker
Post-merge / release-and-maintain (push) Failing after 520h26m59s
Post-merge / detect-and-configure (push) Failing after 520h28m23s
2026-07-16 17:39:31 +00:00
gitea-actions-bot 0545388b34 chore: update badge URLs to commit bf77093c [skip ci] 2026-07-16 15:59:16 +00:00
grm-ci-bot eb0a56350b release: v0.18.1 [skip ci] 2026-07-16 15:58:51 +00:00
emil f712a4493e GRM-152: fix: fetch rootless Docker scripts on Arch Linux
Post-merge / release-and-maintain (push) Failing after 522h9m48s
Post-merge / detect-and-configure (push) Failing after 522h10m55s
2026-07-16 15:56:59 +00:00
gitea-actions-bot 9289b19162 chore: update badge URLs to commit 1249a783 [skip ci] 2026-07-16 15:45:18 +00:00
emil 185251090f GRM-153: chore: bump devx from v0.45.1 to v0.47.1
Post-merge / release-and-maintain (push) Failing after 522h23m40s
Post-merge / detect-and-configure (push) Failing after 522h24m46s
2026-07-16 15:43:09 +00:00
gitea-actions-bot fff930920f chore: update badge URLs to commit 66400175 [skip ci] 2026-07-14 13:41:08 +00:00
emil 88eca1f6b8 GRM-151: chore: bump devx from 0.44.1 to 0.45.1
Post-merge / release-and-maintain (push) Failing after 572h27m49s
Post-merge / detect-and-configure (push) Failing after 572h28m47s
2026-07-14 13:39:11 +00:00
gitea-actions-bot 1718a415c8 chore: update badge URLs to commit eda0ab83 [skip ci] 2026-07-14 02:09:11 +00:00
emil 3f27ee1423 GRM-150: chore: bump devx from 0.44.1 to 0.45.0
Post-merge / release-and-maintain (push) Failing after 583h59m44s
Post-merge / detect-and-configure (push) Failing after 584h0m42s
2026-07-14 02:07:13 +00:00
gitea-actions-bot 2cf267bace chore: update badge URLs to commit 88b0ef4c [skip ci] 2026-07-14 01:17:35 +00:00
emil 87513f9e8f GRM-149: chore: bump devx to 0.44.1
Post-merge / release-and-maintain (push) Failing after 584h51m23s
Post-merge / detect-and-configure (push) Failing after 584h52m20s
2026-07-14 01:15:37 +00:00
gitea-actions-bot 8b1a959bc9 chore: update badge URLs to commit f35c689f [skip ci] 2026-07-13 03:26:32 +00:00
emil f6a4f1fe43 GRM-148: chore: bump devx to v0.41.1, update deps and runner version
Post-merge / release-and-maintain (push) Failing after 606h42m20s
Post-merge / detect-and-configure (push) Failing after 606h43m22s
2026-07-13 03:24:35 +00:00
gitea-actions-bot 9ea7ae656b chore: update badge URLs to commit 64f6f08d [skip ci] 2026-07-12 20:04:24 +00:00
emil 70240a13cf GRM-147: docs: add retrospective for CI consolidation and devx adoption
Post-merge / detect-and-configure (push) Failing after 614h5m52s
Post-merge / release-and-maintain (push) Failing after 614h4m47s
2026-07-12 20:02:03 +00:00
gitea-actions-bot fd7b786db4 chore: update badge URLs to commit cfe82892 [skip ci] 2026-07-12 01:55:49 +00:00
emil c4fe70979c GRM-146: ci: consolidate CI and post-merge workflows
Post-merge / release-and-maintain (push) Failing after 632h13m15s
Post-merge / detect-and-configure (push) Failing after 632h14m17s
2026-07-12 01:53:47 +00:00
gitea-actions-bot dd9fc601fb chore: update badge URLs to commit 793fed7a [skip ci] 2026-07-12 01:15:10 +00:00
grm-ci-bot 62b37d045e release: v0.18.0 [skip ci] 2026-07-12 01:14:36 +00:00
emil 390fcb9d4b GRM-144: feat(runner): enable IPv6 in rootless Docker via pasta network driver
Post-merge / detect-type (push) Waiting to run
Post-merge / validate-commit-msg (push) Canceled after 632h56m33s
Post-merge / release (push) Waiting to run
Post-merge / publish (push) Waiting to run
Post-merge / sync-wiki (push) Waiting to run
Post-merge / badges (push) Waiting to run
Post-merge / vikunja (push) Waiting to run
Post-merge / configure-repo (push) Waiting to run
2026-07-12 01:12:43 +00:00
gitea-actions-bot 9f8adf14bc chore: update badge URLs to commit 625e2176 [skip ci] 2026-07-11 23:58:43 +00:00
grm-ci-bot 92e4d2fd2b release: v0.17.2 [skip ci] 2026-07-11 23:57:11 +00:00
emil f30764d60f GRM-145: refactor: adopt devx v0.40.0
Post-merge / vikunja (push) Failing after 634h13m1s
Post-merge / release (push) Failing after 634h13m56s
Post-merge / detect-type (push) Waiting to run
Post-merge / validate-commit-msg (push) Waiting to run
Post-merge / publish (push) Waiting to run
Post-merge / sync-wiki (push) Waiting to run
Post-merge / badges (push) Waiting to run
Post-merge / configure-repo (push) Waiting to run
2026-07-11 23:55:15 +00:00
gitea-actions-bot a0e3fc09c9 chore: update badge URLs to commit c25e7760 [skip ci] 2026-07-09 11:57:18 +00:00
grm-ci-bot bb9e8e6c4b release: v0.17.1 [skip ci] 2026-07-09 11:56:36 +00:00
emil d8312ff62c GRM-143: fix: disable IPv6 in rootless Docker daemon on runners
Post-merge / vikunja (push) Failing after 694h14m48s
Post-merge / release (push) Failing after 694h14m52s
Post-merge / detect-type (push) Waiting to run
Post-merge / validate-commit-msg (push) Waiting to run
Post-merge / publish (push) Waiting to run
Post-merge / sync-wiki (push) Waiting to run
Post-merge / badges (push) Waiting to run
Post-merge / configure-repo (push) Waiting to run
2026-07-09 11:54:18 +00:00
gitea-actions-bot 39a8f20df2 chore: update badge URLs to commit feb0219b [skip ci] 2026-07-08 21:15:24 +00:00
grm-ci-bot 240f08fff4 release: v0.17.0 [skip ci] 2026-07-08 21:14:47 +00:00
emil d38a64b0e1 GRM-142: feat: bump devx to 0.38.0 and migrate to role-based Gitea tokens
Post-merge / vikunja (push) Failing after 708h55m19s
Post-merge / release (push) Failing after 708h56m38s
Post-merge / detect-type (push) Waiting to run
Post-merge / validate-commit-msg (push) Waiting to run
Post-merge / publish (push) Waiting to run
Post-merge / sync-wiki (push) Waiting to run
Post-merge / badges (push) Waiting to run
Post-merge / configure-repo (push) Waiting to run
2026-07-08 21:12:29 +00:00
gitea-actions-bot 89bd40c4a8 chore: update badge URLs to commit b9122602 [skip ci] 2026-07-07 22:17:56 +00:00
grm-ci-bot ab2101983f release: v0.16.0 [skip ci] 2026-07-07 22:17:22 +00:00
emil 12f4aa4c92 GRM-141: feat: consolidate docs checks into devx-docs-check target
Post-merge / configure-repo (push) Failing after 731h52m45s
Post-merge / sync-wiki (push) Failing after 731h53m54s
Post-merge / detect-type (push) Waiting to run
Post-merge / validate-commit-msg (push) Waiting to run
Post-merge / release (push) Waiting to run
Post-merge / publish (push) Waiting to run
Post-merge / badges (push) Waiting to run
Post-merge / vikunja (push) Waiting to run
2026-07-07 22:15:25 +00:00
gitea-actions-bot 60ad1ad30d chore: update badge URLs to commit f0f26310 [skip ci] 2026-07-06 09:38:28 +00:00
Emil SimeonovandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 0e64353b0d fix: replace --strict with --verify for sync_wiki
Post-merge / badges (push) Failing after 768h33m4s
Post-merge / validate-commit-msg (push) Waiting to run
Post-merge / release (push) Waiting to run
Post-merge / publish (push) Skipped
Post-merge / sync-wiki (push) Waiting to run
Post-merge / configure-repo (push) Waiting to run
Post-merge / detect-type (push) Waiting to run
Post-merge / vikunja (push) Waiting to run
The rewritten sync_wiki.py (devx 0.35.1) removed the --strict flag.
The new git-based approach is strict by default; --verify adds
post-sync page verification.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:35:20 +02:00
gitea-actions-bot 5956bb4fac chore: update badge URLs to commit 07949217 [skip ci] 2026-07-06 09:11:07 +00:00
103 changed files with 1314 additions and 1138 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ If `.venv` doesn't exist, run `make setup` first.
**Always run `make pytest-cov` before pushing** — CI enforces 100% **Always run `make pytest-cov` before pushing** — CI enforces 100%
coverage and will fail the PR if any lines are uncovered. This is the coverage and will fail the PR if any lines are uncovered. This is the
most common cause of CI quality job failures after code changes. The most common cause of CI validate job failures after code changes. The
pre-push git hook only validates Vikunja task existence, not tests. pre-push git hook only validates Vikunja task existence, not tests.
### API Response Type Checking ### API Response Type Checking
+12 -1
View File
@@ -15,7 +15,8 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
# If set, API checks are performed as a bonus but do NOT affect pass/fail. # If set, API checks are performed as a bonus but do NOT affect pass/fail.
# Required scopes: read:user, read:repository, read:admin (or just "admin") # Required scopes: read:user, read:repository, read:admin (or just "admin")
# Generate token at: Settings → Applications → Generate New Token # Generate token at: Settings → Applications → Generate New Token
# CI_GITEA_TOKEN=your-admin-api-token # CI_GITEA_API_TOKEN=your-admin-api-token
# Legacy CI_GITEA_TOKEN is also accepted.
# Integration test API retries (optional, default: 3). # Integration test API retries (optional, default: 3).
# Number of times to retry API checks waiting for runner to appear. # Number of times to retry API checks waiting for runner to appear.
@@ -50,6 +51,16 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
# Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL # Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL
CI_GITEA_USERNAME=your-gitea-username CI_GITEA_USERNAME=your-gitea-username
# Role-based Gitea API tokens (devx 0.40.0+)
# DEVELOPER_GITEA_API_TOKEN is used by local `grm trigger-workflow` and `make create-pr`.
# CI_GITEA_API_TOKEN is used by CI workflows (and accepted as a fallback for local tools).
# REVIEWER_GITEA_API_TOKEN is used by CI to post APPROVE reviews; it must belong to a
# different user than the PR author.
# Legacy CI_GITEA_TOKEN and REVIEW_GITEA_TOKEN are accepted as fallbacks.
# DEVELOPER_GITEA_API_TOKEN=your-developer-token
# CI_GITEA_API_TOKEN=your-ci-token
# REVIEWER_GITEA_API_TOKEN=your-reviewer-token
# Vikunja API token (required for `make create-task` dev workflow) # Vikunja API token (required for `make create-task` dev workflow)
# Generate at: Vikunja → Settings → API Tokens # Generate at: Vikunja → Settings → API Tokens
# VIKUNJA_TOKEN=your-vikunja-api-token # VIKUNJA_TOKEN=your-vikunja-api-token
+92 -166
View File
@@ -6,21 +6,38 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs: jobs:
quality: # Single validation job that merges: quality, detect-changes,
# release-dry-run, pre-merge-check, pr-review, and discover-runners.
# Uses ci-full image (has git-cliff for release-dry-run).
# Saves ~5x checkout+setup overhead vs 6 separate jobs.
validate:
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10 timeout-minutes: 15
defaults:
run:
shell: bash
outputs:
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
runner-count: ${{ steps.discover-runners.outputs.runner-count }}
runner-indices: ${{ steps.discover-runners.outputs.runner-indices }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint run: make setup-image EXTRAS=ci,lint
# --- quality steps ---
- name: Lint all - name: Lint all
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -30,37 +47,20 @@ jobs:
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
make pytest-cov make pytest-cov
- name: Documentation lint check - name: Documentation gate (coverage + stale refs + lint + version refs + prose)
env: env:
PYTHONPATH: src DEVX_DOC_COVERAGE_STRICT: "1"
run: | DEVX_DOC_VERSIONS_PKG: grm
. .venv/bin/activate 2>/dev/null || true DEVX_VALE_LEVEL: warning
python3 -m devx.ci.lint_docs --root .
- name: Documentation version reference check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_doc_versions --root . --package grm
- name: Vale prose lint check
env:
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
# Install vale if not present make devx-docs-check
if ! command -v vale >/dev/null 2>&1; then
python3 -m devx.tools.install_tools --tool vale
fi
vale sync
vale --minAlertLevel=error docs/ AGENTS.md README.md
- name: Translation completeness check - name: Translation completeness check
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_translations --translations src/grm/translations.json python3 -m devx.ci.check_translations --translations src/grm/translations.json
- name: Check unit test speed - name: Check unit test speed
env:
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5 python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
@@ -81,52 +81,10 @@ jobs:
else else
echo "act_runner not found — skipping workflow dry-run (static lint still passed)" echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
fi fi
# --- detect-changes step ---
release-dry-run:
needs: [quality, detect-changes]
if: needs.detect-changes.outputs.user-facing-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Release dry-run validation
env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run
detect-changes:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
outputs:
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Detect changed paths - name: Detect changed paths
id: detect id: detect
env: env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
@@ -134,22 +92,10 @@ jobs:
--base "origin/master" \ --base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \ --head "${{ github.event.pull_request.head.sha || github.sha }}" \
--github-output --github-output
# --- validate-pr + pr-review steps (PR only) ---
pre-merge-check:
needs: [quality, detect-changes]
if: github.event_name == 'pull_request'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
run: make setup-image EXTRAS=ci
- name: Validate auto-merge preconditions - name: Validate auto-merge preconditions
if: github.event_name == 'pull_request'
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
@@ -157,7 +103,6 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
PYTHONPATH: ${{ env.PYTHONPATH }}
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_auto_merge_ready \ python3 -m devx.ci.check_auto_merge_ready \
@@ -165,52 +110,65 @@ jobs:
--pr-title "$PR_TITLE" \ --pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \ --repo "$REPOSITORY" \
--pr-number "$PR_NUMBER" --pr-number "$PR_NUMBER"
- name: Run automated PR review
discover-runners: if: github.event_name == 'pull_request'
needs: [detect-changes] run: |
if: needs.detect-changes.outputs.ansible-changed == 'true' . .venv/bin/activate 2>/dev/null || true
runs-on: docker set -euo pipefail
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest python3 -m devx.ci.pr_review \
timeout-minutes: 10 "${{ github.event.number }}" \
outputs: "${{ github.repository }}"
runner-count: ${{ steps.discover.outputs.runner-count }} # --- release-dry-run step (conditional) ---
runner-indices: ${{ steps.discover.outputs.runner-indices }} - name: Release dry-run validation
steps: if: steps.detect.outputs.user-facing-changed == 'true'
- uses: actions/checkout@v4
- name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} DEVX_VERSION_FILE: src/grm/__init__.py
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} DEVX_TASK_PREFIX: GRM
run: make setup-image EXTRAS=ci run: |
- name: Discover available runners . .venv/bin/activate 2>/dev/null || true
id: discover export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run
# --- discover-runners step (conditional on ansible-changed) ---
- name: Discover available molecule runners
id: discover-runners
if: steps.detect.outputs.ansible-changed == 'true'
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }} MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.discover_runners \ python3 -m devx.molecule.discover_runners \
--owner "${{ github.repository_owner }}" \ --owner "${{ github.repository_owner }}" \
--repo "${{ github.event.repository.name }}" \ --repo "${{ github.event.repository.name }}" \
--github-output --github-output
- 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: molecule-tests:
needs: [quality, detect-changes, discover-runners] needs: [validate]
if: needs.detect-changes.outputs.ansible-changed == 'true' if: needs.validate.outputs.ansible-changed == 'true'
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10 timeout-minutes: 15
strategy: strategy:
fail-fast: true fail-fast: true
max-parallel: 3 max-parallel: 6
matrix: matrix:
runner-index: [1, 2, 3, 4, 5, 6] runner-index: [1, 2, 3, 4, 5, 6]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,molecule run: make setup-image EXTRAS=ci,molecule
- name: Install Ansible collections - name: Install Ansible collections
@@ -220,16 +178,25 @@ jobs:
- name: Discover assigned test pairs - name: Discover assigned test pairs
env: env:
RUNNER_INDEX: ${{ matrix.runner-index }} RUNNER_INDEX: ${{ matrix.runner-index }}
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }} MAX_RUNNERS: 6
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.molecule.distribute_molecule \ python3 -m devx.molecule.distribute_molecule \
--runner-index "$RUNNER_INDEX" \ --runner-index "$RUNNER_INDEX" \
--max-runners "$MAX_RUNNERS" \ --max-runners "$MAX_RUNNERS" \
--github-env --skip-if-excess --github-env
- name: Run molecule tests - name: Run molecule tests
if: env.SKIP != 'true' if: env.SKIP != 'true'
env:
GITEA_URL: ${{ github.server_url }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
RUN_ID: ${{ github.run_id }}
ANSIBLE_INJECT_INVOCATION: "1"
JOB_NAME: ${{ github.job }}
MATRIX_INDEX: ${{ matrix.runner-index }}
GITEA_REPOSITORY: ${{ github.repository }}
DOCKER_HOST: unix:///var/run/docker.sock
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
if [ -z "$TEST_PAIRS" ]; then exit 0; fi if [ -z "$TEST_PAIRS" ]; then exit 0; fi
@@ -237,61 +204,22 @@ jobs:
echo "Docker not available in CI container — skipping molecule tests" echo "Docker not available in CI container — skipping molecule tests"
exit 0 exit 0
fi fi
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin _TOKEN="$CI_GITEA_API_TOKEN"; [ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
[ -z "$_TOKEN" ] && { echo "Gitea API token not set — skipping Docker login"; exit 0; }
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
# shellcheck disable=SC2086 # intentional word splitting for argument expansion # shellcheck disable=SC2086 # intentional word splitting for argument expansion
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
env:
GITEA_URL: ${{ github.server_url }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
RUN_ID: ${{ github.run_id }}
ANSIBLE_INJECT_INVOCATION: "1"
JOB_NAME: ${{ github.job }}
MATRIX_INDEX: ${{ matrix.runner-index }}
GITEA_REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
DOCKER_HOST: unix:///var/run/docker.sock
pr-review:
if: github.event_name == 'pull_request'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Run automated PR review
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
set -euo pipefail
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
auto-merge: auto-merge:
# Auto-merge runs after all CI checks pass. It reads the task ID # Auto-merge runs after validate + molecule-tests pass (or molecule is skipped).
# from the branch name, validates the PR title, and squash-merges.
# Uses always() so it evaluates even when molecule-tests is skipped # Uses always() so it evaluates even when molecule-tests is skipped
# (Gitea Actions skips dependent jobs of skipped jobs by default). # (Gitea Actions skips dependent jobs of skipped jobs by default).
needs: [quality, detect-changes, pre-merge-check, pr-review, molecule-tests, release-dry-run] needs: [validate, molecule-tests]
if: >- if: >-
always() && always() &&
github.event_name == 'pull_request' && github.event_name == 'pull_request' &&
needs.quality.result == 'success' && needs.validate.result == 'success' &&
needs.pre-merge-check.result == 'success' && (needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
needs.pr-review.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped') &&
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10 timeout-minutes: 10
@@ -302,18 +230,17 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.CI_GITEA_TOKEN }} token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci run: make setup-image EXTRAS=ci
- name: Post approval review - name: Post approval review
env: env:
CI_GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }} PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \ python3 -m devx.ci.pr_review \
@@ -322,12 +249,11 @@ jobs:
--event APPROVE \ --event APPROVE \
--checklist-confirmed \ --checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \ --checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review, pre-merge-check)." --body "Auto-approved: all CI checks passed (validate, molecule-tests)."
- name: Squash merge with task ID - name: Squash merge with task ID
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
HEAD_REF: ${{ github.head_ref }} HEAD_REF: ${{ github.head_ref }}
+93 -225
View File
@@ -1,115 +1,136 @@
name: Post-merge name: Post-merge
# Runs on every push to master. A single workflow with conditional jobs # Runs on every push to master (after CI workflow merges a PR).
# for release, publish, wiki sync, badges, and Vikunja task updates. # Consolidated into 2 jobs (from 7) to reduce runner overhead:
# detect-and-configure ──→ release-and-maintain
# #
# Job dependency graph: # Job 1: detect release commit, validate commit msg, configure repo
# (branch protection, labels).
# Job 2: release + publish + sync-wiki + vikunja + badges.
# Individual steps are conditional on job 1 outputs.
# #
# detect-type ──┬── validate-commit-msg (skip if release commit) # The badges step always runs (even on release commits) so version
# ├── release (skip if release commit) # badge picks up the new __version__. It runs last so it sees the
# │ └── publish (needs release — builds & publishes to PyPI) # new version if release created one.
# ├── badges (ALWAYS runs — even on release commits)
# ├── configure-repo (independent — skip if release commit)
# ├── sync-wiki (skip if release commit — runs for ALL merges)
# └── vikunja (skip if release commit — runs for ALL merges)
#
# sync-wiki and vikunja run for ALL non-release commits, not just when
# release succeeds. This ensures the wiki and task tracker are updated
# even for infrastructure-only changes (docs, CI config, etc.).
#
# The badges job uses `if: always()` with no is-release condition so it
# runs on every push to master, including release commits. This ensures
# badges (tests, coverage, version, etc.) are always current.
# #
# When release creates a "release: vX.Y.Z" commit and tag, the publish # When release creates a "release: vX.Y.Z" commit and tag, the publish
# job (which depends on release) builds and publishes the package to the # step builds and publishes the package to the Gitea PyPI registry.
# Gitea PyPI registry. The release commit's post-merge run still updates # The release commit's post-merge run still updates badges. Other
# badges (version badge picks up the new version). Other jobs skip. # steps (sync-wiki, vikunja) skip on release commits.
on: on:
push: push:
branches: [master] branches: [master]
workflow_dispatch: workflow_dispatch:
concurrency:
group: post-merge-${{ github.ref }}
cancel-in-progress: true
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs: jobs:
detect-type: detect-and-configure:
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10 timeout-minutes: 10
defaults:
run:
shell: bash
outputs: outputs:
is-release: ${{ steps.check.outputs.is-release }} is-release: ${{ steps.check.outputs.is-release }}
is-automated: ${{ steps.check.outputs.is-automated }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 0
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci run: make setup-image EXTRAS=ci
- name: Ensure branch protection and labels
env:
DEVX_REPO_NAME: grm
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / validate (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.configure_repo
- name: Check if this is a release commit - name: Check if this is a release commit
id: check id: check
env:
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.detect_release_commit python3 -m devx.ci.detect_release_commit
validate-commit-msg:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Validate latest commit message - name: Validate latest commit message
if: steps.check.outputs.is-automated == 'false'
env: env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
git log -1 --format=%B > commit-msg.txt git log -1 --format=%B > commit-msg.txt
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
rm -f commit-msg.txt rm -f commit-msg.txt
- name: Detect changed paths
id: detect
if: steps.check.outputs.is-release == 'false'
env:
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.classify_changes \
--base "HEAD~1" \
--head "HEAD" \
--github-output
- 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: release-and-maintain:
needs: [detect-type] needs: [detect-and-configure]
if: needs.detect-type.outputs.is-release == 'false' if: always() && needs.detect-and-configure.result == 'success'
runs-on: docker runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15 timeout-minutes: 15
outputs: outputs:
tag: ${{ steps.release-tag.outputs.tag }} tag: ${{ steps.release-tag.outputs.tag }}
defaults:
run:
shell: bash
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.CI_GITEA_TOKEN }} ref: master
token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment - name: Set up environment
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }} CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint run: make setup-image EXTRAS=ci,lint
- name: Configure git - name: Configure git
run: | run: |
git config user.name "grm-ci-bot" git config user.name "grm-ci-bot"
git config user.email "grm-ci-bot@oblachno.fyi" git config user.email "grm-ci-bot@oblachno.fyi"
# --- release + publish (only if not a release commit) ---
- name: Run release - name: Run release
id: release-tag id: release-tag
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
env: env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/grm/__init__.py DEVX_VERSION_FILE: src/grm/__init__.py
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
@@ -117,207 +138,54 @@ jobs:
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release python3 -m devx.ci.release
- name: Notify on failure
if: failure()
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
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" \
--commit "${{ github.sha }}"
publish:
needs: [release]
if: needs.release.outputs.tag != ''
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release.outputs.tag }}
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Build and publish release - name: Build and publish release
if: steps.release-tag.outputs.tag != ''
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish \ git fetch --tags
"${{ needs.release.outputs.tag }}" \ git checkout "${{ steps.release-tag.outputs.tag }}"
"${{ github.repository }}" --auto-login python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
- name: Notify on failure # --- sync-wiki + vikunja (skip on automated/release commits) ---
if: failure()
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
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/publish" \
--commit "${{ github.sha }}"
sync-wiki:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 15
concurrency:
group: sync-wiki-${{ github.repository }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Sync documentation to wiki - name: Sync documentation to wiki
if: needs.detect-and-configure.outputs.is-automated == 'false'
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
- name: Notify on failure
if: failure()
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
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/sync-wiki" \
--commit "${{ github.sha }}"
badges:
needs: [detect-type]
if: always()
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Fetch latest master
run: |
git fetch origin master
git reset --hard origin/master
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=lint
- name: Generate and push badges
env:
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.push_badges
- name: Notify on failure
if: failure()
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
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/badges" \
--commit "${{ github.sha }}"
vikunja:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Update Vikunja task - name: Update Vikunja task
if: needs.detect-and-configure.outputs.is-automated == 'false'
env: env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }} VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6 DEVX_VIKUNJA_PROJECT_ID: 6
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}" python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
- name: Notify on failure # --- badges (always run — even on release commits) ---
if: failure() - name: Generate and push badges
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: |
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/vikunja" \
--commit "${{ github.sha }}"
configure-repo:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Ensure branch protection and labels
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
DEVX_REPO_NAME: grm
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / quality (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
run: | run: |
. .venv/bin/activate 2>/dev/null || true . .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.configure_repo export PATH="$HOME/.local/bin:$PATH"
# Fetch latest master to pick up any release commit that was pushed
git fetch origin master
git reset --hard origin/master
python3 -m devx.ci.push_badges
- name: Notify on failure - name: Notify on failure
if: failure() if: failure()
env: env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }} CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: | run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \ python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \ --repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \ --run-id "${{ github.run_id }}" \
--workflow "post-merge/configure-repo" \ --workflow "post-merge/release-and-maintain" \
--commit "${{ github.sha }}" --commit "${{ github.sha }}"
+4 -3
View File
@@ -81,11 +81,12 @@ repos:
pass_filenames: false pass_filenames: false
stages: [pre-commit] stages: [pre-commit]
- id: lint-docs - id: docs-check
name: documentation lint check name: documentation gate (coverage + stale refs + lint + version refs + prose)
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.lint_docs --root . entry: bash -c 'PYTHONPATH=src DEVX_DOC_COVERAGE_STRICT=1 DEVX_DOC_VERSIONS_PKG=grm DEVX_VALE_LEVEL=warning make devx-docs-check'
language: system language: system
pass_filenames: false pass_filenames: false
always_run: true
stages: [pre-commit] stages: [pre-commit]
- id: pytest-cov - id: pytest-cov
+4 -1
View File
@@ -34,14 +34,17 @@ write-good.E-Prime = NO
write-good.So = NO write-good.So = NO
write-good.ThereIs = NO write-good.ThereIs = NO
write-good.TooWordy = NO write-good.TooWordy = NO
write-good.Passive = NO
# Vale defaults — spelling catches too many technical terms # Vale defaults — spelling catches too many technical terms
Vale.Terms = NO Vale.Terms = NO
Vale.Repetition = NO Vale.Repetition = NO
Vale.Spelling = NO Vale.Spelling = NO
# Readability — warnings only, technical docs are naturally complex # Readability — technical docs are naturally complex, downgrade to suggestions
Readability.FleschReadingEase = suggestion Readability.FleschReadingEase = suggestion
Readability.FleschKincaid = suggestion
Readability.AutomatedReadability = suggestion
Readability.ColemanLiau = suggestion Readability.ColemanLiau = suggestion
Readability.LIX = suggestion Readability.LIX = suggestion
Readability.GunningFog = suggestion Readability.GunningFog = suggestion
+1 -1
View File
@@ -3,4 +3,4 @@ message: "Unlabeled code block — add a language tag (```bash, ```yaml, etc.)"
level: warning level: warning
scope: raw scope: raw
raw: raw:
- '(?s)```\n(?!.*```)' - '(?ms)^\n```\n.*?^```\s*$'
+1 -1
View File
@@ -2,7 +2,7 @@ Based on [write-good](https://github.com/btford/write-good).
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too. > Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
```text ```
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Brian Ford Copyright (c) 2014 Brian Ford
+63 -68
View File
@@ -51,13 +51,13 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
Both run via `make workflow-check` and are part of `make lint-all`. Both run via `make workflow-check` and are part of `make lint-all`.
The pre-commit hook runs actionlint automatically when workflow files change. The pre-commit hook runs actionlint automatically when workflow files change.
The CI `quality` job runs `make setup` (which installs all tools) then `make lint-all`. The CI `validate` job runs `make setup-image` (which installs all tools) then `make lint-all`.
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image). CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
## Architecture ## Architecture
- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible - **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible
- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup - **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role for rootless Docker runner setup with pasta networking (IPv6 support)
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications - **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits - **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
@@ -69,13 +69,13 @@ Every change to master goes through this workflow. No exceptions.
Branch protection and labels are automatically configured by Branch protection and labels are automatically configured by
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`), `devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
which runs as a `configure-repo` job in which runs as a step in the `detect-and-configure` job in
the post-merge workflow on every push to master. the post-merge workflow on every push to master.
The following rules are enforced for `master`: The following rules are enforced for `master`:
- **Require pull request**: No direct pushes to master - **Require pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **Require approval review**: At least 1 `APPROVE` review before merge
- **Require status checks**: CI quality + molecule tests must pass - **Require status checks**: CI validate + molecule tests must pass
- **Block force pushes**: No history rewriting on master - **Block force pushes**: No history rewriting on master
The auto-merge workflow enforces the APPROVE review check programmatically The auto-merge workflow enforces the APPROVE review check programmatically
@@ -84,6 +84,12 @@ as a defense-in-depth measure, but branch protection is the primary gate.
### 1. Create Vikunja Task ### 1. Create Vikunja Task
Create a task in Vikunja project 6 via `make create-task -- --title "Task title" --description "<h2>...</h2>"` (requires `VIKUNJA_TOKEN` in `.env`). This prints the `GRM-N` identifier and next-step instructions. Create a task in Vikunja project 6 via `make create-task -- --title "Task title" --description "<h2>...</h2>"` (requires `VIKUNJA_TOKEN` in `.env`). This prints the `GRM-N` identifier and next-step instructions.
**IMPORTANT:** The task title must NOT include the `GRM-N:` prefix.
The `make create-pr` and `check_auto_merge_ready` commands automatically
prepend `GRM-N: ` to the Vikunja task title when forming the PR title.
If the Vikunja task title already includes the prefix, the PR title will
have a double prefix and auto-merge validation will fail.
### 2. Create Branch ### 2. Create Branch
```bash ```bash
git checkout master && git pull git checkout master && git pull
@@ -117,8 +123,9 @@ architecture, code quality, security, i18n, testing, performance,
UX, documentation, workflow compliance, maintainability, resource UX, documentation, workflow compliance, maintainability, resource
management, backwards compatibility, and logging. management, backwards compatibility, and logging.
**Automated review (CI `pr-review` job):** Every PR triggers an automated **Automated review (CI `validate` job):** Every PR triggers an automated
review via `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`). This job posts a review with review via `python -m devx.ci.pr_review` as a step in the `validate` job.
This posts a review with
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on `COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
the **[auto]** items in the checklist: the **[auto]** items in the checklist:
@@ -166,16 +173,16 @@ it attests that the reviewer has gone through every checklist category.
The `--checklist-categories` flag is also **required** — it must list at The `--checklist-categories` flag is also **required** — it must list at
least 8 of the 13 category numbers, ensuring the reviewer actually least 8 of the 13 category numbers, ensuring the reviewer actually
checked each category rather than rubber-stamping. The review body must checked each category rather than rubber-stamping. The review body must
be substantive (> 50 characters) — trivial approvals like "LGTM" are be substantive (> 50 characters) — perfunctory approvals like "LGTM" are
rejected. rejected.
Then add the `ready-to-merge` label. The auto-merge workflow will: Then add the `ready-to-merge` label. The auto-merge workflow will:
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title 1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments) 2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
3. Wait for all CI checks to pass (including the `pr-review` job) 3. Wait for all CI checks to pass (including the `validate` job)
4. Squash-merge with title: `GRM-N: <conventional commit message>` 4. Squash-merge with title: `GRM-N: <conventional commit message>`
5. The post-merge workflow marks the Vikunja task as done 5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes (see below) 6. The release-and-maintain job automatically versions, tags, and publishes (see below)
**If the branch is behind master** (another PR merged first), auto-merge **If the branch is behind master** (another PR merged first), auto-merge
automatically rebases the PR's head branch via the Gitea API. This triggers automatically rebases the PR's head branch via the Gitea API. This triggers
@@ -191,21 +198,22 @@ No manual rebase needed. To rebase manually: `make rebase` (local) or
### CI Path Filtering ### CI Path Filtering
The CI workflow includes a `pre-merge-check` job (runs after quality + The CI workflow's `validate` job includes a pre-merge validation step
detect-changes) that validates branch format, PR title, and Vikunja task that validates branch format, PR title, and Vikunja task match. This
match. This fails fast before expensive molecule tests run. fails fast before expensive molecule tests run.
The CI workflow includes a `detect-changes` job that checks whether any files The `validate` job also includes a `detect-changes` step that checks
under `ansible/` or `.ansible-lint` have changed. If no Ansible files are whether any files under `ansible/` or `.ansible-lint` have changed. If
changed, molecule tests are skipped — this prevents non-Ansible changes no Ansible files are changed, molecule tests are skipped — this prevents
(e.g., Python scripts, workflow YAML, docs) from being blocked by molecule non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from
test infrastructure flakiness. being blocked by molecule test infrastructure flakiness.
### Dynamic Runner Discovery ### Dynamic Runner Discovery
Molecule tests are distributed across available Gitea Actions runners Molecule tests are distributed across available Gitea Actions runners
dynamically via `devx.molecule.discover_runners`. The `discover-runners` dynamically via `devx.molecule.discover_runners`. The `validate` job
job queries the Gitea API for runners at all levels (repo, org, instance) includes a `discover-runners` step (conditional on ansible-changed) that
queries the Gitea API for runners at all levels (repo, org, instance)
and generates a dynamic matrix. If the API can't see instance-level runners and generates a dynamic matrix. If the API can't see instance-level runners
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable, (no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
then to a default of 3. then to a default of 3.
@@ -218,47 +226,27 @@ then to a default of 3.
### Automated Release Pipeline ### Automated Release Pipeline
After a PR is merged to master, the **post-merge workflow** After a PR is merged to master, the **post-merge workflow**
(`.gitea/workflows/post-merge.yml`) runs automatically. This single (`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
workflow consolidates release, wiki sync, badge generation, and into 2 jobs (from 7) to reduce runner overhead:
Vikunja task updates:
1. **detect-type** — Checks if the commit is a regular merge or a 1. **detect-and-configure** — Configures repo (branch protection, labels),
release commit (`release: vX.Y.Z`). All subsequent jobs skip for detects release commit, validates commit message. Outputs `is-release`
release commits (the `[skip ci]` tag also prevents re-triggering). and `is-automated` for the next job.
2. **release** — Runs `devx.ci.release` which: 2. **release-and-maintain** — Runs all post-merge maintenance as
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only conditional steps:
workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`, - **release** (if not a release commit) — Runs `devx.ci.release` which
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version checks for user-facing changes via `classify_changes` (skips if only
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes. workflow/infrastructure files changed), uses git-cliff for semver,
- Uses **git-cliff** to calculate the next semver version from conventional commits updates `__version__`, updates `CHANGELOG.md`, runs lint+tests, commits
- Updates `__version__` in `src/grm/__init__.py` (single source of truth) with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes to master.
- Updates `CHANGELOG.md` with the new version section - **publish** (if release created a tag) — Builds and publishes the
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy package to the Gitea PyPI registry. Checks out the release tag
- If lint or tests fail, **aborts immediately** — no commit, no tag within the same job.
- Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents - **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
re-triggering post-merge on the release commit) - **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
- Creates an annotated tag `vX.Y.Z` on the release commit - **badges** (always) — Generates and pushes quality badge SVGs to the
- Pushes both the commit and tag to master `badges` branch. Fetches latest master first to pick up release commits.
- `--skip-tests` flag bypasses test verification (emergency use only, not recommended)
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
non-release commits (not just when release succeeds), so docs-only
changes still update the wiki.
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
Uses `if: always()` so it runs on every push, including release commits.
The script fetches the latest master before generating badges to pick up
any release commits.
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
non-release commits (not just when release succeeds), so infrastructure-only
changes still update the task tracker.
6. **publish** — Runs after release succeeds (needs: release). Builds and
publishes the package to the Gitea PyPI registry. Gets the tag from the
release job's `tag` output.
### Smart CI: User-Facing vs Workflow-Only Changes ### Smart CI: User-Facing vs Workflow-Only Changes
@@ -379,9 +367,9 @@ platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this `devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
avoids dev tools importing directly from CI modules. avoids dev tools importing directly from CI modules.
2. **Publish job** (in `post-merge.yml`, needs: release): 2. **Publish step** (in the `release-and-maintain` job, runs after the release step creates a tag):
- Runs after the release job creates a tag - Runs after the release step creates a tag
- Gets the tag from `needs.release.outputs.tag` - Gets the tag from the release step's output
- Builds the Python package - Builds the Python package
- Publishes to the Gitea PyPI registry - Publishes to the Gitea PyPI registry
- Creates a Gitea release with git-cliff-generated release notes - Creates a Gitea release with git-cliff-generated release notes
@@ -486,7 +474,13 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
- `install_runner.yml` handles: download, config, validate, register, service - `install_runner.yml` handles: download, config, validate, register, service
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates) - `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
- `systemctl --user` tasks must be guarded by `docker_rootless_setup` - `systemctl --user` tasks must be guarded by `docker_rootless_setup`
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files) - Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
- On Arch Linux, `rootless_docker.yml` fetches the rootless setup scripts
(`dockerd-rootless-setuptool.sh`, `dockerd-rootless.sh`) from `moby/moby` `contrib/`
at a pinned ref (`gitea_runner_rootless_scripts_ref`) into `/usr/bin` and installs
`rootlesskit` — Arch's `docker` package ships neither. These fetch tasks run
regardless of `docker_rootless_setup` so CI exercises them on the archlinux platform.
See ADR-011 in the decision log.
## Molecule Scenarios ## Molecule Scenarios
@@ -536,7 +530,7 @@ docs/
### Documentation Coverage ### Documentation Coverage
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented - `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
- Runs as a CI step in the quality job with `--fail-on-missing` (blocks CI if docs are missing) - Runs as a CI step in the validate job with `--fail-on-missing` (blocks CI if docs are missing)
- Enforced: 100% coverage for public CLI commands and major architectural components - Enforced: 100% coverage for public CLI commands and major architectural components
### Updating Documentation ### Updating Documentation
@@ -566,7 +560,7 @@ the user should not need to specify which profile to use.
| Profile | Purpose | | Profile | Purpose |
|---------|---------| |---------|---------|
| `ci-investigator` | Investigate CI failures (quality, molecule, release, publish, wiki sync) | | `ci-investigator` | Investigate CI failures (validate, molecule-tests, release-and-maintain) |
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail | | `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification | | `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs | | `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs |
@@ -576,7 +570,7 @@ the user should not need to specify which profile to use.
| Trigger | Profile | Mode | | Trigger | Profile | Mode |
|---------|---------|------| |---------|---------|------|
| CI run failure (quality, molecule-tests, release, publish, sync-wiki) | `ci-investigator` | Background | | CI run failure (validate, molecule-tests, release-and-maintain) | `ci-investigator` | Background |
| PR ready for review | `pr-reviewer` | Foreground | | PR ready for review | `pr-reviewer` | Foreground |
| Molecule tests need to run | `molecule-runner` | Background | | Molecule tests need to run | `molecule-runner` | Background |
| Dependency upgrade requested | `dep-upgrader` | Background | | Dependency upgrade requested | `dep-upgrader` | Background |
@@ -590,7 +584,7 @@ the user should not need to specify which profile to use.
2. **Background by default, foreground when blocking.** 2. **Background by default, foreground when blocking.**
3. **Provide full context in the prompt** — subagents don't inherit conversation history. 3. **Provide full context in the prompt** — subagents don't inherit conversation history.
4. **One subagent per concern.** Chain: investigate → fix in main session → review. 4. **One subagent per concern.** Chain: investigate → fix in main session → review.
5. **Don't delegate trivial work** (<30s, <50 lines of context). 5. **Don't delegate minor work** (<30s, <50 lines of context).
6. **Compact after subagent returns.** 6. **Compact after subagent returns.**
7. **Never skip delegation to save time** — it keeps main context small. 7. **Never skip delegation to save time** — it keeps main context small.
@@ -602,8 +596,9 @@ tool, workflow, or process issues that warrant follow-up. These issues
use the `feedback` label plus a category label (`tooling`, use the `feedback` label plus a category label (`tooling`,
`ci-improvement`, `doc-improvement`, `workflow-improvement`). `ci-improvement`, `doc-improvement`, `workflow-improvement`).
Standard labels are created automatically by `configure_repo` (runs in Standard labels are created automatically by `configure_repo` (runs as
post-merge on every master push). If a label does not exist yet, the a step in `detect-and-configure` in post-merge on every master push).
If a label does not exist yet, the
subagent's issue creation will still succeed — labels can be added subagent's issue creation will still succeed — labels can be added
afterwards. afterwards.
+46
View File
@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.18.2] - 2026-07-16
### Bug Fixes
- Load tun module and pre-configure systemd override for Arch rootless Docker
## [0.18.1] - 2026-07-16
### Bug Fixes
- Fetch rootless Docker scripts on Arch Linux
## [0.18.0] - 2026-07-12
### Features
- *(runner)* Enable IPv6 in rootless Docker via pasta network driver
## [0.17.2] - 2026-07-11
### Refactor
- Adopt devx v0.40.0
## [0.17.1] - 2026-07-09
### Bug Fixes
- Disable IPv6 in rootless Docker daemon on runners
## [0.17.0] - 2026-07-08
### Features
- Bump devx to 0.38.0 and migrate to role-based Gitea tokens
## [0.16.0] - 2026-07-07
### Features
- Consolidate docs checks into devx-docs-check target
### Bug Fixes
- Replace --strict with --verify for sync_wiki
## [0.15.0] - 2026-07-06 ## [0.15.0] - 2026-07-06
### Features ### Features
+15 -9
View File
@@ -1,6 +1,7 @@
.PHONY: all setup setup-ci setup-quality setup-molecule setup-release setup-image install update lint ansible-lint makefile-lint lint-all lint-ruff lint-format lint-bandit lint-deps typecheck checkmake install-hooks test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools check-api-identity-checks .PHONY: all setup setup-ci setup-quality setup-molecule setup-release setup-image install update lint ansible-lint makefile-lint lint-all lint-ruff lint-format lint-bandit lint-deps typecheck checkmake install-hooks test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools check-api-identity-checks
.PHONY: configure-gitea-pypi .PHONY: configure-gitea-pypi
.PHONY: create-task create-pr push-with-pr git-push .PHONY: create-task create-pr push-with-pr git-push
.PHONY: check-docs docs-check
PYTHON := python3 PYTHON := python3
VENV := .venv VENV := .venv
@@ -49,13 +50,14 @@ setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install $(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
# Lean setup for CI jobs that need pytest + lint tools + runtime deps # Lean setup for CI jobs that need pytest + lint tools + runtime deps
# (detect-changes, discover-runners, pr-review, sync-wiki, badges) # (validate job steps: detect-changes, discover-runners, pr-review;
# badges job runs generate_badges.py which needs ruff, pyright, bandit # release-and-maintain job steps: sync-wiki, badges)
# badges step runs generate_badges.py which needs ruff, pyright, bandit
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]' @$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login @$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
# Setup for the quality job (lint + test deps, actionlint tool) # Setup for the validate CI job (lint + test deps, actionlint tool)
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]' @$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.install_tools @$(BIN)/python -m devx.tools.install_tools
@@ -85,7 +87,8 @@ setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
# the venv symlink first, then installs the project. # the venv symlink first, then installs the project.
setup-image: setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \ @if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$$CI_GITEA_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \ _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; \
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \ pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
@@ -158,10 +161,9 @@ workflow-dryrun: devx-workflow-dryrun
workflow-check: devx-workflow-check workflow-check: devx-workflow-check
configure-gitea-pypi: configure-gitea-pypi:
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \ _TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \ if [ -z "$$_TOKEN" ]; then echo "[configure-gitea-pypi] Gitea API token not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \ echo "[configure-gitea-pypi] Gitea PyPI registry configured (token present)."
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)."
ansible-lint: ansible-lint:
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/ PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
@@ -182,7 +184,7 @@ test-integration:
$(BIN)/pytest tests/integration/ -v --no-cov $(BIN)/pytest tests/integration/ -v --no-cov
MOLECULE := $(realpath $(BIN))/molecule MOLECULE := $(realpath $(BIN))/molecule
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE) MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea_runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
# Quick local test: Ubuntu 22.04 only, all scenarios # Quick local test: Ubuntu 22.04 only, all scenarios
molecule: molecule:
@@ -202,3 +204,7 @@ create-task: devx-create-task
create-pr: devx-create-pr create-pr: devx-create-pr
push-with-pr: devx-push-with-pr push-with-pr: devx-push-with-pr
git-push: devx-push git-push: devx-push
# --- Documentation checks (via devx.mak fragment) -----------------------------
check-docs: devx-check-docs
docs-check: devx-docs-check
+12 -12
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/python.svg)](https://www.python.org/downloads/)
## Why GRM? ## Why GRM?
@@ -147,7 +147,7 @@ GRM provides a single `grm` command with subcommands for the full runner lifecyc
| `grm enable <name>` | Enable a runner to start on boot | | `grm enable <name>` | Enable a runner to start on boot |
| `grm disable <name>` | Disable and deregister a runner | | `grm disable <name>` | Disable and deregister a runner |
| `grm status <name>` | Check the status of a registered runner | | `grm status <name>` | Check the status of a registered runner |
| `grm remove <name>` | Remove a runner completely (with remote cleanup) | | `grm remove <name>` | Remove a runner entirely (with remote cleanup) |
| `grm remove <name> --force` | Remove only the local registry entry (skip remote cleanup) | | `grm remove <name> --force` | Remove only the local registry entry (skip remote cleanup) |
| `grm list` | List all registered runners with live status | | `grm list` | List all registered runners with live status |
| `grm list --no-status` | List registered runners without SSH status checks | | `grm list --no-status` | List registered runners without SSH status checks |
@@ -187,7 +187,7 @@ GRM reads configuration from a `.env` file in the current directory (loaded auto
### Sudo Password Handling ### Sudo Password Handling
GRM delegates remote operations to Ansible, which uses `sudo` (become) on the target host. There are several ways to provide the sudo password, in priority order: GRM delegates remote operations to Ansible, which uses `sudo` (become) on the target host. There are multiple ways to provide the sudo password, in priority order:
1. **`--become-password-file <path>`** (CLI flag, global) — Read sudo password from a file. Works for all commands including `grm list`. 1. **`--become-password-file <path>`** (CLI flag, global) — Read sudo password from a file. Works for all commands including `grm list`.
2. **`GRM_BECOME_PASSWORD_FILE`** (env var) — Same as above, set in `.env` or environment. 2. **`GRM_BECOME_PASSWORD_FILE`** (env var) — Same as above, set in `.env` or environment.
@@ -266,8 +266,8 @@ One of GRM's core features is the ability to run multiple isolated runners on th
- **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/` - **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/`
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock` - **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
- **Data directory**: `/var/lib/gitea-runner/<name>/` - **Data directory**: `/var/lib/gitea_runner/<name>/`
- **Config directory**: `/etc/gitea-runner/<name>/` - **Config directory**: `/etc/gitea_runner/<name>/`
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable) - **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
- **Docker prune timer**: Per-instance daily cleanup - **Docker prune timer**: Per-instance daily cleanup
@@ -341,13 +341,13 @@ GRM consists of two layers:
1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list. 1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list.
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer. 2. **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer.
```text ```text
grm install <host> grm install <host>
└── RunnerManager.install() └── RunnerManager.install()
└── ansible-playbook ansible/install-runner.yml └── ansible-playbook ansible/install-runner.yml
└── role: gitea-runner └── role: gitea_runner
├── user_setup.yml (create per-runner system user + lingering) ├── user_setup.yml (create per-runner system user + lingering)
├── rootless_docker.yml (rootless Docker setup under runner user) ├── rootless_docker.yml (rootless Docker setup under runner user)
├── install_runner.yml (download binary, config, register, service) ├── install_runner.yml (download binary, config, register, service)
+6 -6
View File
@@ -6,13 +6,13 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Stop gitea-runner user service - name: Stop gitea-runner user service
ansible.builtin.command: systemctl --user stop gitea-runner ansible.builtin.command: systemctl --user stop gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: systemd_available.stat.exists
@@ -21,7 +21,7 @@
- name: Stop and disable healthcheck timer - name: Stop and disable healthcheck timer
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: systemd_available.stat.exists
@@ -30,14 +30,14 @@
- name: Include deregistration - name: Include deregistration
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: deregister.yml tasks_from: deregister.yml
when: not skip_runner_registration | default(false) when: not gitea_runner_skip_registration | default(false)
- name: Disable gitea-runner user service - name: Disable gitea-runner user service
ansible.builtin.command: systemctl --user disable gitea-runner ansible.builtin.command: systemctl --user disable gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: systemd_available.stat.exists
+3 -3
View File
@@ -6,13 +6,13 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Enable gitea-runner user service - name: Enable gitea-runner user service
ansible.builtin.command: systemctl --user enable gitea-runner ansible.builtin.command: systemctl --user enable gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: systemd_available.stat.exists
@@ -21,7 +21,7 @@
- name: Start gitea-runner user service - name: Start gitea-runner user service
ansible.builtin.command: systemctl --user start gitea-runner ansible.builtin.command: systemctl --user start gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: systemd_available.stat.exists
+1 -1
View File
@@ -3,4 +3,4 @@
hosts: all hosts: all
become: true become: true
roles: roles:
- role: gitea-runner - role: gitea_runner
+34 -34
View File
@@ -6,11 +6,11 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Get runner user UID - name: Get runner user UID
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
register: runner_uid_result register: runner_uid_result
changed_when: false changed_when: false
failed_when: false failed_when: false
@@ -23,20 +23,20 @@
- name: Stop gitea-runner user service - name: Stop gitea-runner user service
ansible.builtin.command: systemctl --user stop gitea-runner ansible.builtin.command: systemctl --user stop gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: gitea_runner_systemd_available.stat.exists
changed_when: true changed_when: true
failed_when: false failed_when: false
- name: Disable gitea-runner user service - name: Disable gitea-runner user service
ansible.builtin.command: systemctl --user disable gitea-runner ansible.builtin.command: systemctl --user disable gitea-runner
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: gitea_runner_systemd_available.stat.exists
changed_when: true changed_when: true
failed_when: false failed_when: false
@@ -47,7 +47,7 @@
args: args:
executable: /bin/bash executable: /bin/bash
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
@@ -57,7 +57,7 @@
- name: Prune all Docker images, volumes, and build cache (rootless) - name: Prune all Docker images, volumes, and build cache (rootless)
ansible.builtin.command: docker system prune -af --volumes ansible.builtin.command: docker system prune -af --volumes
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
@@ -67,7 +67,7 @@
- name: Stop rootless Docker daemon - name: Stop rootless Docker daemon
ansible.builtin.command: systemctl --user stop docker ansible.builtin.command: systemctl --user stop docker
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
changed_when: true changed_when: true
@@ -75,120 +75,120 @@
- name: Include deregistration - name: Include deregistration
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: deregister.yml tasks_from: deregister.yml
when: not skip_runner_registration | default(false) when: not gitea_runner_skip_registration | default(false)
- name: Stop and disable healthcheck timer - name: Stop and disable healthcheck timer
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
become: true become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
when: systemd_available.stat.exists when: gitea_runner_systemd_available.stat.exists
changed_when: true changed_when: true
failed_when: false failed_when: false
- name: Remove docker-prune user service file - name: Remove docker-prune user service file
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.service" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.service"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove docker-prune user timer file - name: Remove docker-prune user timer file
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.timer" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.timer"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove healthcheck user service file - name: Remove healthcheck user service file
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.service" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/runner-healthcheck.service"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove healthcheck user timer file - name: Remove healthcheck user timer file
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.timer" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/runner-healthcheck.timer"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove healthcheck script - name: Remove healthcheck script
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}/healthcheck.sh" path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ gitea_runner_name) }}/healthcheck.sh"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove systemd user unit file - name: Remove systemd user unit file
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/gitea-runner.service"
state: absent state: absent
when: remove_systemd_template | default(true) when: remove_systemd_template | default(true)
- name: Kill remaining processes of runner user - name: Kill remaining processes of runner user
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
failed_when: false failed_when: false
changed_when: true changed_when: true
- name: Wait for processes to terminate - name: Wait for processes to terminate
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
failed_when: false failed_when: false
changed_when: false changed_when: false
- name: Disable lingering for runner user - name: Disable lingering for runner user
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
failed_when: false failed_when: false
changed_when: true changed_when: true
- name: Remove runner user and home directory - name: Remove runner user and home directory
ansible.builtin.user: ansible.builtin.user:
name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}" name: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
state: absent state: absent
remove: true remove: true
when: remove_runner_user | default(true) when: gitea_runner_remove_user | default(true)
failed_when: false failed_when: false
- name: Remove Docker data root when user is kept - name: Remove Docker data root when user is kept
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.local/share/docker" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.local/share/docker"
state: absent state: absent
when: not (remove_runner_user | default(true)) when: not (gitea_runner_remove_user | default(true))
failed_when: false failed_when: false
- name: Remove act cache when user is kept - name: Remove act cache when user is kept
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.cache/act" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.cache/act"
state: absent state: absent
when: not (remove_runner_user | default(true)) when: not (gitea_runner_remove_user | default(true))
failed_when: false failed_when: false
- name: Remove systemd user config dir when user is kept - name: Remove systemd user config dir when user is kept
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user" path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user"
state: absent state: absent
when: not (remove_runner_user | default(true)) when: not (gitea_runner_remove_user | default(true))
failed_when: false failed_when: false
- name: Remove subuid entry for runner user - name: Remove subuid entry for runner user
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /etc/subuid path: /etc/subuid
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:" regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove subgid entry for runner user - name: Remove subgid entry for runner user
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /etc/subgid path: /etc/subgid
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:" regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
state: absent state: absent
failed_when: false failed_when: false
- name: Remove runner data directory - name: Remove runner data directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}" path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ gitea_runner_name) }}"
state: absent state: absent
- name: Remove runner config directory - name: Remove runner config directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}" path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ gitea_runner_name) }}"
state: absent state: absent
+1 -1
View File
@@ -2,6 +2,6 @@ collections:
- name: community.general - name: community.general
version: "==13.1.0" version: "==13.1.0"
- name: ansible.posix - name: ansible.posix
version: "==2.2.0" version: "==2.2.1"
- name: community.docker - name: community.docker
version: "==5.2.1" version: "==5.2.1"
+2 -2
View File
@@ -7,12 +7,12 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Resolve runner UID - name: Resolve runner UID
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: resolve_uid.yml tasks_from: resolve_uid.yml
- name: Stop gitea-runner user service - name: Stop gitea-runner user service
@@ -1,55 +0,0 @@
---
gitea_runner_version: "1.0.8"
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-26.04"
skip_runner_registration: false
# Per-runner user (rootless isolation)
gitea_runner_user_prefix: "grm-"
gitea_runner_base_home: "/home"
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ runner_name }}"
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
# Base paths (instance-scoped via runner_name)
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
gitea_runner_base_config_dir: "/etc/gitea-runner"
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
# Prune configuration
gitea_runner_prune_until: "24h"
gitea_runner_prune_schedule: "daily"
gitea_runner_prune_label: "gitea-runner=true"
# Service configuration
gitea_runner_service_restart_sec: "5"
# Health check configuration
gitea_runner_healthcheck_interval: "5min"
gitea_runner_healthcheck_boot_delay: "2min"
gitea_runner_healthcheck_disk_threshold: 85
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
# 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.
gitea_admin_token: ""
# Removal defaults
remove_systemd_template: true
remove_runner_user: true
# Runner configuration
gitea_runner_log_level: "info"
gitea_runner_container_label: "gitea-runner=true"
gitea_runner_file: ".runner"
# Docker installation (for rootless dependencies)
docker_gpg_key_path: "/etc/apt/keyrings/docker.gpg"
docker_apt_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
docker_apt_source_line: >-
deb [arch={{ docker_apt_arch }} signed-by={{ docker_gpg_key_path }}]
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
{{ ansible_facts['distribution_release'] }} stable
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
docker_rootless_setup: true
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "molecule-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "deregister-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "lifecycle-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "remove-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "template-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,12 +0,0 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
runner_name: "update-test-runner"
skip_runner_registration: true
docker_rootless_setup: false
roles:
- role: gitea-runner
@@ -1,58 +0,0 @@
---
- name: Check if runner registration file exists
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_stat
- name: Read runner registration file
ansible.builtin.slurp:
src: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_content
when: runner_file_stat.stat.exists | default(false) | bool
- name: Parse runner registration data
ansible.builtin.set_fact:
runner_reg: >
{{ (runner_file_content.content | b64decode | from_json)
if (runner_file_content is defined and runner_file_content.content is defined)
else {} }}
when: runner_file_stat.stat.exists | default(false) | bool
- name: Deregister runner from Gitea via API
ansible.builtin.command: >
curl -sf --connect-timeout 5 --max-time 10 -X DELETE
-H "Authorization: token {{ gitea_admin_token | default(registration_token) }}"
"{{ gitea_url }}/api/v1/admin/actions/runners/{{ runner_reg.id }}"
args:
chdir: "{{ gitea_runner_data_dir }}"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
when:
- runner_file_stat.stat.exists | default(false) | bool
- not skip_runner_registration
- runner_reg.id is defined
register: deregister_output
changed_when: deregister_output.rc == 0
failed_when: false
- name: Warn if deregistration failed
ansible.builtin.debug:
msg: >-
WARNING: Runner deregistration from Gitea failed (rc={{ deregister_output.rc | default('N/A') }}).
The runner entry may remain in Gitea's admin UI as offline.
Use an admin token (gitea_admin_token var) to enable automatic cleanup,
or remove it manually from {{ gitea_url }}/-/admin/actions/runners
when:
- runner_file_stat.stat.exists | default(false) | bool
- not skip_runner_registration
- deregister_output is defined
- deregister_output.rc | default(1) != 0
- name: Remove runner registration file
ansible.builtin.file:
path: "{{ gitea_runner_data_dir }}/.runner"
state: absent
when: runner_file_stat.stat.exists | default(false) | bool
@@ -1,102 +0,0 @@
---
- name: Check runner registration file exists
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_stat
- name: Read runner registration file
ansible.builtin.slurp:
src: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_content
when: runner_file_stat.stat.exists | default(false) | bool
- name: Parse runner registration data
ansible.builtin.set_fact:
runner_reg: >
{{ (runner_file_content.content | b64decode | from_json)
if (runner_file_content is defined and runner_file_content.content is defined)
else {} }}
when: runner_file_stat.stat.exists | default(false) | bool
- name: Verify runner user service 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 }}"
register: service_check
changed_when: false
when:
- systemd_available.stat.exists
- docker_rootless_setup
- name: Validate runner installation
ansible.builtin.fail:
msg: >
Runner '{{ runner_name }}' is not properly installed:
{% if not (runner_file_stat.stat.exists | default(false)) %}
- Registration file (.runner) is missing. Registration may have failed.
{% endif %}
{% if docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active' %}
- Systemd user service is not active.
{% endif %}
when: >
not (runner_file_stat.stat.exists | default(false))
or (docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active')
- name: Report runner status
ansible.builtin.debug:
msg: >
Runner '{{ runner_name }}' is installed and running.
Registered: {{ runner_file_stat.stat.exists | default(false) }}
{% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %}
{% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %}
{% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %}
Service: {{ service_check.stdout | default('unknown') | trim }}
- name: Optional Gitea API verification
when:
- gitea_url is defined
- gitea_admin_token is defined
- gitea_admin_token | length > 0
block:
- name: Check admin runners API
ansible.builtin.uri:
url: "{{ gitea_url }}/api/v1/admin/runners"
headers:
Authorization: "token {{ gitea_admin_token }}"
method: GET
status_code: [200, 401, 403, 404]
return_content: true
body_format: json
register: admin_api_response
ignore_errors: true
- name: Check repo runners API
ansible.builtin.uri:
url: "{{ gitea_url }}/api/v1/repos/{{ gitea_runner_test_repo | default('oblachno-oss/grm') }}/actions/runners"
headers:
Authorization: "token {{ gitea_admin_token }}"
method: GET
status_code: [200, 401, 403, 404]
return_content: true
body_format: json
register: repo_api_response
ignore_errors: true
- name: Report API status (informational only)
ansible.builtin.debug:
msg: >
API checks (informational only — not used for pass/fail):
Admin API: {{ admin_api_response.status | default('no response') }}.
Repo API: {{ repo_api_response.status | default('no response') }}.
{% if admin_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
Runner found in admin API.
{% endif %}
{% if repo_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
Runner found in repo API.
{% endif %}
rescue:
- name: API check failed
ansible.builtin.debug:
msg: "API verification skipped due to connection or permission error."
@@ -1,112 +0,0 @@
---
- name: Ensure keyrings directory exists (Debian/Ubuntu)
ansible.builtin.file:
path: "/etc/apt/keyrings"
state: directory
mode: "0755"
when: ansible_facts['os_family'] == 'Debian'
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
ansible.builtin.shell: |
set -o pipefail
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" | gpg --dearmor --yes -o {{ docker_gpg_key_path }}
args:
creates: "{{ docker_gpg_key_path }}"
executable: /bin/bash
when: ansible_facts['os_family'] == 'Debian'
- name: Add Docker APT repository (Debian/Ubuntu)
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/docker.list
content: "{{ docker_apt_source_line }}\n"
mode: "0644"
register: docker_apt_repo
when: ansible_facts['os_family'] == 'Debian'
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
when:
- ansible_facts['os_family'] == 'Debian'
- docker_apt_repo is changed
- name: Install rootless Docker dependencies (Debian/Ubuntu)
ansible.builtin.apt:
name:
- uidmap
- slirp4netns
- fuse-overlayfs
- docker-ce
- docker-ce-cli
- docker-ce-rootless-extras
- containerd.io
- docker-compose-plugin
- rsync
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Update pacman cache (Arch Linux)
community.general.pacman:
update_cache: true
when: ansible_facts['os_family'] == 'Archlinux'
changed_when: false
- name: Install rootless Docker dependencies (Arch Linux)
community.general.pacman:
name:
- docker
- docker-compose
- slirp4netns
- fuse-overlayfs
- rsync
state: present
when: ansible_facts['os_family'] == 'Archlinux'
- name: Check if rootless Docker is already set up
ansible.builtin.stat:
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
register: rootless_docker_check
- name: Set up rootless Docker for runner user
ansible.builtin.command: dockerd-rootless-setuptool.sh install
args:
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when:
- docker_rootless_setup
- not rootless_docker_check.stat.exists
- name: Start rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user start docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when: docker_rootless_setup
- name: Enable rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user enable docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when: docker_rootless_setup
- name: Wait for rootless Docker daemon to be ready
ansible.builtin.command: docker version
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 }}"
register: docker_ready
until: docker_ready.rc == 0
retries: 10
delay: 2
changed_when: false
when: docker_rootless_setup
@@ -0,0 +1,72 @@
---
gitea_runner_version: "2.0.1"
gitea_runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-26.04"
gitea_runner_skip_registration: false
# Per-runner user (rootless isolation)
gitea_runner_user_prefix: "grm-"
gitea_runner_base_home: "/home"
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ gitea_runner_name }}"
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
# Base paths (instance-scoped via gitea_runner_name)
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
gitea_runner_base_config_dir: "/etc/gitea-runner"
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ gitea_runner_name }}"
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ gitea_runner_name }}"
gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
# Prune configuration
gitea_runner_prune_until: "24h"
gitea_runner_prune_schedule: "daily"
gitea_runner_prune_label: "gitea-runner=true"
# Service configuration
gitea_runner_service_restart_sec: "5"
# Health check configuration
gitea_runner_healthcheck_interval: "5min"
gitea_runner_healthcheck_boot_delay: "2min"
gitea_runner_healthcheck_disk_threshold: 85
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
# 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.
gitea_runner_admin_token: ""
# Removal defaults
gitea_runner_remove_systemd_template: true
gitea_runner_remove_user: true
# Runner configuration
gitea_runner_log_level: "info"
gitea_runner_container_label: "gitea-runner=true"
gitea_runner_file: ".runner"
# Docker installation (for rootless dependencies)
gitea_runner_docker_gpg_key_path: "/etc/apt/keyrings/docker.gpg"
gitea_runner_docker_apt_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
gitea_runner_docker_apt_source_line: >-
deb [arch={{ gitea_runner_docker_apt_arch }} signed-by={{ gitea_runner_docker_gpg_key_path }}]
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
{{ ansible_facts['distribution_release'] }} stable
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
gitea_runner_docker_rootless_setup: true
# Rootless Docker helper scripts (dockerd-rootless-setuptool.sh / dockerd-rootless.sh).
# Arch Linux's "docker" package does not ship these (unlike Debian's docker-ce-rootless-extras),
# and no official Arch package provides them. They are fetched from the upstream moby/moby
# "contrib/" directory at the git ref below. The scripts are stable bash wrappers that are
# version-agnostic with respect to the dockerd binary, so a pinned ref is safe.
gitea_runner_rootless_scripts_ref: "v28.5.1"
# Install dir MUST match the location of the "docker" / "dockerd" / "rootlesskit" binaries so
# 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"
# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet)
gitea_runner_docker_ipv6_cidr: "fd00:dead:beef::/48"
@@ -9,4 +9,4 @@
when: when:
- ansible_facts is defined - ansible_facts is defined
- ansible_facts['service_mgr'] | default('') == 'systemd' - ansible_facts['service_mgr'] | default('') == 'systemd'
- docker_rootless_setup - gitea_runner_docker_rootless_setup
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "molecule-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "molecule-test-runner" gitea_runner_name: "molecule-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "deregister-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "deregister-test-runner" gitea_runner_name: "deregister-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -13,7 +13,7 @@
ansible.builtin.copy: ansible.builtin.copy:
dest: "{{ gitea_runner_data_dir }}/.runner" dest: "{{ gitea_runner_data_dir }}/.runner"
content: | content: |
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"} {"id": 1, "uuid": "test-uuid-1234", "name": "{{ gitea_runner_name }}", "address": "http://localhost:3000"}
owner: "{{ gitea_runner_service_user }}" owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}" group: "{{ gitea_runner_service_user }}"
mode: "0644" mode: "0644"
@@ -22,12 +22,12 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "deregister-test-runner" gitea_runner_name: "deregister-test-runner"
registration_token: "fake-token-for-testing" registration_token: "fake-token-for-testing"
gitea_url: "http://localhost:3000" gitea_url: "http://localhost:3000"
skip_runner_registration: false gitea_runner_skip_registration: false
tasks: tasks:
- name: Include deregistration tasks - name: Include deregistration tasks
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: deregister.yml tasks_from: deregister.yml
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "deregister-test-runner" gitea_runner_name: "deregister-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -12,12 +12,12 @@
- name: Check registration file was removed - name: Check registration file was removed
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner" path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_stat register: gitea_runner_file_stat
- name: Assert registration file no longer exists - name: Assert registration file no longer exists
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- not runner_file_stat.stat.exists - not gitea_runner_file_stat.stat.exists
fail_msg: "Registration file (.runner) was not removed by deregistration" fail_msg: "Registration file (.runner) was not removed by deregistration"
- name: Check systemd user service still exists - name: Check systemd user service still exists
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "lifecycle-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "lifecycle-test-runner" gitea_runner_name: "lifecycle-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -23,7 +23,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "lifecycle-test-runner" gitea_runner_name: "lifecycle-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "lifecycle-test-runner" gitea_runner_name: "lifecycle-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -5,11 +5,11 @@
vars: vars:
gitea_url: "http://localhost:3000" gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing" registration_token: "fake-token-for-testing"
runner_name: "molecule-runner-a" gitea_runner_name: "molecule-runner-a"
skip_runner_registration: true gitea_runner_skip_registration: true
docker_rootless_setup: false gitea_runner_docker_rootless_setup: false
roles: roles:
- role: gitea-runner - role: gitea_runner
- name: Converge second runner instance - name: Converge second runner instance
hosts: all hosts: all
@@ -17,8 +17,8 @@
vars: vars:
gitea_url: "http://localhost:3000" gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing" registration_token: "fake-token-for-testing"
runner_name: "molecule-runner-b" gitea_runner_name: "molecule-runner-b"
skip_runner_registration: true gitea_runner_skip_registration: true
docker_rootless_setup: false gitea_runner_docker_rootless_setup: false
roles: roles:
- role: gitea-runner - role: gitea_runner
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "remove-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -2,7 +2,7 @@
- name: Remove runner via remove-runner playbook - name: Remove runner via remove-runner playbook
ansible.builtin.import_playbook: "../../../../remove-runner.yml" ansible.builtin.import_playbook: "../../../../remove-runner.yml"
vars: vars:
runner_name: "remove-test-runner" gitea_runner_name: "remove-test-runner"
registration_token: "fake-token-for-testing" registration_token: "fake-token-for-testing"
gitea_url: "http://localhost:3000" gitea_url: "http://localhost:3000"
skip_runner_registration: true gitea_runner_skip_registration: true
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "remove-test-runner" gitea_runner_name: "remove-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "template-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "template-test-runner" gitea_runner_name: "template-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -0,0 +1,12 @@
---
- name: Converge
hosts: all
become: true
vars:
gitea_url: "http://localhost:3000"
registration_token: "fake-token-for-testing"
gitea_runner_name: "update-test-runner"
gitea_runner_skip_registration: true
gitea_runner_docker_rootless_setup: false
roles:
- role: gitea_runner
@@ -3,9 +3,9 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "update-test-runner" gitea_runner_name: "update-test-runner"
tasks: tasks:
- name: Include update tasks - name: Include update tasks
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: update_runner.yml tasks_from: update_runner.yml
@@ -3,7 +3,7 @@
hosts: all hosts: all
become: true become: true
vars: vars:
runner_name: "update-test-runner" gitea_runner_name: "update-test-runner"
pre_tasks: pre_tasks:
- name: Load role defaults - name: Load role defaults
ansible.builtin.include_vars: ansible.builtin.include_vars:
@@ -0,0 +1,58 @@
---
- name: Check if runner registration file exists
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner"
register: gitea_runner_file_stat
- name: Read runner registration file
ansible.builtin.slurp:
src: "{{ gitea_runner_data_dir }}/.runner"
register: gitea_runner_file_content
when: gitea_runner_file_stat.stat.exists | default(false) | bool
- name: Parse runner registration data
ansible.builtin.set_fact:
gitea_runner_reg: >
{{ (gitea_runner_file_content.content | b64decode | from_json)
if (gitea_runner_file_content is defined and gitea_runner_file_content.content is defined)
else {} }}
when: gitea_runner_file_stat.stat.exists | default(false) | bool
- name: Deregister runner from Gitea via API
ansible.builtin.command: >
curl -sf --connect-timeout 5 --max-time 10 -X DELETE
-H "Authorization: token {{ gitea_runner_admin_token | default(registration_token) }}"
"{{ gitea_url }}/api/v1/admin/actions/runners/{{ gitea_runner_reg.id }}"
args:
chdir: "{{ gitea_runner_data_dir }}"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
when:
- gitea_runner_file_stat.stat.exists | default(false) | bool
- not gitea_runner_skip_registration
- gitea_runner_reg.id is defined
register: gitea_runner_deregister_output
changed_when: gitea_runner_deregister_output.rc == 0
failed_when: false
- name: Warn if deregistration failed
ansible.builtin.debug:
msg: >-
WARNING: Runner deregistration from Gitea failed (rc={{ gitea_runner_deregister_output.rc | default('N/A') }}).
The runner entry may remain in Gitea's admin UI as offline.
Use an admin token (gitea_runner_admin_token var) to enable automatic cleanup,
or remove it manually from {{ gitea_url }}/-/admin/actions/runners
when:
- gitea_runner_file_stat.stat.exists | default(false) | bool
- not gitea_runner_skip_registration
- gitea_runner_deregister_output is defined
- gitea_runner_deregister_output.rc | default(1) != 0
- name: Remove runner registration file
ansible.builtin.file:
path: "{{ gitea_runner_data_dir }}/.runner"
state: absent
when: gitea_runner_file_stat.stat.exists | default(false) | bool
@@ -31,8 +31,8 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
- name: Enable and start healthcheck user timer - name: Enable and start healthcheck user timer
ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer
@@ -42,5 +42,5 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
@@ -15,7 +15,7 @@
- name: Include registration - name: Include registration
ansible.builtin.include_tasks: register.yml ansible.builtin.include_tasks: register.yml
when: not skip_runner_registration when: not gitea_runner_skip_registration
- name: Include service setup - name: Include service setup
ansible.builtin.include_tasks: service.yml ansible.builtin.include_tasks: service.yml
@@ -0,0 +1,102 @@
---
- name: Check runner registration file exists
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner"
register: gitea_runner_file_stat
- name: Read runner registration file
ansible.builtin.slurp:
src: "{{ gitea_runner_data_dir }}/.runner"
register: gitea_runner_file_content
when: gitea_runner_file_stat.stat.exists | default(false) | bool
- name: Parse runner registration data
ansible.builtin.set_fact:
gitea_runner_reg: >
{{ (gitea_runner_file_content.content | b64decode | from_json)
if (gitea_runner_file_content is defined and gitea_runner_file_content.content is defined)
else {} }}
when: gitea_runner_file_stat.stat.exists | default(false) | bool
- name: Verify runner user service 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 }}"
register: gitea_runner_service_check
changed_when: false
when:
- gitea_runner_systemd_available.stat.exists
- gitea_runner_docker_rootless_setup
- name: Validate runner installation
ansible.builtin.fail:
msg: >
Runner '{{ gitea_runner_name }}' is not properly installed:
{% if not (gitea_runner_file_stat.stat.exists | default(false)) %}
- Registration file (.runner) is missing. Registration may have failed.
{% endif %}
{% if gitea_runner_docker_rootless_setup and not (gitea_runner_service_check.stdout | default('') | trim) == 'active' %}
- Systemd user service is not active.
{% endif %}
when: >
not (gitea_runner_file_stat.stat.exists | default(false))
or (gitea_runner_docker_rootless_setup and not (gitea_runner_service_check.stdout | default('') | trim) == 'active')
- name: Report runner status
ansible.builtin.debug:
msg: >
Runner '{{ gitea_runner_name }}' is installed and running.
Registered: {{ gitea_runner_file_stat.stat.exists | default(false) }}
{% if gitea_runner_reg.id is defined %}Runner ID: {{ gitea_runner_reg.id }}{% endif %}
{% if gitea_runner_reg.uuid is defined %}UUID: {{ gitea_runner_reg.uuid }}{% endif %}
{% if gitea_runner_reg.address is defined %}Gitea: {{ gitea_runner_reg.address }}{% endif %}
Service: {{ gitea_runner_service_check.stdout | default('unknown') | trim }}
- name: Optional Gitea API verification
when:
- gitea_url is defined
- gitea_runner_admin_token is defined
- gitea_runner_admin_token | length > 0
block:
- name: Check admin runners API
ansible.builtin.uri:
url: "{{ gitea_url }}/api/v1/admin/runners"
headers:
Authorization: "token {{ gitea_runner_admin_token }}"
method: GET
status_code: [200, 401, 403, 404]
return_content: true
body_format: json
register: gitea_runner_admin_api_response
ignore_errors: true
- name: Check repo runners API
ansible.builtin.uri:
url: "{{ gitea_url }}/api/v1/repos/{{ gitea_runner_test_repo | default('oblachno-oss/grm') }}/actions/runners"
headers:
Authorization: "token {{ gitea_runner_admin_token }}"
method: GET
status_code: [200, 401, 403, 404]
return_content: true
body_format: json
register: gitea_runner_repo_api_response
ignore_errors: true
- name: Report API status (informational only)
ansible.builtin.debug:
msg: >
API checks (informational only — not used for pass/fail):
Admin API: {{ gitea_runner_admin_api_response.status | default('no response') }}.
Repo API: {{ gitea_runner_repo_api_response.status | default('no response') }}.
{% if gitea_runner_admin_api_response.json.runners | default([]) | selectattr('name', 'equalto', gitea_runner_name) | list | length > 0 %}
Runner found in admin API.
{% endif %}
{% if gitea_runner_repo_api_response.json.runners | default([]) | selectattr('name', 'equalto', gitea_runner_name) | list | length > 0 %}
Runner found in repo API.
{% endif %}
rescue:
- name: API check failed
ansible.builtin.debug:
msg: "API verification skipped due to connection or permission error."
@@ -19,4 +19,4 @@
- name: Include integration test - name: Include integration test
ansible.builtin.include_tasks: integration_test.yml ansible.builtin.include_tasks: integration_test.yml
when: not skip_runner_registration when: not gitea_runner_skip_registration
@@ -23,8 +23,8 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
- name: Enable and start docker-prune user timer - name: Enable and start docker-prune user timer
ansible.builtin.command: systemctl --user enable --now docker-prune.timer ansible.builtin.command: systemctl --user enable --now docker-prune.timer
@@ -34,5 +34,5 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
@@ -10,15 +10,15 @@
- name: Check if runner is already registered - name: Check if runner is already registered
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ gitea_runner_data_dir }}/.runner" path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_registered register: gitea_runner_registered
- name: Register runner with Gitea - name: Register runner with Gitea
ansible.builtin.command: > ansible.builtin.command: >
{{ gitea_runner_binary_path }} register {{ gitea_runner_binary_path }} register
--token {{ registration_token }} --token {{ registration_token }}
--name {{ runner_name }} --name {{ gitea_runner_name }}
--instance {{ gitea_url }} --instance {{ gitea_url }}
--labels {{ runner_labels }} --labels {{ gitea_runner_labels }}
--no-interactive --no-interactive
args: args:
chdir: "{{ gitea_runner_data_dir }}" chdir: "{{ gitea_runner_data_dir }}"
@@ -27,7 +27,7 @@
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
when: not runner_registered.stat.exists when: not gitea_runner_registered.stat.exists
register: register_output register: gitea_runner_register_output
changed_when: "'already exists' not in register_output.stdout | default('')" changed_when: "'already exists' not in gitea_runner_register_output.stdout | default('')"
timeout: 60 timeout: 60
@@ -6,14 +6,14 @@
- name: Resolve runner service user - name: Resolve runner service user
ansible.builtin.set_fact: ansible.builtin.set_fact:
gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ runner_name }}" gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ gitea_runner_name }}"
gitea_runner_base_data_dir: "/var/lib/gitea-runner" gitea_runner_base_data_dir: "/var/lib/gitea-runner"
gitea_runner_base_config_dir: "/etc/gitea-runner" gitea_runner_base_config_dir: "/etc/gitea-runner"
- name: Resolve runner data and config dirs - name: Resolve runner data and config dirs
ansible.builtin.set_fact: ansible.builtin.set_fact:
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}" gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ gitea_runner_name }}"
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}" gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ gitea_runner_name }}"
- name: Resolve runner service user UID - name: Resolve runner service user UID
ansible.builtin.getent: ansible.builtin.getent:
@@ -0,0 +1,248 @@
---
- name: Ensure keyrings directory exists (Debian/Ubuntu)
ansible.builtin.file:
path: "/etc/apt/keyrings"
state: directory
mode: "0755"
when: ansible_facts['os_family'] == 'Debian'
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
ansible.builtin.shell: |
set -o pipefail
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" \
| gpg --dearmor --yes -o {{ gitea_runner_docker_gpg_key_path }}
args:
creates: "{{ gitea_runner_docker_gpg_key_path }}"
executable: /bin/bash
when: ansible_facts['os_family'] == 'Debian'
- name: Add Docker APT repository (Debian/Ubuntu)
ansible.builtin.copy:
dest: /etc/apt/sources.list.d/docker.list
content: "{{ gitea_runner_docker_apt_source_line }}\n"
mode: "0644"
register: gitea_runner_docker_apt_repo
when: ansible_facts['os_family'] == 'Debian'
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
when:
- ansible_facts['os_family'] == 'Debian'
- gitea_runner_docker_apt_repo is changed
- name: Install rootless Docker dependencies (Debian/Ubuntu)
ansible.builtin.apt:
name:
- uidmap
- slirp4netns
- passt
- fuse-overlayfs
- docker-ce
- docker-ce-cli
- docker-ce-rootless-extras
- containerd.io
- docker-compose-plugin
- rsync
state: present
when: ansible_facts['os_family'] == 'Debian'
- name: Update pacman cache (Arch Linux)
community.general.pacman:
update_cache: true
when: ansible_facts['os_family'] == 'Archlinux'
changed_when: false
- name: Install rootless Docker dependencies (Arch Linux)
community.general.pacman:
name:
- docker
- docker-compose
- slirp4netns
- passt
- fuse-overlayfs
- rsync
# rootlesskit is the userspace networking/namespace driver for rootless Docker.
# It is NOT a dependency of the "docker" package on Arch and must be installed explicitly.
- rootlesskit
state: present
when: ansible_facts['os_family'] == 'Archlinux'
# The tun kernel module is required by rootlesskit (both slirp4netns and pasta
# drivers create a tap device inside a user namespace). On Arch Linux, CONFIG_TUN=m
# so the module must be loaded. If the running kernel doesn't match the installed
# kernel (e.g. after a pacman -Syu that upgraded linux but didn't reboot), modprobe
# will fail — in that case we warn but don't fail, as a reboot will fix it.
- name: Load tun kernel module for rootless networking
community.general.modprobe:
name: tun
state: present
ignore_errors: true
register: gitea_runner_tun_module
when: ansible_facts['os_family'] == 'Archlinux'
- name: Warn if tun module could not be loaded (kernel mismatch — reboot needed)
ansible.builtin.debug:
msg: >-
WARNING: Could not load the tun kernel module. This is likely because the
running kernel ({{ ansible_facts['kernel'] }}) does not match the installed
kernel modules. A reboot is required before rootless Docker can start.
when:
- ansible_facts['os_family'] == 'Archlinux'
- gitea_runner_tun_module is failed
# Arch Linux's "docker" package does not ship the rootless setup scripts (dockerd-rootless-setuptool.sh
# and dockerd-rootless.sh), unlike Debian/Ubuntu's docker-ce-rootless-extras. No official Arch package
# provides them, so fetch them from the upstream moby/moby contrib/ directory. They are installed
# alongside the docker binaries (/usr/bin) because dockerd-rootless-setuptool.sh derives its BIN
# directory from its own location and expects docker/dockerd/rootlesskit to be co-located there.
- name: Fetch rootless Docker setup scripts (Arch Linux)
ansible.builtin.get_url:
url: "https://raw.githubusercontent.com/moby/moby/{{ gitea_runner_rootless_scripts_ref }}/contrib/{{ item.name }}"
dest: "{{ gitea_runner_rootless_scripts_install_dir }}/{{ item.name }}"
mode: "0755"
owner: root
group: root
loop:
- name: dockerd-rootless-setuptool.sh
- name: dockerd-rootless.sh
when: ansible_facts['os_family'] == 'Archlinux'
- name: Check if rootless Docker is already set up
ansible.builtin.stat:
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
register: gitea_runner_rootless_docker_check
# Create the systemd override BEFORE running the setuptool so that when
# the setuptool starts docker.service, it picks up the pasta network driver
# instead of the default slirp4netns (which may fail on some kernels).
- name: Ensure systemd user override directory exists
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d"
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 network driver override
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_FLAGS=--ipv6"
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
- name: Set up rootless Docker for runner user
ansible.builtin.command: dockerd-rootless-setuptool.sh install
args:
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DOCKERD_ROOTLESS_ROOTLESSKIT_NET: "{{ gitea_runner_docker_rootless_net_driver }}"
when:
- gitea_runner_docker_rootless_setup
- not gitea_runner_rootless_docker_check.stat.exists
- name: Start rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user start docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when: gitea_runner_docker_rootless_setup
- name: Enable rootless Docker daemon (systemd user service)
ansible.builtin.command: systemctl --user enable docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when: gitea_runner_docker_rootless_setup
- name: Ensure Docker config directory exists
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
- name: Configure rootless Docker to use pasta with IPv6
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_FLAGS=--ipv6"
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
register: gitea_runner_docker_network_override
when: gitea_runner_docker_rootless_setup
- name: Reload systemd user daemon if network config changed
ansible.builtin.command: systemctl --user daemon-reload
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_docker_network_override is changed
- name: Configure rootless Docker daemon with IPv6 enabled
ansible.builtin.copy:
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
content: |
{
"ipv6": true,
"ip6tables": true,
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}"
}
mode: "0644"
owner: "{{ gitea_runner_service_user }}"
group: "{{ gitea_runner_service_user }}"
register: gitea_runner_docker_ipv6_config
when: gitea_runner_docker_rootless_setup
- name: Restart rootless Docker if config changed
ansible.builtin.command: systemctl --user restart docker
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true
when:
- gitea_runner_docker_rootless_setup
- gitea_runner_docker_ipv6_config is changed or gitea_runner_docker_network_override is changed
- name: Wait for rootless Docker daemon to be ready
ansible.builtin.command: docker version
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 }}"
register: gitea_runner_docker_ready
until: gitea_runner_docker_ready.rc == 0
retries: 10
delay: 2
changed_when: false
when: gitea_runner_docker_rootless_setup
@@ -15,8 +15,8 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
- name: Enable and start gitea-runner user service - name: Enable and start gitea-runner user service
ansible.builtin.command: systemctl --user enable --now gitea-runner ansible.builtin.command: systemctl --user enable --now gitea-runner
@@ -26,5 +26,5 @@
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
changed_when: true changed_when: true
when: when:
- systemd_available.stat.exists - gitea_runner_systemd_available.stat.exists
- docker_rootless_setup - gitea_runner_docker_rootless_setup
@@ -2,4 +2,4 @@
- name: Check if systemd is available - name: Check if systemd is available
ansible.builtin.stat: ansible.builtin.stat:
path: /run/systemd/system path: /run/systemd/system
register: systemd_available register: gitea_runner_systemd_available
@@ -9,6 +9,6 @@
environment: environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when: when:
- systemd_available.stat.exists | default(false) | bool - gitea_runner_systemd_available.stat.exists | default(false) | bool
- docker_rootless_setup - gitea_runner_docker_rootless_setup
changed_when: true changed_when: true
@@ -6,21 +6,21 @@
shell: /bin/bash shell: /bin/bash
system: true system: true
create_home: true create_home: true
register: runner_user register: gitea_runner_user
- name: Set runner UID fact - name: Set runner UID fact
ansible.builtin.set_fact: ansible.builtin.set_fact:
gitea_runner_uid: "{{ runner_user.uid }}" gitea_runner_uid: "{{ gitea_runner_user.uid }}"
- name: Check if lingering is already enabled - name: Check if lingering is already enabled
ansible.builtin.stat: ansible.builtin.stat:
path: "/var/lib/systemd/linger/{{ gitea_runner_service_user }}" path: "/var/lib/systemd/linger/{{ gitea_runner_service_user }}"
register: linger_stat register: gitea_runner_linger_stat
- name: Enable lingering for runner user - name: Enable lingering for runner user
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }} ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
changed_when: not linger_stat.stat.exists changed_when: not gitea_runner_linger_stat.stat.exists
when: systemd_available.stat.exists when: gitea_runner_systemd_available.stat.exists
- name: Ensure subuid entry for runner user - name: Ensure subuid entry for runner user
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
@@ -21,6 +21,6 @@
environment: environment:
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
register: docker_version_output register: gitea_runner_docker_version_output
changed_when: false changed_when: false
when: docker_rootless_setup when: gitea_runner_docker_rootless_setup
+4 -4
View File
@@ -6,12 +6,12 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Resolve runner UID - name: Resolve runner UID
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: resolve_uid.yml tasks_from: resolve_uid.yml
- name: Check if runner is already registered - name: Check if runner is already registered
@@ -21,11 +21,11 @@
- name: Include registration if not registered - name: Include registration if not registered
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: register.yml tasks_from: register.yml
when: when:
- not runner_registered.stat.exists - not runner_registered.stat.exists
- not skip_runner_registration | default(false) - not gitea_runner_skip_registration | default(false)
- name: Start gitea-runner user service - name: Start gitea-runner user service
ansible.builtin.command: systemctl --user start gitea-runner ansible.builtin.command: systemctl --user start gitea-runner
+2 -2
View File
@@ -6,12 +6,12 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Resolve runner UID - name: Resolve runner UID
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: resolve_uid.yml tasks_from: resolve_uid.yml
- name: Check systemd user service status - name: Check systemd user service status
+2 -2
View File
@@ -6,12 +6,12 @@
tasks: tasks:
- name: Include systemd availability check - name: Include systemd availability check
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: systemd_check.yml tasks_from: systemd_check.yml
- name: Resolve runner UID - name: Resolve runner UID
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: resolve_uid.yml tasks_from: resolve_uid.yml
- name: Stop gitea-runner user service - name: Stop gitea-runner user service
+1 -1
View File
@@ -6,5 +6,5 @@
tasks: tasks:
- name: Update runner - name: Update runner
ansible.builtin.include_role: ansible.builtin.include_role:
name: gitea-runner name: gitea_runner
tasks_from: update_runner.yml tasks_from: update_runner.yml
+6 -6
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE) [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki) [![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions) [![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases) [![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/06bbd782a8a1274ae025d54c8a4a0b2300cd099f/python.svg)](https://www.python.org/downloads/) [![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/bf77093ceedcbb44e60ebfdbd10ebf17643377d5/python.svg)](https://www.python.org/downloads/)
## Overview ## Overview
@@ -0,0 +1,105 @@
# Retrospective: CI Consolidation and devx Adoption
## Date
2026-07-12
## Context
The grm repo (Gitea Runner Manager) underwent CI workflow consolidation
and adopted the latest devx package (v0.40.0 → v0.40.1) during this
period. The self-approval fallback fix in devx v0.40.1 required passing
`CI_GITEA_API_TOKEN` to the approval step in grm's CI workflow. This
retrospective covers grm v0.17.2 through v0.18.0.
## Scope
PRs: GRM-144 (IPv6/pasta), GRM-145 (devx v0.40.0 adoption), GRM-146 (CI
consolidation). Plus the self-approval fallback cherry-pick. ~12 commits.
## Timeline of Key Events
| Event | Description |
|-------|-------------|
| GRM-144 merged | IPv6 support via pasta network driver (v0.18.0) |
| GRM-145 merged | Adopted devx v0.40.0, removed redundant crypto/vault wrappers |
| GRM-146 merged | Consolidated CI and post-merge workflows (7→2 jobs) |
| devx v0.40.1 bump | Cherry-picked self-approval fallback into grm CI |
No CI failures were specific to grm during this period. The grm CI
passed cleanly on all runs. The only issue was the cross-repo
self-approval bug (inherited from devx), which was fixed by bumping
devx to v0.40.1 and passing `CI_GITEA_API_TOKEN` to the approval step.
## What Served Us Well
- **Clean CI consolidation.** GRM-146 merged 7 CI jobs into 2
(validate + molecule-tests) without any CI failures. The
consolidation pattern was already proven in devx (DEVX-126), so the
application to grm was straightforward.
- **devx adoption was smooth.** GRM-145 adopted devx v0.40.0 and removed
redundant crypto/vault wrappers. The refactoring was clean — no test
failures, no coverage drops.
- **Molecule tests stable.** All 6 molecule scenarios passed on every
CI run. The pasta networking change (GRM-144) was well-tested with
molecule before merge.
- **Pre-push hook caught missing Vikunja tasks.** The pre-push hook
validates Vikunja task existence, preventing pushes without
corresponding tasks.
## What Could Be Improved
### 1. Cross-Repo Dependency Propagation
When devx v0.40.1 was released with the self-approval fix, grm needed
to bump its devx version and update the CI workflow to pass
`CI_GITEA_API_TOKEN`. This was a manual process — there's no automated
mechanism to detect that a devx release affects downstream repos.
**Impact:** The self-approval fix was in devx for ~30 min before grm
was updated. If the timing had been different, grm PRs could have been
blocked.
**Lesson:** When releasing a devx fix that affects CI workflows in
downstream repos, bump devx in all repos in the same session. Consider
a "dependabot" style check that flags outdated devx versions.
### 2. No Repo-Specific Retrospective Directory
The grm repo didn't have a `docs/retrospectives/` directory until now.
Previous retrospectives were only in the infra repo. This meant grm-
specific lessons weren't being captured.
**Impact:** Low — grm had fewer issues during this period. But going
forward, grm-specific learnings should be documented here.
**Fix:** Created `docs/retrospectives/` directory with this
retrospective.
## Improvements Implemented
### 1. CI Workflow Consolidation (MEDIUM impact)
Merged 7 CI jobs into 2 (validate + molecule-tests), matching the
pattern established in devx. Reduced runner overhead by ~4 min per CI
run.
### 2. devx v0.40.1 Adoption (HIGH impact)
Bumped devx to v0.40.1, picking up the self-approval fallback fix.
Updated CI workflow to pass `CI_GITEA_API_TOKEN` to the approval step.
### 3. IPv6 Support via Pasta (MEDIUM impact)
GRM-144 enabled IPv6 in rootless Docker via the pasta network driver,
replacing the previous slirp4netns setup. This improves network
performance and enables IPv6 connectivity for runner containers.
## Action Items for Future Sessions
1. **Bump devx in all downstream repos when a CI-affecting fix is
released.** Don't leave repos on stale devx versions.
2. **Document grm-specific lessons in this retrospective directory.**
Don't rely on the infra retrospective to cover grm issues.
3. **When consolidating CI workflows, verify that all status checks
referenced by branch protection are still present.** The
consolidation renamed `quality` to `validate`, requiring a branch
protection update.
+27 -8
View File
@@ -3,7 +3,7 @@
GRM consists of two layers: GRM consists of two layers:
1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. 1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea. 2. **Ansible Role** (`ansible/roles/gitea_runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea.
## High-Level Design ## High-Level Design
@@ -26,7 +26,7 @@ The Ansible role handles all remote state: user creation, package installation,
grm install <host> grm install <host>
└── RunnerManager.install() └── RunnerManager.install()
└── ansible-playbook ansible/install-runner.yml └── ansible-playbook ansible/install-runner.yml
└── role: gitea-runner └── role: gitea_runner
├── user_setup.yml (create per-runner system user + lingering) ├── user_setup.yml (create per-runner system user + lingering)
├── rootless_docker.yml (rootless Docker setup under runner user) ├── rootless_docker.yml (rootless Docker setup under runner user)
├── install_runner.yml (download binary, config, register, service) ├── install_runner.yml (download binary, config, register, service)
@@ -44,7 +44,7 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
- `install_runner.yml` handles: download, config, validate, register, service - `install_runner.yml` handles: download, config, validate, register, service
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates) - `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
- `systemctl --user` tasks must be guarded by `docker_rootless_setup` - `systemctl --user` tasks must be guarded by `docker_rootless_setup`
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files) - Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
### Ansible task files ### Ansible task files
@@ -53,7 +53,7 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
| `main.yml` | Entry point — includes all other task files in order | | `main.yml` | Entry point — includes all other task files in order |
| `systemd_check.yml` | Verifies systemd is available on the target host | | `systemd_check.yml` | Verifies systemd is available on the target host |
| `user_setup.yml` | Creates the per-runner system user, enables lingering, configures subuid/subgid, creates data and config directories | | `user_setup.yml` | Creates the per-runner system user, enables lingering, configures subuid/subgid, creates data and config directories |
| `rootless_docker.yml` | Installs Docker packages (apt for Debian/Ubuntu, pacman for Arch), runs `dockerd-rootless-setuptool.sh install`, starts and enables the rootless Docker daemon | | `rootless_docker.yml` | Installs Docker packages (apt for Debian/Ubuntu, pacman for Arch), provisions the rootless setup scripts on Arch (not shipped by the `docker` package), runs `dockerd-rootless-setuptool.sh install`, starts and enables the rootless Docker daemon |
| `install_runner.yml` | Downloads the gitea_runner binary, creates the config file, validates the binary, registers the runner with Gitea, creates and starts the systemd user service | | `install_runner.yml` | Downloads the gitea_runner binary, creates the config file, validates the binary, registers the runner with Gitea, creates and starts the systemd user service |
| `download_gitea_runner.yml` | Downloads the gitea_runner binary from GitHub releases | | `download_gitea_runner.yml` | Downloads the gitea_runner binary from GitHub releases |
| `validate.yml` | Validates the downloaded binary | | `validate.yml` | Validates the downloaded binary |
@@ -83,8 +83,8 @@ Each runner runs as a systemd user service under a dedicated system user (`grm-<
- **User**: `grm-<name>` (dedicated system user with lingering enabled) - **User**: `grm-<name>` (dedicated system user with lingering enabled)
- **Home**: `/home/grm-<name>/` - **Home**: `/home/grm-<name>/`
- **Data**: `/var/lib/gitea-runner/<name>/` - **Data**: `/var/lib/gitea_runner/<name>/`
- **Config**: `/etc/gitea-runner/<name>/` - **Config**: `/etc/gitea_runner/<name>/`
- **Service**: `gitea-runner.service` (systemd user service) - **Service**: `gitea-runner.service` (systemd user service)
- **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner) - **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner)
- **subuid/subgid**: `grm-<name>:100000:65536` (user namespace mapping) - **subuid/subgid**: `grm-<name>:100000:65536` (user namespace mapping)
@@ -100,7 +100,7 @@ flowchart TD
EXEC["Executor<br/>executor.py"] EXEC["Executor<br/>executor.py"]
REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"] REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"]
ANS["ansible-playbook subprocess"] ANS["ansible-playbook subprocess"]
ROLE["Ansible Role<br/>ansible/roles/gitea-runner/"] ROLE["Ansible Role<br/>ansible/roles/gitea_runner/"]
USER["user_setup.yml<br/>create system user + lingering"] USER["user_setup.yml<br/>create system user + lingering"]
DOCKER["rootless_docker.yml<br/>rootless Docker setup"] DOCKER["rootless_docker.yml<br/>rootless Docker setup"]
INSTALL["install_runner.yml<br/>download, config, register, service"] INSTALL["install_runner.yml<br/>download, config, register, service"]
@@ -173,11 +173,30 @@ Each runner operates under a dedicated unprivileged system user. The Docker daem
- User namespace mapping via `/etc/subuid` and `/etc/subgid` (range: 100000-165535) - User namespace mapping via `/etc/subuid` and `/etc/subgid` (range: 100000-165535)
- Rootless Docker socket at `/run/user/<UID>/docker.sock` - Rootless Docker socket at `/run/user/<UID>/docker.sock`
- `slirp4netns` for user-mode networking - `pasta` for user-mode networking with IPv6 support (replaces `slirp4netns`, which lacks outgoing IPv6)
- `fuse-overlayfs` for rootless container storage - `fuse-overlayfs` for rootless container storage
The rootless Docker daemon is configured via a systemd user override
(`docker.service.d/override.conf`) that sets:
- `DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta` — use pasta as the network driver
- `DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit` — pasta's native port forwarding
- `DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6` — enable IPv6 routing
The daemon.json enables IPv6 with a ULA subnet (`fd00:dead:beef::/48`)
for container addressing. This ensures runner containers can reach
both IPv4 and IPv6 services (e.g., the Gitea registry) without
per-workaround DNS hacks.
Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering. Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering.
#### Platform-specific rootless provisioning
The rootless setup scripts (`dockerd-rootless-setuptool.sh` and `dockerd-rootless.sh`) are provided differently per OS:
- **Debian/Ubuntu** — shipped by the `docker-ce-rootless-extras` package (installed via the Docker APT repo).
- **Arch Linux** — the `docker` package does **not** include these scripts, and no official Arch package provides them. The role fetches them from the upstream `moby/moby` `contrib/` directory at a pinned, overridable git ref (`gitea_runner_rootless_scripts_ref`, default `v28.5.1`) and installs them into `/usr/bin` — co-located with `docker`/`dockerd`/`rootlesskit`, which is required because `dockerd-rootless-setuptool.sh` derives its `BIN` directory from its own location and expects those binaries alongside it. The `rootlesskit` package (a required rootless runtime dependency that is not pulled in by Arch's `docker` package) is also installed explicitly.
### Secret handling ### Secret handling
Registration tokens and admin API tokens are never exposed on the command line. The `RunnerManager._extra_vars_file()` context manager: Registration tokens and admin API tokens are never exposed on the command line. The `RunnerManager._extra_vars_file()` context manager:
+42 -33
View File
@@ -6,10 +6,9 @@ GRM uses a fully automated CI/CD pipeline built on Gitea Actions. Every change t
| Workflow | Trigger | Purpose | | Workflow | Trigger | Purpose |
|----------|---------|---------| |----------|---------|---------|
| `ci.yml` | PR opened/synchronized | Quality checks (lint, test, coverage) + molecule tests | | `ci.yml` | PR opened/synchronized | Validate (lint, test, coverage, detect-changes, release-dry-run, pr-review, discover-runners) + molecule tests |
| `auto-merge.yml` | PR labeled `ready-to-merge` | Validates and squash-merges the PR | | `auto-merge.yml` | PR labeled `ready-to-merge` | Validates and squash-merges the PR |
| `post-merge.yml` | Push to `master` | Release, wiki sync, badges, Vikunja task update | | `post-merge.yml` | Push to `master` | Detect-and-configure + release-and-maintain (release, publish, wiki sync, badges, Vikunja task update) |
| `publish.yml` | Tag push (`v*`) | Build and publish package to PyPI, create Gitea release |
Every change to master goes through a mandatory PR workflow. No exceptions. Every change to master goes through a mandatory PR workflow. No exceptions.
@@ -101,14 +100,14 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
3. Wait for all CI checks to pass 3. Wait for all CI checks to pass
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated) 4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
5. The post-merge workflow marks the Vikunja task as done 5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes 6. The release-and-maintain job automatically versions, tags, and publishes
### 9. Post-Merge Automation ### 9. Post-Merge Automation
After the squash-merge: After the squash-merge:
- The **post-merge workflow** (`.gitea/workflows/post-merge.yml`) triggers on push to `master` and runs `devx.ci.post_merge` to mark the Vikunja task as done, extracting the task ID from the merge commit message. - The **post-merge workflow** (`.gitea/workflows/post-merge.yml`) triggers on push to `master`. The `detect-and-configure` job configures the repo and detects the commit type. The `release-and-maintain` job then runs the release, publish, sync-wiki, badges, and Vikunja steps as appropriate.
- The **release workflow** (`.gitea/workflows/release.yml`) triggers on push to `master` and automatically versions, tags, and publishes (see below). - The **release step** (in the `release-and-maintain` job) automatically versions, tags, and publishes (see below).
## Branch Protection (Required Gitea Settings) ## Branch Protection (Required Gitea Settings)
@@ -116,42 +115,46 @@ Configure the following branch protection rules for `master` in Gitea repo setti
- **Require pull request**: No direct pushes to master - **Require pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **Require approval review**: At least 1 `APPROVE` review before merge
- **Require status checks**: CI quality + molecule tests must pass - **Require status checks**: CI validate + molecule tests must pass
- **Block force pushes**: No history rewriting on master - **Block force pushes**: No history rewriting on master
The auto-merge workflow enforces the APPROVE review check programmatically as a defense-in-depth measure, but branch protection is the primary gate. The auto-merge workflow enforces the APPROVE review check programmatically as a defense-in-depth measure, but branch protection is the primary gate.
## CI Path Filtering ## CI Path Filtering
The CI workflow (`.gitea/workflows/ci.yml`) includes a `detect-changes` job that checks whether any files under `ansible/` or `.ansible-lint` have changed. If no Ansible files are changed, molecule tests are skipped — this prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. The CI workflow (`.gitea/workflows/ci.yml`) includes a `detect-changes` step in the `validate` job that checks whether any files under `ansible/` or `.ansible-lint` have changed. If no Ansible files are changed, molecule tests are skipped — this prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness.
The `detect-changes` job: The `detect-changes` step:
- For pull requests: compares `origin/master` against the PR head SHA - For pull requests: compares `origin/master` against the PR head SHA
- For pushes to master: compares `HEAD~1` against `HEAD` - For pushes to master: compares `HEAD~1` against `HEAD`
- Outputs `ansible-changed` as `true` or `false` - Outputs `ansible-changed` as `true` or `false`
The `molecule-tests` job depends on both `quality` and `detect-changes`, and only runs if `ansible-changed == 'true'`. The `molecule-tests` job depends on the `validate` job (which includes the `detect-changes` step), and only runs if `ansible-changed == 'true'`.
CI triggers only on `opened` and `synchronize` PR events (not `labeled`). CI triggers only on `opened` and `synchronize` PR events (not `labeled`).
## CI Quality Job ## CI Validate Job
The `quality` job in `.gitea/workflows/ci.yml` runs: The `validate` job in `.gitea/workflows/ci.yml` consolidates the former quality, detect-changes, release-dry-run, pre-merge-check, pr-review, and discover-runners jobs into a single job. It runs:
1. `make setup` — full environment setup 1. `make setup` — full environment setup
2. `make lint-all` — ruff + pyright + bandit + ansible-lint + checkmake 2. `make lint-all` — ruff + pyright + bandit + ansible-lint + checkmake
3. `make pytest-cov` — unit tests with 100% coverage enforcement 3. `make pytest-cov` — unit tests with 100% coverage enforcement
4. `python -m devx.tools.check_test_speed --max-seconds 10` — verify unit tests run fast 4. `python -m devx.tools.check_test_speed --max-seconds 10` — verify unit tests run fast
5. `PYTHONPATH=src python -m devx.ci.release --dry-run` — release dry-run validation 5. `PYTHONPATH=src python -m devx.ci.release --dry-run` — release dry-run validation (release-dry-run step)
6. Pre-merge validation step — validates branch format, PR title, and Vikunja task match
7. `detect-changes` step — checks whether Ansible files changed (gates molecule tests)
8. `pr-review` step — automated PR review via `devx.ci.pr_review`
9. `discover-runners` step — dynamic runner discovery for molecule tests (conditional on ansible-changed)
## Automated Release Pipeline ## Automated Release Pipeline
After a PR is merged to master, the release pipeline runs automatically. After a PR is merged to master, the release pipeline runs automatically.
### Release Workflow (`.gitea/workflows/release.yml`) ### Release Step (in the release-and-maintain job)
- Triggers on push to `master` - Runs as a conditional step in the `release-and-maintain` job (skipped for release commits)
- Sets up full dev environment (`make setup`) so lint and tests can run - Sets up full dev environment (`make setup`) so lint and tests can run
- Installs git-cliff (version 2.13.0) - Installs git-cliff (version 2.13.0)
- Configures git as `grm-ci-bot` - Configures git as `grm-ci-bot`
@@ -169,9 +172,10 @@ After a PR is merged to master, the release pipeline runs automatically.
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits - Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
- On failure, creates a Gitea issue via `devx.ci.notify_failure` - On failure, creates a Gitea issue via `devx.ci.notify_failure`
### Publish Workflow (`.gitea/workflows/publish.yml`) ### Publish Step (in the release-and-maintain job)
- Triggers on tag push (`v*`) - Runs as a conditional step in the `release-and-maintain` job (only if the release step created a tag)
- Checks out the release tag within the same job
- Installs git-cliff (version 2.13.0) - Installs git-cliff (version 2.13.0)
- Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`) - Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`)
- Validates `PYPI_TOKEN` is set (warns if missing) - Validates `PYPI_TOKEN` is set (warns if missing)
@@ -190,12 +194,17 @@ After a PR is merged to master, the release pipeline runs automatically.
### Post-Merge Workflow (`.gitea/workflows/post-merge.yml`) ### Post-Merge Workflow (`.gitea/workflows/post-merge.yml`)
- Triggers on push to `master` - Triggers on push to `master`
- Consolidates release, wiki sync, badge generation, and Vikunja task updates into a single workflow - Consolidated from 7 jobs into 2 jobs to reduce runner overhead
- **detect-type** — Runs `devx.ci.detect_release_commit` to check if the commit is a release commit (`release: vX.Y.Z`). All subsequent jobs skip for release commits (the `[skip ci]` tag also prevents re-triggering). - **detect-and-configure** — Configures repo (branch protection, labels), detects release commit, validates commit message. Outputs `is-release` and `is-automated` for the next job.
- **release** — Runs `devx.ci.release` (see Automated Release Pipeline below) - **detect-type step** — Runs `devx.ci.detect_release_commit` to check if the commit is a release commit (`release: vX.Y.Z`). All subsequent steps skip for release commits (the `[skip ci]` tag also prevents re-triggering).
- **sync-wiki** — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki` - **validate-commit-msg step** — Validates the commit message follows conventional commit format.
- **badges** — Generates and pushes quality badge SVGs to the `badges` branch via `devx.ci.push_badges`. Runs after the release job (even if release fails or is skipped) so the version badge always reflects the latest state. - **configure-repo step** — Runs `devx.tools.configure_repo` to set up branch protection and labels.
- **vikunja** — Marks the corresponding Vikunja task as done via `devx.ci.post_merge` - **release-and-maintain** — Runs all post-merge maintenance as conditional steps:
- **release step** (if not a release commit) — Runs `devx.ci.release` (see Automated Release Pipeline below)
- **publish step** (if release created a tag) — Builds and publishes the package to the Gitea PyPI registry
- **sync-wiki step** (if not automated) — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki`
- **vikunja step** (if not automated) — Marks the corresponding Vikunja task as done via `devx.ci.post_merge`
- **badges step** (always) — Generates and pushes quality badge SVGs to the `badges` branch via `devx.ci.push_badges`. Runs even if release fails or is skipped so the version badge always reflects the latest state.
### Smart CI: User-Facing vs Workflow-Only Changes ### Smart CI: User-Facing vs Workflow-Only Changes
@@ -218,16 +227,16 @@ from accidentally skipping releases. Classification is config-driven via
**CI behavior based on classification:** **CI behavior based on classification:**
- **Molecule tests**: Only run when `ansible/` or `.ansible-lint` files change - **Molecule tests**: Only run when `ansible/` or `.ansible-lint` files change
- **Release dry-run**: Only runs when user-facing files change (separate `release-dry-run` job) - **Release dry-run**: Only runs when user-facing files change (release-dry-run step in the validate job)
- **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs - **Validate job** (lint, unit tests, coverage, doc-coverage): Always runs
- **Release workflow**: `release.py` calls `classify_changes` to check if any - **Release step**: `release.py` calls `classify_changes` to check if any
user-facing files changed since the last tag. If not, the release is skipped user-facing files changed since the last tag. If not, the release is skipped
entirely — no version bump, no tag, no publish. entirely — no version bump, no tag, no publish.
### Dynamic Runner Discovery ### Dynamic Runner Discovery
Molecule tests are distributed across available Gitea Actions runners Molecule tests are distributed across available Gitea Actions runners
dynamically. The `discover-runners` job runs `devx.molecule.discover_runners` which queries the Gitea API for dynamically. The `discover-runners` step in the `validate` job runs `devx.molecule.discover_runners` which queries the Gitea API for
registered runners at three levels (repo, org, instance) and generates registered runners at three levels (repo, org, instance) and generates
a matrix of runner indices. If the API query fails (e.g., no admin a matrix of runner indices. If the API query fails (e.g., no admin
access for instance-level runners), it falls back to the access for instance-level runners), it falls back to the
@@ -263,15 +272,15 @@ feature branches.
### Release Commit Detection ### Release Commit Detection
The `detect-type` job in the post-merge workflow runs The `detect-type` step in the `detect-and-configure` job (post-merge workflow) runs
`devx.ci.detect_release_commit` to check whether the latest commit `devx.ci.detect_release_commit` to check whether the latest commit
is a release commit (format: `release: vX.Y.Z`). When a release commit is a release commit (format: `release: vX.Y.Z`). When a release commit
is detected, all post-merge jobs (release, sync-wiki, badges, vikunja) is detected, all subsequent steps in the `release-and-maintain` job (release, publish, sync-wiki, vikunja)
are skipped — the tag push triggers the publish workflow instead. are skipped — the tag push triggers the publish step instead.
### Badge Generation and Push ### Badge Generation and Push
The `badges` job in the post-merge workflow runs The `badges` step in the `release-and-maintain` job (post-merge workflow) runs
`devx.ci.push_badges` which: `devx.ci.push_badges` which:
1. Fetches the latest master and hard-resets to it (picks up release commits) 1. Fetches the latest master and hard-resets to it (picks up release commits)
2. Generates quality badge SVG files via `devx.tools.generate_badges` 2. Generates quality badge SVG files via `devx.tools.generate_badges`
@@ -279,8 +288,8 @@ The `badges` job in the post-merge workflow runs
4. Copies SVG files to the branch root 4. Copies SVG files to the branch root
5. Force-pushes the branch to the remote 5. Force-pushes the branch to the remote
The badges job depends on the `release` job and uses `if: always()` so it The badges step runs with `if: always()` so it
runs even if release fails or is skipped. This ensures the version badge runs even if the release step fails or is skipped. This ensures the version badge
always reflects the actual state of the repository after any release always reflects the actual state of the repository after any release
commits have been pushed. commits have been pushed.
+5 -5
View File
@@ -77,7 +77,7 @@ Every change to master goes through this workflow. No exceptions.
6. **Review** — review the full diff focusing on: functional completeness, edge cases, technical excellence (architecture, SRP, deduplication, code smells, best practices, code quality, reusability, clean code, readability, maintainability, extensibility), performance, security, UX, documentation completeness/relevance. Post review comments via `devx.ci.pr_review`. 6. **Review** — review the full diff focusing on: functional completeness, edge cases, technical excellence (architecture, SRP, deduplication, code smells, best practices, code quality, reusability, clean code, readability, maintainability, extensibility), performance, security, UX, documentation completeness/relevance. Post review comments via `devx.ci.pr_review`.
7. **Address comments** — fix each comment, commit, push, re-review 7. **Address comments** — fix each comment, commit, push, re-review
8. **Approve** — post an `APPROVE` review via `devx.ci.pr_review` 8. **Approve** — post an `APPROVE` review via `devx.ci.pr_review`
9. **Add `ready-to-merge` label** — auto-merge workflow squash-merges with title `GRM-N <conventional commit message>`, post-merge workflow marks the Vikunja task as done, release workflow automatically versions and tags 9. **Add `ready-to-merge` label** — auto-merge workflow squash-merges with title `GRM-N <conventional commit message>`, post-merge workflow marks the Vikunja task as done, release-and-maintain job automatically versions and tags
### 1. Create Vikunja task ### 1. Create Vikunja task
Create a task in Vikunja project 6 to get a `GRM-N` identifier. Create a task in Vikunja project 6 to get a `GRM-N` identifier.
@@ -165,17 +165,17 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
3. Wait for all CI checks to pass 3. Wait for all CI checks to pass
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated) 4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
5. The post-merge workflow marks the Vikunja task as done 5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes 6. The release-and-maintain job automatically versions, tags, and publishes
> **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge workflow by adding the `ready-to-merge` label. Manual merges bypass the `GRM-N <conventional>` format enforcement. > **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge workflow by adding the `ready-to-merge` label. Manual merges bypass the `GRM-N <conventional>` format enforcement.
### Branch Protection (Required Gitea Settings) ### Branch Protection (Required Gitea Settings)
Branch protection is automatically configured by `devx.tools.configure_repo` (runs as a `configure-repo` job in the post-merge workflow). The following rules are enforced for `master`: Branch protection is automatically configured by the `configure-repo` step in the `detect-and-configure` job of the post-merge workflow. The following rules are enforced for `master`:
- **Require pull request**: No direct pushes to master - **Require pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge - **Require approval review**: At least 1 `APPROVE` review before merge
- **Require status checks**: CI quality + molecule tests must pass - **Require status checks**: CI validate + molecule tests must pass
- **Block force pushes**: No history rewriting on master - **Block force pushes**: No history rewriting on master
The auto-merge workflow enforces the APPROVE review check programmatically as a defense-in-depth measure, but branch protection is the primary gate. The auto-merge workflow enforces the APPROVE review check programmatically as a defense-in-depth measure, but branch protection is the primary gate.
@@ -202,7 +202,7 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
- `install_runner.yml` handles: download, config, validate, register, service - `install_runner.yml` handles: download, config, validate, register, service
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates) - `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
- `systemctl --user` tasks must be guarded by `docker_rootless_setup` - `systemctl --user` tasks must be guarded by `docker_rootless_setup`
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files) - Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
- `apt` tasks use `cache_valid_time: 3600` to avoid unnecessary cache updates - `apt` tasks use `cache_valid_time: 3600` to avoid unnecessary cache updates
- `remove-runner.yml` runs `loginctl disable-linger` and removes subuid/subgid entries - `remove-runner.yml` runs `loginctl disable-linger` and removes subuid/subgid entries
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -16,7 +16,7 @@
│ ├── ui.py # Colorised console output │ ├── ui.py # Colorised console output
│ └── translations.json # Translation strings │ └── translations.json # Translation strings
├── ansible/ ├── ansible/
│ ├── roles/gitea-runner/ # Main Ansible role │ ├── roles/gitea_runner/ # Main Ansible role
│ │ ├── defaults/main.yml # Default variables │ │ ├── defaults/main.yml # Default variables
│ │ ├── tasks/ # Task files (13 files) │ │ ├── tasks/ # Task files (13 files)
│ │ ├── templates/ # Jinja2 templates (4 files) │ │ ├── templates/ # Jinja2 templates (4 files)
@@ -222,7 +222,7 @@ The pre-commit hook runs actionlint automatically when workflow files change.
|--------|-------------| |--------|-------------|
| `make setup` | Full setup: venv, deps, hooks, CI tools | | `make setup` | Full setup: venv, deps, hooks, CI tools |
| `make setup-ci` | Lean setup for CI jobs (pytest + lint, no Ansible collections) | | `make setup-ci` | Lean setup for CI jobs (pytest + lint, no Ansible collections) |
| `make setup-quality` | Setup for the quality CI job (lint + test deps) | | `make setup-quality` | Setup for the validate CI job (lint + test deps) |
| `make setup-molecule` | Full setup for molecule testing | | `make setup-molecule` | Full setup for molecule testing |
| `make setup-release` | Setup for release jobs (git-cliff, tea, lint tools) | | `make setup-release` | Setup for release jobs (git-cliff, tea, lint tools) |
| `make install-tools` | Install actionlint, git-cliff, act_runner, tea to `~/.local/bin` | | `make install-tools` | Install actionlint, git-cliff, act_runner, tea to `~/.local/bin` |
+4 -4
View File
@@ -16,11 +16,11 @@ From the `Makefile`:
- `test-unit``pytest tests/unit/ -v --no-cov` (unit tests without coverage) - `test-unit``pytest tests/unit/ -v --no-cov` (unit tests without coverage)
- `pytest-cov``pytest tests/ -v --cov=src/grm --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement) - `pytest-cov``pytest tests/ -v --cov=src/grm --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement)
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release. The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI validate job runs `make pytest-cov` on every PR, and the release step in the release-and-maintain job runs it again before tagging a release.
### Test speed verification ### Test speed verification
The CI quality job also runs `python -m devx.tools.check_test_speed --max-seconds 10` to verify that unit tests run fast (under 10 seconds total). This catches performance regressions early. The CI validate job also runs `python -m devx.tools.check_test_speed --max-seconds 10` to verify that unit tests run fast (under 10 seconds total). This catches performance regressions early.
## Integration Tests ## Integration Tests
@@ -43,11 +43,11 @@ make molecule # Quick: all 6 scenarios on Ubuntu 22.04
make molecule-all # Full: all 6 scenarios on all 4 supported OSes make molecule-all # Full: all 6 scenarios on all 4 supported OSes
``` ```
Molecule tests validate the Ansible role (`ansible/roles/gitea-runner/`) by creating Docker containers as test hosts, applying the role, and verifying the results. Each scenario tests a specific aspect of the role. Molecule tests validate the Ansible role (`ansible/roles/gitea_runner/`) by creating Docker containers as test hosts, applying the role, and verifying the results. Each scenario tests a specific aspect of the role.
### Scenarios ### Scenarios
Seven Molecule scenarios are defined under `ansible/roles/gitea-runner/molecule/`: Seven Molecule scenarios are defined under `ansible/roles/gitea_runner/molecule/`:
| Scenario | Description | What it verifies | | Scenario | Description | What it verifies |
|----------|-------------|------------------| |----------|-------------|------------------|
+3 -3
View File
@@ -14,7 +14,7 @@ GRM provides the following CLI commands for managing Gitea Actions runners. The
| `grm enable` | `<runner_name>` | Enable a runner to start on boot | | `grm enable` | `<runner_name>` | Enable a runner to start on boot |
| `grm disable` | `<runner_name>` | Disable and deregister a runner | | `grm disable` | `<runner_name>` | Disable and deregister a runner |
| `grm status` | `<runner_name>` | Check the status of a registered runner | | `grm status` | `<runner_name>` | Check the status of a registered runner |
| `grm remove` | `<runner_name>` | Remove a runner completely | | `grm remove` | `<runner_name>` | Remove a runner entirely |
| `grm list` | — | List all registered runners with live status | | `grm list` | — | List all registered runners with live status |
| `grm health` | `[runner_name]` | Run health check (Docker, runner service, disk) on one or all runners | | `grm health` | `[runner_name]` | Run health check (Docker, runner service, disk) on one or all runners |
| `grm trigger-workflow` | `<workflow_id>` | Trigger a Gitea Actions workflow via the API | | `grm trigger-workflow` | `<workflow_id>` | Trigger a Gitea Actions workflow via the API |
@@ -244,7 +244,7 @@ grm status <runner_name> [options]
## remove ## remove
Remove a registered Gitea Runner completely. Remove a registered Gitea Runner entirely.
```bash ```bash
grm remove <runner_name> [options] grm remove <runner_name> [options]
@@ -335,7 +335,7 @@ grm health prod-runner
Output shows NAME, HOST, HEALTHY (yes/no), and MESSAGE columns. The command exits with code 1 if any runner is unhealthy. Output shows NAME, HOST, HEALTHY (yes/no), and MESSAGE columns. The command exits with code 1 if any runner is unhealthy.
The health check is also run automatically via a systemd timer installed by the Ansible role. See `ansible/roles/gitea-runner/templates/runner-healthcheck.sh.j2` for the script and `runner-healthcheck.timer.j2` for the timer. The health check is also run automatically via a systemd timer installed by the Ansible role. See `ansible/roles/gitea_runner/templates/runner-healthcheck.sh.j2` for the script and `runner-healthcheck.timer.j2` for the timer.
## trigger-workflow ## trigger-workflow
+15 -13
View File
@@ -1,6 +1,6 @@
# FAQ # FAQ
## How do I obtain the Gitea registration token? ## How to obtain the Gitea registration token
There are three levels of registration tokens, depending on which repositories the runner should serve: There are three levels of registration tokens, depending on which repositories the runner should serve:
@@ -10,15 +10,15 @@ There are three levels of registration tokens, depending on which repositories t
Set the token as `GITEA_REGISTRATION_TOKEN` in your `.env` file or pass it via `--token` on the command line. Set the token as `GITEA_REGISTRATION_TOKEN` in your `.env` file or pass it via `--token` on the command line.
## What is the CI_GITEA_TOKEN and do I need it? ## What is the CI_GITEA_TOKEN and is it needed?
`CI_GITEA_TOKEN` is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the `.runner` file and systemd service, not the API check. `CI_GITEA_TOKEN` is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the `.runner` file and systemd service, not the API check.
To generate one: Settings → Applications → Generate New Token, with the `admin` scope (or at minimum `read:user`, `read:repository`, `read:admin`). To generate one: Settings → Applications → Generate New Token, with the `admin` scope (or at minimum `read:user`, `read:repository`, `read:admin`).
If you skip it, GRM will still verify the runner correctly — it just won't show the extra API confirmation. If you skip it, GRM will still verify the runner correctly — it won't show the extra API confirmation.
## How do I skip the sudo password prompt for automation? ## How to skip the sudo password prompt for automation
Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to the CLI command. This is recommended for CI/CD pipelines. Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to the CLI command. This is recommended for CI/CD pipelines.
@@ -34,7 +34,7 @@ Then use:
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --no-ask-become-pass grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --no-ask-become-pass
``` ```
## Can I run multiple runners on the same host? ## Can multiple runners run on the same host?
Yes. Each runner instance is fully isolated with its own system user (`grm-<name>`), rootless Docker daemon, data directory, and systemd user service. Install additional runners with different `--name` values and manage them independently by name. Yes. Each runner instance is fully isolated with its own system user (`grm-<name>`), rootless Docker daemon, data directory, and systemd user service. Install additional runners with different `--name` values and manage them independently by name.
@@ -46,7 +46,7 @@ grm list
Runners on the same host never interfere with each other or with the host's Docker installation. Runners on the same host never interfere with each other or with the host's Docker installation.
## Why does my runner appear offline after installation? ## Why does a runner appear offline after installation?
Check that `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` are correct, verify the runner service is running with `sudo -u grm-<name> systemctl --user status gitea-runner`, and check the logs for registration errors. You can also confirm the runner appears as **Online** in the Gitea UI under **Actions → Runners**. Check that `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` are correct, verify the runner service is running with `sudo -u grm-<name> systemctl --user status gitea-runner`, and check the logs for registration errors. You can also confirm the runner appears as **Online** in the Gitea UI under **Actions → Runners**.
@@ -64,7 +64,7 @@ This is a harmless cleanup traceback from Molecule's Docker driver when the test
GRM stores each runner's connection details (host, user, SSH key, Gitea URL, labels) in a local JSON registry at `~/.local/share/grm/runners.json`. After installation, lifecycle commands work by runner name only — you can override any stored value by passing the corresponding flag. GRM stores each runner's connection details (host, user, SSH key, Gitea URL, labels) in a local JSON registry at `~/.local/share/grm/runners.json`. After installation, lifecycle commands work by runner name only — you can override any stored value by passing the corresponding flag.
## How do I update the gitea_runner binary? ## How to update the gitea_runner binary
Use the `grm update` command: Use the `grm update` command:
@@ -80,7 +80,7 @@ grm update 192.168.1.10 --user ubuntu --version 1.0.8
The update command downloads the new binary and replaces the existing one at `/usr/local/bin/gitea_runner`. The runner service is restarted automatically. The update command downloads the new binary and replaces the existing one at `/usr/local/bin/gitea_runner`. The runner service is restarted automatically.
## How do I completely remove a runner? ## How to remove a runner entirely
Use the `grm remove` command: Use the `grm remove` command:
@@ -99,13 +99,15 @@ grm remove prod-runner --force
## What is the difference between disable and remove? ## What is the difference between disable and remove?
- **`grm disable <name>`** — Deregisters the runner from Gitea and stops the service, but leaves the user, directories, and service files in place. The runner can be re-enabled later with `grm enable` and re-registered with a new token. - **`grm disable <name>`** — Deregisters the runner from Gitea and stops the service, but leaves the user, directories, and service files in place. The runner can be re-enabled later with `grm enable` and re-registered with a new token.
- **`grm remove <name>`** — Completely removes the runner: deregisters from Gitea, stops and disables the service, removes the system user, deletes all directories, and removes the local registry entry. This is irreversible. - **`grm remove <name>`** — Removes the runner entirely: deregisters from Gitea, stops and disables the service, removes the system user, deletes all directories, and removes the local registry entry. This is irreversible.
## What operating systems are supported? ## What operating systems are supported?
GRM supports Arch Linux (rolling), Ubuntu 22.04/24.04, and Debian 12. All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files. GRM supports Arch Linux (rolling), Ubuntu 22.04/24.04, and Debian 12. All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files.
## How do I change the UI language? On Arch Linux, the rootless Docker setup scripts (`dockerd-rootless-setuptool.sh`, `dockerd-rootless.sh`) and the `rootlesskit` runtime are provisioned automatically — Arch's `docker` package does not ship them. No extra setup is required.
## How to change the UI language
Set the `GRM_LANG` environment variable to one of the supported languages: `en` (English, default), `bg` (Bulgarian), `de` (German), `ru` (Russian), `zh` (Chinese), `pl` (Polish). Set the `GRM_LANG` environment variable to one of the supported languages: `en` (English, default), `bg` (Bulgarian), `de` (German), `ru` (Russian), `zh` (Chinese), `pl` (Polish).
@@ -119,7 +121,7 @@ Or set it in your `.env` file:
GRM_LANG=bg GRM_LANG=bg
``` ```
## How do I enable debug logging? ## How to enable debug logging
Set the `GRM_LOG_LEVEL` environment variable to `DEBUG`: Set the `GRM_LOG_LEVEL` environment variable to `DEBUG`:
@@ -129,7 +131,7 @@ GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
The log file at `~/.local/state/grm/logs/grm.log` always captures DEBUG level regardless of this setting. Ansible execution logs are stored in timestamped files at `~/.local/state/grm/logs/ansible-<timestamp>.log`. The log file at `~/.local/state/grm/logs/grm.log` always captures DEBUG level regardless of this setting. Ansible execution logs are stored in timestamped files at `~/.local/state/grm/logs/ansible-<timestamp>.log`.
## What runner labels should I use? ## What runner labels should be used?
By default, runners are registered with `docker,ubuntu-latest:docker://runner-images:ubuntu-22.04`. You can override this with `--labels` or the `GITEA_RUNNER_LABELS` environment variable. By default, runners are registered with `docker,ubuntu-latest:docker://runner-images:ubuntu-22.04`. You can override this with `--labels` or the `GITEA_RUNNER_LABELS` environment variable.
@@ -151,7 +153,7 @@ Yes. GRM is designed with security as a first-class concern:
- **No shell injection**: The CLI never uses `shell=True` with subprocess. - **No shell injection**: The CLI never uses `shell=True` with subprocess.
- **Bandit security scan**: The CI pipeline runs Bandit on every PR. - **Bandit security scan**: The CI pipeline runs Bandit on every PR.
## Can I install GRM via pip? ## Can GRM be installed via pip?
Yes: Yes:
+1 -1
View File
@@ -223,7 +223,7 @@ make update HOST=192.168.1.10 USER=ubuntu VERSION=1.0.8
## Removing Runners ## Removing Runners
To remove a runner completely (deregisters from Gitea, removes user, directories, and service files): To remove a runner entirely (deregisters from Gitea, removes user, directories, and service files):
```bash ```bash
grm remove prod-runner --token <registration-token> grm remove prod-runner --token <registration-token>
+9
View File
@@ -2,6 +2,14 @@
## Installation Issues ## Installation Issues
### `No such file or directory: dockerd-rootless-setuptool.sh` (Arch Linux)
**Symptom:** `grm install` fails on an Arch Linux host during rootless Docker setup with `Error executing command: [Errno 2] No such file or directory: b'dockerd-rootless-setuptool.sh'`.
**Cause:** Arch's `docker` package does not ship the rootless setup scripts (unlike Debian/Ubuntu's `docker-ce-rootless-extras`), and `rootlesskit` is not a dependency of the `docker` package.
**Solution:** This is handled automatically by GRM ≥ v0.19.0. Ensure you are on the latest release (`git checkout $(git describe --tags --abbrev=0)`). The role fetches the scripts from upstream `moby/moby` and installs `rootlesskit` automatically. If it still fails, verify the host can reach `raw.githubusercontent.com` (the script source) and check the Ansible log under `~/.local/state/grm/logs/`.
### Ansible connection fails (UNREACHABLE) ### Ansible connection fails (UNREACHABLE)
**Symptom:** Ansible reports `UNREACHABLE` when trying to connect to the target host. **Symptom:** Ansible reports `UNREACHABLE` when trying to connect to the target host.
@@ -179,6 +187,7 @@ If Docker is not installed, install it via your package manager or [Docker's off
| Symptom | Likely Cause | Solution | | Symptom | Likely Cause | Solution |
|---------|-------------|----------| |---------|-------------|----------|
| Ansible UNREACHABLE | SSH connection failed | Verify `--user`, `--key`, and host reachability | | Ansible UNREACHABLE | SSH connection failed | Verify `--user`, `--key`, and host reachability |
| `No such file ... dockerd-rootless-setuptool.sh` (Arch) | Arch `docker` pkg lacks rootless scripts + `rootlesskit` | Fixed in v0.19.0; update GRM to latest release |
| `GITEA_URL must be set` | Missing environment variable | Set `GITEA_URL` in `.env` or pass `--url` | | `GITEA_URL must be set` | Missing environment variable | Set `GITEA_URL` in `.env` or pass `--url` |
| `GITEA_REGISTRATION_TOKEN must be set` | Missing environment variable | Set `GITEA_REGISTRATION_TOKEN` in `.env` or pass `--token` | | `GITEA_REGISTRATION_TOKEN must be set` | Missing environment variable | Set `GITEA_REGISTRATION_TOKEN` in `.env` or pass `--token` |
| Runner appears offline | Registration failed or service not running | Check GITEA_URL, token validity, and service status | | Runner appears offline | Registration failed or service not running | Check GITEA_URL, token validity, and service status |
+15 -9
View File
@@ -27,25 +27,26 @@ version = {attr = "grm.__version__"}
[project.optional-dependencies] [project.optional-dependencies]
# Minimal deps for CI scripts that only need click/dotenv # Minimal deps for CI scripts that only need click/dotenv
# (detect-changes, discover-runners, pr-review, sync-wiki, badges, etc.) # (validate job steps: detect-changes, discover-runners, pr-review;
# release-and-maintain job steps: sync-wiki, badges, etc.)
ci = [ ci = [
"pytest==9.1.1", "pytest==9.1.1",
"pytest-cov==7.1.0", "pytest-cov==7.1.0",
"build==1.5.0", "build==1.5.1",
"twine==6.2.0", "twine==6.2.0",
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.) # Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
"devx==0.35.1", "devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.1",
] ]
# Lint and type-checking tools (quality job) # Lint and type-checking tools (validate job)
lint = [ lint = [
"ruff==0.15.20", "ruff==0.15.21",
"pyright==1.1.411", "pyright==1.1.411",
"bandit==1.9.4", "bandit==1.9.4",
"pip-audit==2.10.1", "pip-audit==2.10.1",
"pre-commit==4.6.0", "pre-commit==4.6.0",
"ansible-lint==26.4.0", "ansible-lint==26.6.0",
] ]
# Molecule testing (molecule-tests job) # Molecule testing (molecule-tests job — stays as a separate CI job)
molecule = [ molecule = [
"molecule==26.6.0", "molecule==26.6.0",
"molecule-docker==2.1.0", "molecule-docker==2.1.0",
@@ -54,7 +55,7 @@ molecule = [
dev = [ dev = [
"grm[ci,lint,molecule]", "grm[ci,lint,molecule]",
# Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr) # Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr)
"devx==0.35.1", "devx @ git+https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git@v0.47.1",
# Non-Python dev dependency: checkmake (Makefile linter) # Non-Python dev dependency: checkmake (Makefile linter)
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest # Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
] ]
@@ -101,8 +102,13 @@ vikunja_project_id = 6
repo_owner = "oblachno-oss" repo_owner = "oblachno-oss"
repo_name = "grm" repo_name = "grm"
# grm doesn't have its own CI scripts — it uses devx's CI modules.
# Skip CI script checks (doc_coverage would otherwise look for src/ci/).
[tool.devx.doc_coverage]
ci_scripts_dir = ""
# Molecule test weights for LPT scheduling. # Molecule test weights for LPT scheduling.
# GRM has a single role (gitea-runner) with 7 scenarios. # GRM has a single role (gitea_runner) with 7 scenarios.
# Weights are estimates — recalibrate from CI logs after next run. # Weights are estimates — recalibrate from CI logs after next run.
[tool.devx.molecule.weights] [tool.devx.molecule.weights]
"multi-instance" = 8 "multi-instance" = 8
+1 -1
View File
@@ -1,3 +1,3 @@
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners.""" """Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
__version__ = "0.15.0" __version__ = "0.18.2"
+6 -4
View File
@@ -125,8 +125,8 @@ def cli(ctx: click.Context, become_password_file: str | None, verbose: bool) ->
@click.option( @click.option(
"--admin-token", "--admin-token",
"-a", "-a",
default=lambda: os.getenv("CI_GITEA_TOKEN"), default=lambda: os.getenv("CI_GITEA_API_TOKEN") or os.getenv("CI_GITEA_TOKEN"),
help=_("Gitea admin API token for integration test (env: CI_GITEA_TOKEN)"), help=_("Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)"),
) )
@click.option( @click.option(
"--integration-retries", "--integration-retries",
@@ -528,8 +528,10 @@ def list_runners(ask_become_pass: bool, no_status: bool) -> None:
) )
@click.option( @click.option(
"--token", "--token",
default=lambda: os.getenv("CI_GITEA_TOKEN"), default=lambda: (
help=_("Gitea API token (env: CI_GITEA_TOKEN)"), os.getenv("DEVELOPER_GITEA_API_TOKEN") or os.getenv("CI_GITEA_API_TOKEN") or os.getenv("CI_GITEA_TOKEN")
),
help=_("Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)"),
) )
@click.option( @click.option(
"--list", "--list",
+1 -1
View File
@@ -157,7 +157,7 @@ class AnsibleExecutor:
with open(log_file) as f: with open(log_file) as f:
content = f.read() content = f.read()
match = re.search( match = re.search(
r'TASK \[gitea-runner : Report runner status\].*?"msg":\s*"([^"]+)"', r'TASK \[gitea_runner : Report runner status\].*?"msg":\s*"([^"]+)"',
content, content,
re.DOTALL, re.DOTALL,
) )
+9 -9
View File
@@ -100,14 +100,14 @@ class RunnerManager:
extra_vars: dict[str, str | int] = { extra_vars: dict[str, str | int] = {
"registration_token": token, "registration_token": token,
"runner_name": name, "gitea_runner_name": name,
"gitea_url": gitea_url, "gitea_url": gitea_url,
"gitea_runner_integration_retries": integration_retries, "gitea_runner_integration_retries": integration_retries,
} }
if admin_token: if admin_token:
extra_vars["gitea_admin_token"] = admin_token extra_vars["gitea_admin_token"] = admin_token
if labels is not None: if labels is not None:
extra_vars["runner_labels"] = labels extra_vars["gitea_runner_labels"] = labels
with track_steps() as tracker: with track_steps() as tracker:
tracker.begin(_("Installing Gitea Runner on {host}", host=host)) tracker.begin(_("Installing Gitea Runner on {host}", host=host))
@@ -216,7 +216,7 @@ class RunnerManager:
"start-runner.yml", "start-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name}, {"gitea_runner_name": name},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -243,7 +243,7 @@ class RunnerManager:
"stop-runner.yml", "stop-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name}, {"gitea_runner_name": name},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Stopping Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Stopping Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -270,7 +270,7 @@ class RunnerManager:
"restart-runner.yml", "restart-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name}, {"gitea_runner_name": name},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Restarting Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Restarting Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -297,7 +297,7 @@ class RunnerManager:
"enable-runner.yml", "enable-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name}, {"gitea_runner_name": name},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Enabling Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Enabling Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -331,7 +331,7 @@ class RunnerManager:
"disable-runner.yml", "disable-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name, "registration_token": token, "gitea_url": resolved_gitea_url}, {"gitea_runner_name": name, "registration_token": token, "gitea_url": resolved_gitea_url},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Disabling Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Disabling Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -358,7 +358,7 @@ class RunnerManager:
"status-runner.yml", "status-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name}, {"gitea_runner_name": name},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Checking status of Gitea Runner {name} on {host}", name=name, host=actual_host), description=_("Checking status of Gitea Runner {name} on {host}", name=name, host=actual_host),
@@ -401,7 +401,7 @@ class RunnerManager:
"remove-runner.yml", "remove-runner.yml",
actual_host, actual_host,
actual_user, actual_user,
{"runner_name": name, "registration_token": resolved_token, "gitea_url": resolved_gitea_url}, {"gitea_runner_name": name, "registration_token": resolved_token, "gitea_url": resolved_gitea_url},
actual_key, actual_key,
ask_become_pass, ask_become_pass,
description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host), description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host),
+14 -14
View File
@@ -175,13 +175,13 @@
"ru": "URL Gitea (env: GITEA_URL)", "ru": "URL Gitea (env: GITEA_URL)",
"zh": "Gitea URL(环境变量: GITEA_URL" "zh": "Gitea URL(环境变量: GITEA_URL"
}, },
"Gitea admin API token for integration test (env: CI_GITEA_TOKEN)": { "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)": {
"bg": "Gitea admin API токен за интеграционен тест (env: CI_GITEA_TOKEN)", "bg": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
"de": "Gitea-Admin-API-Token für Integrationstest (env: CI_GITEA_TOKEN)", "de": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
"en": "Gitea admin API token for integration test (env: CI_GITEA_TOKEN)", "en": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
"pl": "Token API administratora Gitea do testów integracyjnych (env: CI_GITEA_TOKEN)", "pl": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
"ru": "Токен админ API Gitea для интеграционного теста (env: CI_GITEA_TOKEN)", "ru": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
"zh": "Gitea 管理员 API 令牌,用于集成测试(环境变量: CI_GITEA_TOKEN" "zh": "Gitea admin API token for integration test(环境变量: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN"
}, },
"HEALTHY": { "HEALTHY": {
"bg": "ЗДРАВ", "bg": "ЗДРАВ",
@@ -703,13 +703,13 @@
"ru": "Git ref для запуска workflow (по умолчанию: master)", "ru": "Git ref для запуска workflow (по умолчанию: master)",
"zh": "运行工作流的 Git ref(默认:master" "zh": "运行工作流的 Git ref(默认:master"
}, },
"Gitea API token (env: CI_GITEA_TOKEN)": { "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)": {
"bg": "Gitea API токен (env: CI_GITEA_TOKEN)", "bg": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
"de": "Gitea API-Token (env: CI_GITEA_TOKEN)", "de": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
"en": "Gitea API token (env: CI_GITEA_TOKEN)", "en": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
"pl": "Token API Gitea (env: CI_GITEA_TOKEN)", "pl": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
"ru": "Токен API Gitea (env: CI_GITEA_TOKEN)", "ru": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
"zh": "Gitea API 令牌(环境变量:CI_GITEA_TOKEN" "zh": "Gitea API token(环境变量:DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN"
}, },
"List available workflows instead of triggering one": { "List available workflows instead of triggering one": {
"bg": "Списък на наличните работни процеси вместо изпълнение", "bg": "Списък на наличните работни процеси вместо изпълнение",

Some files were not shown because too many files have changed in this diff Show More