Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41fc36ff4a | ||
|
|
e585543e9d | ||
|
|
c62c35f5b6 | ||
|
|
4b900ce673 | ||
|
|
cae66e0743 | ||
|
|
0b3a76c550 | ||
|
|
a4d5ba6b70 | ||
|
|
d9ce4e240f | ||
|
|
c1f68f115a | ||
|
|
fdf1293c85 | ||
|
|
01b3f594f7 | ||
|
|
9ffa7a3671 | ||
|
|
f04be9c39b | ||
|
|
f67dff8458 | ||
|
|
763f7640af | ||
|
|
f8eeea611f | ||
|
|
774bf479ab | ||
|
|
844171ee93 | ||
|
|
9a5be879a2 | ||
|
|
f24ed4c963 | ||
|
|
d1e1d05be5 | ||
|
|
dbb9bd7108 | ||
|
|
f905550aba | ||
|
|
cae4e2a860 | ||
|
|
efbd24daec | ||
|
|
9066ef9724 | ||
|
|
96770a770e | ||
|
|
e753b34788 | ||
|
|
48422b18e5 | ||
|
|
190157cce6 | ||
|
|
1d0a082044 | ||
|
|
799d36f254 | ||
|
|
e0d43b0ed8 | ||
|
|
3189161f61 | ||
|
|
c339698603 | ||
|
|
cbf082c78f | ||
|
|
4070135fda | ||
|
|
ace0176e3a | ||
|
|
fda1d99d86 | ||
|
|
465f45d939 | ||
|
|
103beaa0e8 | ||
|
|
2e97578269 | ||
|
|
c31ec312ac | ||
|
|
c67b810e58 | ||
|
|
ef16b07cdf | ||
|
|
2c849c7324 | ||
|
|
5804a18974 | ||
|
|
9dbe20ba73 | ||
|
|
b92c87ba68 | ||
|
|
dc4430d160 | ||
|
|
7aa0ebaefe | ||
|
|
e93da43219 | ||
|
|
b131a2872d | ||
|
|
e4dd8f308f | ||
|
|
467e0d66e6 | ||
|
|
6ee5b74bb5 | ||
|
|
21cc89899f | ||
|
|
0382e155a6 | ||
|
|
d87c0d7e9a | ||
|
|
4be480a18e | ||
|
|
de92f675ed | ||
|
|
b5803a8611 | ||
|
|
ec22d20a15 | ||
|
|
e96012af7f | ||
|
|
addef7500c | ||
|
|
8a0d2c428b | ||
|
|
d68fb6d272 | ||
|
|
6721864b87 | ||
|
|
06471d1403 | ||
|
|
1a04971622 | ||
|
|
91440c1b0a | ||
|
|
3bb8d75748 | ||
|
|
d4a8172a25 | ||
|
|
2199ec0bfe | ||
|
|
9ae9a76b11 | ||
|
|
31d0eeae98 | ||
|
|
acc768eaea | ||
|
|
cf2314c20f | ||
|
|
aac47e3472 | ||
|
|
811e7a2309 | ||
|
|
cb68c85bb5 | ||
|
|
0ba30e09ea | ||
|
|
6a02581687 | ||
|
|
4679473183 | ||
|
|
6359eab962 | ||
|
|
2017a5ee3e | ||
|
|
465e9bd484 | ||
|
|
da0656b949 | ||
|
|
64ab0f059b | ||
|
|
a58f5ec301 | ||
|
|
1dc20025d8 | ||
|
|
e6918a9be9 | ||
|
|
c5d8cbef5a | ||
|
|
6032a07038 | ||
|
|
af251ffdaa | ||
|
|
493051b79b | ||
|
|
cb94709091 | ||
|
|
7987778a4f | ||
|
|
488a7ee048 | ||
|
|
7fca2a3ebd | ||
|
|
f14ef14dc6 | ||
|
|
6758b69a5f | ||
|
|
ba5b05bde2 | ||
|
|
041e5ac4aa | ||
|
|
a0f997cb3e | ||
|
|
00828527f9 | ||
|
|
564b917234 | ||
|
|
f445085d54 | ||
|
|
60a7f72156 | ||
|
|
5174e103f7 | ||
|
|
1189807d4e | ||
|
|
9e420e3dab | ||
|
|
c9c46e88ac | ||
|
|
1e04d38d59 | ||
|
|
dcb2ed0fd9 | ||
|
|
5e270f21d1 | ||
|
|
8e532839fd | ||
|
|
f5177c823c | ||
|
|
f649120172 | ||
|
|
7d3cf999d6 | ||
|
|
3ae263fb00 | ||
|
|
505673bc62 | ||
|
|
a791800809 | ||
|
|
d1531ac81f | ||
|
|
0bf78d4f83 | ||
|
|
ad3c43bf8e | ||
|
|
28e61aa166 | ||
|
|
b8ab4f854b | ||
|
|
8bde4cd12b | ||
|
|
b6e87a519b | ||
|
|
bec7b59671 | ||
|
|
7a6d93cddc | ||
|
|
81e8c2a159 | ||
|
|
fe715f99be | ||
|
|
eedef42c13 | ||
|
|
1f75017395 | ||
|
|
8c16703106 | ||
|
|
69089f6d2c | ||
|
|
ff0e733e07 | ||
|
|
8dc014a907 | ||
|
|
599fc17dd3 | ||
|
|
5cd7c15d11 | ||
|
|
1b3c3f6ca8 | ||
|
|
7591c99c02 | ||
|
|
789890b9ea | ||
|
|
f8327a8e89 | ||
|
|
d8d025789b | ||
|
|
5b4aaffa3b | ||
|
|
b2b7383266 | ||
|
|
c8e12dc722 | ||
|
|
31edf866f3 | ||
|
|
9959b9c4ed | ||
|
|
749b4d025f | ||
|
|
28b4acf323 | ||
|
|
f5431c54cf | ||
|
|
cff8a35244 | ||
|
|
54a584d609 | ||
|
|
402e2dce7e | ||
|
|
7dfc9f6014 | ||
|
|
a0e6cd0a73 | ||
|
|
a0b03f01ef | ||
|
|
3f5808d6be | ||
|
|
60c94b2b93 | ||
|
|
2ca56ed317 | ||
|
|
fb76ac91ef | ||
|
|
0c54efbf6b | ||
|
|
945344f960 | ||
|
|
713e752860 | ||
|
|
e0ae01e36e | ||
|
|
5511cba1b0 | ||
|
|
56eb241b77 | ||
|
|
6b3f2a5866 | ||
|
|
6f181e85e1 | ||
|
|
539bfea516 | ||
|
|
5b05db4e6d | ||
|
|
7fe85423b4 | ||
|
|
76d9983514 | ||
|
|
3dcdde80ad | ||
|
|
2e5ca5a88f | ||
|
|
e7f8e4ac66 | ||
|
|
a1b493f2a3 | ||
|
|
63e25d1245 | ||
|
|
996a8dc806 | ||
|
|
ea6276ff38 | ||
|
|
f0afa8171a | ||
|
|
09699696c0 | ||
|
|
ea8b71da1a | ||
|
|
a7eb4d1a68 | ||
|
|
e00d40dd54 | ||
|
|
63fb259bac | ||
|
|
5c1d848311 | ||
|
|
1717d55013 | ||
|
|
a676adb025 | ||
|
|
1d85a6da9d | ||
|
|
3822f6fe9a | ||
|
|
7b0e700fe5 | ||
|
|
b7a04f37de | ||
|
|
fcd26dd110 | ||
|
|
8579a4064f | ||
|
|
c271293b2d | ||
|
|
6ef38631fc | ||
|
|
92ca7ef7bb | ||
|
|
78c9b635e9 | ||
|
|
d65b2190e2 | ||
|
|
69b96e4785 | ||
|
|
a57a5c328c | ||
|
|
312b8241df | ||
|
|
227db4c457 | ||
|
|
da86eb0e9d | ||
|
|
084ea49523 | ||
|
|
8af88efeb5 | ||
|
|
36207be565 | ||
|
|
02909bf8b4 | ||
|
|
119d70e137 | ||
|
|
74db5f28c7 | ||
|
|
8aa00c7091 | ||
|
|
2a803c611b | ||
|
|
43b2a01361 | ||
|
|
d4ea2eef61 | ||
|
|
55c2746569 | ||
|
|
0c6c735000 | ||
|
|
c8970da942 | ||
|
|
3364355c73 | ||
|
|
0fadb7c504 | ||
|
|
ea18793963 | ||
|
|
9c51c8b62c | ||
|
|
d949bd3444 | ||
|
|
d8c31238bd | ||
|
|
ae27417a5f | ||
|
|
e9c0f22fc0 | ||
|
|
fefddda715 | ||
|
|
790b5c3769 | ||
|
|
b2acaefaf9 | ||
|
|
e1b589efab | ||
|
|
4fa3aeb54c | ||
|
|
7d25e059f7 | ||
|
|
4d2ac8d449 | ||
|
|
3048d7ade7 | ||
|
|
129cfe3c79 | ||
|
|
1a917e7a5d | ||
|
|
d4766da5f9 | ||
|
|
c05d7c9c4f | ||
|
|
c91626a8ff | ||
|
|
ad7c6255d5 | ||
|
|
e1173aeeb8 | ||
|
|
4da724ce53 | ||
|
|
1d3d2487ac | ||
|
|
39ef86647c | ||
|
|
5fb282e38e | ||
|
|
3d943b57dc | ||
|
|
a9adb71a08 | ||
|
|
1bbe3b4f43 | ||
|
|
12e6ce1601 | ||
|
|
51f204f90a | ||
|
|
ebb1088a8c | ||
|
|
18760f6a2a | ||
|
|
677745ac99 | ||
|
|
6ede871054 | ||
|
|
47d5df0aed | ||
|
|
3f7507500b | ||
|
|
3b3e002f7f | ||
|
|
815b59c537 | ||
|
|
3b68302274 | ||
|
|
b3ac955515 | ||
|
|
4c08606d0c | ||
|
|
a575a89026 | ||
|
|
adb758f5f5 | ||
|
|
e5964ca5a9 | ||
|
|
047ee05fa1 | ||
|
|
564c12e782 | ||
|
|
6414f2306c | ||
|
|
1ad7c0a816 | ||
|
|
68dcbc9b16 | ||
|
|
9378451a12 | ||
|
|
e08c11ea83 | ||
|
|
81dd11e720 | ||
|
|
a05cd1c7ee | ||
|
|
55fee77c91 | ||
|
|
b48038a3a9 | ||
|
|
994c30da57 | ||
|
|
aef24352c1 | ||
|
|
8c385dcbdc | ||
|
|
2530ec54cc | ||
|
|
cc000c226c | ||
|
|
0cea9b9490 | ||
|
|
b3838eb180 | ||
|
|
3c8654342f |
@@ -5,3 +5,6 @@ exclude_paths:
|
||||
- molecule/
|
||||
- .molecule/
|
||||
- .pytest_cache/
|
||||
skip_list:
|
||||
# Rootless Docker uses `systemctl --user` which the systemd module doesn't support
|
||||
- command-instead-of-module
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[checkmake]
|
||||
# Disable the phony rule which flags common .PHONY placement patterns
|
||||
# as it produces false positives for standard Makefile layouts
|
||||
disable=maxbodylength
|
||||
|
||||
+62
-3
@@ -1,4 +1,63 @@
|
||||
# Gitea instance URL (used for runner registration and API validation)
|
||||
GITEA_URL=https://git.example.com
|
||||
GITEA_TOKEN=your-personal-access-token
|
||||
# Optional: GITEA_RUNNER_USER=ubuntu
|
||||
# Optional: GITEA_RUNNER_KEY=~/.ssh/id_ed25519
|
||||
|
||||
# Runner registration token from Gitea.
|
||||
# Three levels are available:
|
||||
# Instance-level: Site Administration → Actions → Runners → Create Registration Token
|
||||
# Org-level: Organization → Settings → Actions → Runners → Create Registration Token
|
||||
# Repo-level: Repository → Settings → Actions → Runners → Create Registration Token
|
||||
GITEA_REGISTRATION_TOKEN=your-registration-token
|
||||
|
||||
# Gitea admin API token for optional post-install API checks (informational only).
|
||||
# The integration test primarily verifies the runner by checking:
|
||||
# 1. The .runner registration file exists and is valid
|
||||
# 2. The container/service is running
|
||||
# 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")
|
||||
# Generate token at: Settings → Applications → Generate New Token
|
||||
# CI_GITEA_TOKEN=your-admin-api-token
|
||||
|
||||
# Integration test API retries (optional, default: 3).
|
||||
# Number of times to retry API checks waiting for runner to appear.
|
||||
# GITEA_INTEGRATION_RETRIES=3
|
||||
|
||||
# Default SSH user for remote hosts (optional, overrides --user)
|
||||
# GITEA_RUNNER_USER=ubuntu
|
||||
|
||||
# Repository for grm trigger-workflow (optional, default: oblachno-oss/grm)
|
||||
# GRM_REPO=oblachno-oss/grm
|
||||
|
||||
# Default SSH private key path (optional, overrides --key)
|
||||
# GITEA_RUNNER_KEY=~/.ssh/id_ed25519
|
||||
|
||||
# Default runner labels for Gitea Actions (optional, overrides --labels)
|
||||
# Format: <label>:<docker-image>[:<command>]
|
||||
# Use an official Gitea runner image with Node.js, Python and Docker CLI.
|
||||
# Avoid bare OS images like alpine:latest because actions/checkout@v4 needs Node.
|
||||
# GITEA_RUNNER_LABELS=docker:docker://gitea/runner-images:ubuntu-latest
|
||||
|
||||
# UI language for GRM console messages (optional, default: en)
|
||||
# Supported: en, bg, de, ru, zh, pl
|
||||
# GRM_LANG=en
|
||||
|
||||
# Sudo password file for Ansible become operations (optional)
|
||||
# When set, GRM reads the sudo password from this file instead of prompting.
|
||||
# Priority: --become-password-file CLI flag > GRM_BECOME_PASSWORD_FILE > ANSIBLE_BECOME_PASSWORD_FILE
|
||||
# GRM_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||
# ANSIBLE_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||
|
||||
# Gitea PyPI registry username (for private package access)
|
||||
# Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL
|
||||
CI_GITEA_USERNAME=emil
|
||||
|
||||
# Vikunja API token (required for `make create-task` dev workflow)
|
||||
# Generate at: Vikunja → Settings → API Tokens
|
||||
# VIKUNJA_TOKEN=your-vikunja-api-token
|
||||
|
||||
# devx configuration (GRM-specific overrides)
|
||||
# Task prefix for Vikunja task IDs
|
||||
DEVX_TASK_PREFIX=GRM
|
||||
# Vikunja project ID for GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID=6
|
||||
# Version file path (relative to repo root)
|
||||
DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# actionlint configuration for Gitea Actions workflows
|
||||
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
|
||||
#
|
||||
# Run: actionlint -config-file .gitea/actionlint.yaml .gitea/workflows/*.yml
|
||||
|
||||
# Custom self-hosted runner labels used in runs-on
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- docker
|
||||
@@ -0,0 +1,285 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality: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=lint
|
||||
- name: Lint all
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
make lint-all
|
||||
- name: Unit tests with 100% coverage
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
make pytest-cov
|
||||
- name: Translation completeness check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.check_translations --translations src/gitea_runner_manager/translations.json
|
||||
- name: Check unit test speed
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
- name: Dependency security scan
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
# Install pip in venv if missing (needed by pip-audit)
|
||||
.venv/bin/python -m ensurepip 2>/dev/null || true
|
||||
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
||||
pip-audit --desc --skip-editable 2>&1 || true
|
||||
- name: Workflow dry-run validation
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
# Best-effort: only runs if act_runner is installed
|
||||
if command -v act_runner >/dev/null 2>&1; then
|
||||
make workflow-dryrun
|
||||
else
|
||||
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
||||
fi
|
||||
|
||||
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/gitea_runner_manager/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
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
|
||||
id: detect
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.classify_changes \
|
||||
--base "origin/master" \
|
||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||
--github-output
|
||||
|
||||
discover-runners:
|
||||
needs: [detect-changes]
|
||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
runner-count: ${{ steps.discover.outputs.runner-count }}
|
||||
runner-indices: ${{ steps.discover.outputs.runner-indices }}
|
||||
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: Discover available runners
|
||||
id: discover
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.molecule.discover_runners \
|
||||
--owner "${{ github.repository_owner }}" \
|
||||
--repo "${{ github.event.repository.name }}" \
|
||||
--github-output
|
||||
|
||||
molecule-tests:
|
||||
needs: [quality, detect-changes, discover-runners]
|
||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
runner-index: [1, 2, 3]
|
||||
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,molecule
|
||||
- name: Install Ansible collections
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.tools.setup --skip-install --no-pre-commit --no-tea-login
|
||||
- name: Discover assigned test pairs
|
||||
env:
|
||||
RUNNER_INDEX: ${{ matrix.runner-index }}
|
||||
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.molecule.distribute_molecule \
|
||||
--runner-index "$RUNNER_INDEX" \
|
||||
--max-runners "$MAX_RUNNERS" \
|
||||
--github-env --skip-if-excess
|
||||
- name: Run molecule tests
|
||||
if: env.SKIP != 'true'
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
|
||||
if ! python3 -c "import docker; docker.from_env().ping()" 2>/dev/null; then
|
||||
echo "Docker not available in CI container — skipping molecule tests"
|
||||
exit 0
|
||||
fi
|
||||
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
||||
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
|
||||
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
|
||||
python3 -m devx.ci.pr_review \
|
||||
"${{ github.event.number }}" \
|
||||
"${{ github.repository }}"
|
||||
|
||||
auto-merge:
|
||||
# Auto-merge runs after all CI checks pass. It reads the task ID
|
||||
# from the branch name, validates the PR title, and squash-merges.
|
||||
# Uses always() so it evaluates even when molecule-tests is skipped
|
||||
# (Gitea Actions skips dependent jobs of skipped jobs by default).
|
||||
needs: [quality, detect-changes, pr-review, molecule-tests, release-dry-run]
|
||||
if: >-
|
||||
always() &&
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.quality.result == 'success' &&
|
||||
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
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
- 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: Post approval review
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.pr_review \
|
||||
"$PR_NUMBER" \
|
||||
"$REPOSITORY" \
|
||||
--event APPROVE \
|
||||
--checklist-confirmed \
|
||||
--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)."
|
||||
- name: Squash merge with task ID
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.auto_merge \
|
||||
"$HEAD_REF" \
|
||||
"$PR_TITLE" \
|
||||
"$REPOSITORY" \
|
||||
"$PR_NUMBER"
|
||||
@@ -0,0 +1,320 @@
|
||||
name: Post-merge
|
||||
|
||||
# Runs on every push to master. A single workflow with conditional jobs
|
||||
# for release, publish, wiki sync, badges, and Vikunja task updates.
|
||||
#
|
||||
# Job dependency graph:
|
||||
#
|
||||
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
||||
# ├── release (skip if release commit)
|
||||
# │ └── publish (needs release — builds & publishes to PyPI)
|
||||
# ├── 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
|
||||
# job (which depends on release) builds and publishes the package to the
|
||||
# Gitea PyPI registry. The release commit's post-merge run still updates
|
||||
# badges (version badge picks up the new version). Other jobs skip.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
|
||||
jobs:
|
||||
detect-type:
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
is-release: ${{ steps.check.outputs.is-release }}
|
||||
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: Check if this is a release commit
|
||||
id: check
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
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
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
git log -1 --format=%B > commit-msg.txt
|
||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||
rm -f commit-msg.txt
|
||||
|
||||
release:
|
||||
needs: [detect-type]
|
||||
if: needs.detect-type.outputs.is-release == 'false'
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
tag: ${{ steps.release-tag.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
- 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: Configure git
|
||||
run: |
|
||||
git config user.name "grm-ci-bot"
|
||||
git config user.email "grm-ci-bot@oblachno.fyi"
|
||||
- name: Run release
|
||||
id: release-tag
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
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
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.publish \
|
||||
"${{ needs.release.outputs.tag }}" \
|
||||
"${{ github.repository }}" --auto-login
|
||||
- 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/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: 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: Sync documentation to wiki
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
- 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
|
||||
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
|
||||
env:
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||
- 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/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: |
|
||||
. .venv/bin/activate
|
||||
python3 -m devx.tools.configure_repo
|
||||
- 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/configure-repo" \
|
||||
--commit "${{ github.sha }}"
|
||||
+13
@@ -25,6 +25,19 @@ build/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Security scanner
|
||||
.bandit
|
||||
bandit-report.*
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
.DS_Store
|
||||
activate.sh
|
||||
activate.fish
|
||||
activate.zsh
|
||||
|
||||
# Generated badges (CI pushes to badges branch)
|
||||
.badges/
|
||||
|
||||
# Deprecated CI task tracking (branch name is the sole source of truth)
|
||||
.taskid
|
||||
|
||||
+28
-18
@@ -1,25 +1,40 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: ruff-lint
|
||||
- id: validate-commit-msg
|
||||
name: validate commit message
|
||||
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.validate_commit_msg
|
||||
language: system
|
||||
stages: [commit-msg]
|
||||
pass_filenames: true
|
||||
|
||||
- id: lint-ruff
|
||||
name: ruff lint
|
||||
entry: .venv/bin/ruff check src/ tests/
|
||||
entry: make lint-ruff
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: ruff-format
|
||||
- id: lint-format
|
||||
name: ruff format check
|
||||
entry: .venv/bin/ruff format --check src/ tests/
|
||||
entry: make lint-format
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: pyright
|
||||
- id: typecheck
|
||||
name: pyright type check
|
||||
entry: .venv/bin/pyright
|
||||
entry: make typecheck
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: lint-bandit
|
||||
name: bandit security scan
|
||||
entry: make lint-bandit
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
@@ -27,30 +42,25 @@ repos:
|
||||
|
||||
- id: ansible-lint
|
||||
name: ansible-lint
|
||||
entry: .venv/bin/ansible-lint ansible/
|
||||
entry: make ansible-lint
|
||||
language: system
|
||||
types: [yaml]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: detect-secrets
|
||||
name: detect-secrets
|
||||
entry: .venv/bin/detect-secrets scan --baseline .secrets.baseline
|
||||
- id: workflow-lint
|
||||
name: actionlint (workflow YAML)
|
||||
entry: make workflow-lint
|
||||
language: system
|
||||
files: ^\.gitea/workflows/
|
||||
types: [yaml]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: pytest-cov
|
||||
name: pytest with 100% coverage
|
||||
entry: .venv/bin/pytest tests/unit/ --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100
|
||||
entry: make pytest-cov
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
stages: [pre-push]
|
||||
|
||||
- id: test-all
|
||||
name: run all tests
|
||||
entry: make test-all
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-push]
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.11.11
|
||||
3.12
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
# AGENTS.md — Project Conventions for GRM
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps, set up hooks, install CI tools
|
||||
make install-tools # Install actionlint, git-cliff, act_runner to ~/.local/bin
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||
make test-unit # Unit tests without coverage
|
||||
make molecule # All 6 scenarios on Ubuntu 22.04
|
||||
make molecule-all # All 6 scenarios on all 4 supported OSes
|
||||
make test-all # pytest-cov + molecule
|
||||
make workflow-lint # Static lint of .gitea/workflows/*.yml (actionlint)
|
||||
make workflow-dryrun # Dry-run all workflows in Docker (act_runner exec --dryrun)
|
||||
make workflow-check # workflow-lint + workflow-dryrun
|
||||
```
|
||||
|
||||
`make setup` automatically installs all development tools:
|
||||
- **Python deps** via `pip install -e .[dev]` (includes devx from Gitea PyPI registry, configured by `make configure-gitea-pypi`)
|
||||
- **Post-install setup** via `devx.tools.setup --skip-install` (ansible-galaxy, pre-commit hooks, tea CLI login)
|
||||
- **checkmake** via `devx.tools.install_checkmake` (Makefile linter)
|
||||
- **actionlint, git-cliff, act_runner, tea** via `devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
|
||||
|
||||
## Workflow Verification (Before Push)
|
||||
|
||||
Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
|
||||
|
||||
1. **actionlint** — Static linter that catches syntax errors, invalid
|
||||
expressions, unknown keys, type mismatches, and shellcheck issues.
|
||||
Config: `.gitea/actionlint.yaml` (registers custom `docker` runner label).
|
||||
Installed automatically by `make setup` via `devx.tools.install_tools`.
|
||||
|
||||
2. **act_runner exec --dryrun** — Gitea's own runner in dry-run mode.
|
||||
Validates job dependencies, step ordering, and Docker image selection
|
||||
without starting containers. Installed automatically by `make setup`.
|
||||
|
||||
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 CI `quality` job runs `make setup` (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).
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Python CLI** (`src/gitea_runner_manager/`) — Click-based CLI that delegates to Ansible
|
||||
- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup
|
||||
- **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
|
||||
|
||||
## PR Workflow (Mandatory)
|
||||
|
||||
Every change to master goes through this workflow. No exceptions.
|
||||
|
||||
### Branch Protection (Required Gitea Settings)
|
||||
|
||||
Branch protection and labels are automatically configured by
|
||||
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
|
||||
which runs as a `configure-repo` job in
|
||||
the post-merge workflow on every push to master.
|
||||
|
||||
The following rules are enforced for `master`:
|
||||
- **Require pull request**: No direct pushes to master
|
||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||
- **Require status checks**: CI quality + molecule tests must pass
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 2. Create Branch
|
||||
```bash
|
||||
git checkout master && git pull
|
||||
git checkout -b GRM-N-short-description
|
||||
```
|
||||
|
||||
### 3. Implement Changes
|
||||
- Write code following conventions below
|
||||
- Write/update tests (100% coverage required)
|
||||
- Update documentation (CHANGELOG, README, AGENTS.md as needed)
|
||||
|
||||
### 4. Commit (Conventional Commits)
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
```
|
||||
|
||||
### 5. Push and Create PR
|
||||
- Push: `git push -u origin HEAD` (pre-push hook validates Vikunja task existence via `devx.tools.pre_push_check`)
|
||||
- Create PR: `make create-pr` (creates a PR with title `GRM-N: <vikunja task title>`, auto-derived from the branch name and Vikunja task)
|
||||
- Or both in one step: `make push-with-pr`
|
||||
- PR body: summary of changes, `Closes GRM-N`
|
||||
- Add `ready-to-merge` label **only after review is complete**
|
||||
|
||||
### 6. Review the PR (Mandatory — Before Adding ready-to-merge Label)
|
||||
|
||||
**Review checklist:** Every PR is reviewed against 13 categories covering
|
||||
architecture, code quality, security, i18n, testing, performance,
|
||||
UX, documentation, workflow compliance, maintainability, resource
|
||||
management, backwards compatibility, and logging.
|
||||
|
||||
**Automated review (CI `pr-review` 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
|
||||
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
|
||||
the **[auto]** items in the checklist:
|
||||
|
||||
- Architecture compliance (no subprocess in CLI, no hardcoded URLs)
|
||||
- Best practices (no `print()`, no bare `except`, no `TODO`/`FIXME`,
|
||||
no functions > 50 lines)
|
||||
- Security (no hardcoded secrets, no `shell=True`, no `eval`/`exec`)
|
||||
- i18n (no raw strings in `click.echo()` without `_()` wrapper)
|
||||
- Resource management (no `open()` without `with`, no `Popen()` without cleanup)
|
||||
- Documentation (source changes must include doc updates)
|
||||
- Test coverage (source changes must include test updates)
|
||||
- Commit conventions (conventional commit format on PR commits)
|
||||
|
||||
The automated review posts inline comments on specific lines and
|
||||
includes a summary of the checklist categories. The agent **must** address all
|
||||
`REQUEST_CHANGES` issues before proceeding.
|
||||
|
||||
**Manual review (agent):** After the automated review passes, the agent
|
||||
must go through **every category** listed above and verify
|
||||
the **[manual]** items by reviewing the full diff
|
||||
(`git diff master...HEAD`).
|
||||
|
||||
Post review comments using `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`):
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||
--event REQUEST_CHANGES \
|
||||
--body "Review summary"
|
||||
```
|
||||
|
||||
### 7. Address Review Comments
|
||||
Fix each comment one by one, commit, and push. Re-review until satisfied.
|
||||
|
||||
### 8. Approve and Merge
|
||||
Once all checklist items are verified and comments are addressed, post
|
||||
an approval review with `--checklist-confirmed` and `--checklist-categories`:
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||
--event APPROVE --checklist-confirmed \
|
||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||
--body "All 13 checklist categories verified. Architecture: <summary>. Security: <summary>. Tests: <summary>. Docs: <summary>."
|
||||
```
|
||||
|
||||
The `--checklist-confirmed` flag is **required** for APPROVE events —
|
||||
it attests that the reviewer has gone through every checklist category.
|
||||
The `--checklist-categories` flag is also **required** — it must list at
|
||||
least 8 of the 13 category numbers, ensuring the reviewer actually
|
||||
checked each category rather than rubber-stamping. The review body must
|
||||
be substantive (> 50 characters) — trivial approvals like "LGTM" are
|
||||
rejected.
|
||||
|
||||
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
|
||||
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)
|
||||
4. Squash-merge with title: `GRM-N: <conventional commit message>`
|
||||
5. The post-merge workflow marks the Vikunja task as done
|
||||
6. The release workflow automatically versions, tags, and publishes (see below)
|
||||
|
||||
> **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, producing incorrectly named commits.
|
||||
> The auto-merge script validates the PR title matches the Vikunja task ID
|
||||
> and conventional commit format before merging.
|
||||
|
||||
### CI Path Filtering
|
||||
|
||||
The CI workflow 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.
|
||||
|
||||
### Dynamic Runner Discovery
|
||||
|
||||
Molecule tests are distributed across available Gitea Actions runners
|
||||
dynamically via `devx.molecule.discover_runners`. The `discover-runners`
|
||||
job 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
|
||||
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
|
||||
then to a default of 3.
|
||||
|
||||
**When adding/removing Gitea runners:**
|
||||
1. Repo/org-level runners are auto-detected via the API
|
||||
2. For instance-level runners, update the `MOLECULE_RUNNERS` repo variable
|
||||
3. The workflow automatically scales the matrix to match available runners
|
||||
|
||||
### Automated Release Pipeline
|
||||
|
||||
After a PR is merged to master, the **post-merge workflow**
|
||||
(`.gitea/workflows/post-merge.yml`) runs automatically. This single
|
||||
workflow consolidates release, wiki sync, badge generation, and
|
||||
Vikunja task updates:
|
||||
|
||||
1. **detect-type** — Checks if the commit is a regular merge or a
|
||||
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
|
||||
release commits (the `[skip ci]` tag also prevents re-triggering).
|
||||
|
||||
2. **release** — Runs `devx.ci.release` which:
|
||||
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only
|
||||
workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`,
|
||||
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version
|
||||
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes.
|
||||
- Uses **git-cliff** to calculate the next semver version from conventional commits
|
||||
- Updates `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
||||
- Updates `CHANGELOG.md` with the new version section
|
||||
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
||||
- If lint or tests fail, **aborts immediately** — no commit, no tag
|
||||
- Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents
|
||||
re-triggering post-merge on the release commit)
|
||||
- Creates an annotated tag `vX.Y.Z` on the release commit
|
||||
- Pushes both the commit and tag to master
|
||||
- `--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
|
||||
|
||||
Not all changes require the full CI pipeline or a new release. The project
|
||||
classifies changes into two categories using `devx.ci.classify_changes`:
|
||||
|
||||
**Classification strategy (safe-by-default):** Any file NOT in the explicit
|
||||
workflow-only allowlist is treated as user-facing. This prevents new file
|
||||
types from accidentally skipping releases. Classification is config-driven
|
||||
via `[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**Workflow-only paths** (infrastructure → no release needed):
|
||||
- `.gitea/**` — Gitea Actions workflows
|
||||
- `scripts/**` — Dev tools and CI/CD automation (not part of installed package)
|
||||
- `docs/**` — Documentation
|
||||
- `tests/**` — Test files
|
||||
- `AGENTS.md`, `README.md`, `CHANGELOG.md`, `TROUBLESHOOTING.md`, `CONTRIBUTING.md` — Project docs
|
||||
- `Makefile`, `cliff.toml`, `uv.lock` — Build tooling
|
||||
- `.pre-commit-config.yaml`, `.ansible-lint`, `.checkmake.ini` — Lint config (ruff config is in `pyproject.toml`)
|
||||
- `.env.example`, `.gitignore` — Config
|
||||
- `.devin/**` — Agent/CI tooling config
|
||||
- `hooks/**` — Git hooks
|
||||
- `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts
|
||||
|
||||
**User-facing paths** (tool changes → release needed) — everything else:
|
||||
- `src/gitea_runner_manager/**` — Python CLI source (except `__init__.py`)
|
||||
- `ansible/**` — Ansible role
|
||||
- `pyproject.toml` — Package metadata
|
||||
- Any new file type not in the allowlist
|
||||
|
||||
**devx module structure** (installed from git, not in this repo):
|
||||
- `devx.ci.*` — CI/CD automation (run by workflows): release, publish, auto_merge, classify_changes, detect_release_commit, push_badges, doc_coverage, sync_wiki, distribute_molecule, molecule_ci_guard, discover_runners, notify_failure, post_merge, pr_review, validate_commit_msg
|
||||
- `devx.tools.*` — Dev tools (run locally): check_test_speed, configure_repo, install_checkmake, install_tools, setup, generate_badges
|
||||
- `devx.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule, molecule_ci_guard
|
||||
- `devx.gitea_cli` — Tea CLI wrapper
|
||||
- `devx.i18n` — i18n translation system
|
||||
- `devx.config` — Shared configuration (DEVX_* env vars)
|
||||
- `devx.api_clients` — GiteaClient, VikunjaClient
|
||||
- `devx.exceptions` — APIError and other exceptions
|
||||
|
||||
**CI behavior based on classification:**
|
||||
- **Molecule tests**: Only run when `ansible/` or `.ansible-lint` files change
|
||||
- **Release dry-run**: Only runs when user-facing files change
|
||||
- **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs
|
||||
- **Release workflow**: Skips entirely when no user-facing files changed since last tag
|
||||
|
||||
**AI agents must follow these rules:**
|
||||
- When working on workflow/CI/docs-only changes, use `ci:` or `docs:` commit prefixes
|
||||
- Do NOT bump the version or create tags for workflow-only changes
|
||||
- The `classify_changes` module enforces this automatically — no manual intervention needed
|
||||
|
||||
## Source Code Separation and devx Integration
|
||||
|
||||
The codebase enforces strict separation between the GRM tool and the devx package:
|
||||
|
||||
### Directory Layout
|
||||
|
||||
| Directory | Purpose | Release impact |
|
||||
|-----------|---------|----------------|
|
||||
| `src/gitea_runner_manager/` | User-facing GRM CLI tool | Changes trigger release |
|
||||
| `devx` package (installed from git) | Reusable CI/CD and dev tools | Not in this repo (no release impact) |
|
||||
| `ansible/` | Ansible role for runner setup | Changes trigger release |
|
||||
|
||||
### Import Rules
|
||||
|
||||
1. **`src/gitea_runner_manager/` NEVER imports from devx** — the GRM tool is self-contained
|
||||
2. **devx MAY import from `gitea_runner_manager`** — one-way dependency (devx uses the tool's API clients, config, i18n)
|
||||
3. **Cross-module imports within devx** are allowed (devx modules importing from other devx modules) and must be documented
|
||||
4. **`devx.gitea_cli`** is a shared wrapper around the `tea` CLI — devx modules import from it for Gitea API operations (issues, labels, PRs, releases, reviews)
|
||||
|
||||
### tea CLI Integration
|
||||
|
||||
The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
|
||||
|
||||
**`devx.gitea_cli`** — Python wrapper around `tea` CLI with JSON output parsing:
|
||||
- `TeaCLI.create_issue()` — Create issues with labels
|
||||
- `TeaCLI.list_labels()` / `TeaCLI.create_label()` / `TeaCLI.add_label()` — Label management
|
||||
- `TeaCLI.create_pr()` / `TeaCLI.merge_pr()` / `TeaCLI.review_pr()` — Pull request operations
|
||||
- `TeaCLI.create_release()` / `TeaCLI.list_releases()` — Release management
|
||||
- `TeaCLI.list_branches()` — Branch listing
|
||||
|
||||
**Modules using tea (via `devx.gitea_cli`):**
|
||||
- `devx.ci.publish` — Creates Gitea releases via `tea releases create`
|
||||
- `devx.ci.notify_failure` — Creates issues via `tea issues create` (falls back to `GiteaClient` if tea not installed)
|
||||
- `devx.tools.configure_repo` — Creates labels via `tea labels create` (falls back to `GiteaClient` if tea fails; branch protection still uses `GiteaClient` since tea only supports basic protect/unprotect)
|
||||
|
||||
**Operations still using `GiteaClient` (not supported by tea):**
|
||||
- PR reviews (`devx.ci.pr_review`) — tea v0.14.1 only supports interactive reviews
|
||||
- Wiki page management (`devx.ci.sync_wiki`)
|
||||
- Commit status checks (`devx.ci.auto_merge`)
|
||||
- Runner discovery (`devx.molecule.discover_runners`)
|
||||
- Branch protection with detailed config (`devx.tools.configure_repo`)
|
||||
- PR file/commit listing (`devx.ci.pr_review`)
|
||||
|
||||
### PYTHONPATH Configuration
|
||||
|
||||
Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `gitea_runner_manager`:
|
||||
|
||||
| PYTHONPATH | When to use | Example modules |
|
||||
|------------|-------------|-----------------|
|
||||
| `src` | Module imports from `gitea_runner_manager` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.pr_review`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` |
|
||||
| (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.molecule.molecule_ci_guard`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` |
|
||||
|
||||
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
||||
```yaml
|
||||
- name: Run module
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: python -m devx.ci.example
|
||||
```
|
||||
|
||||
**Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `gitea_runner_manager`.
|
||||
|
||||
### Shared Constants
|
||||
|
||||
`devx.molecule.platforms` is the single source of truth for the molecule
|
||||
platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
|
||||
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
|
||||
avoids dev tools importing directly from CI modules.
|
||||
|
||||
2. **Publish job** (in `post-merge.yml`, needs: release):
|
||||
- Runs after the release job creates a tag
|
||||
- Gets the tag from `needs.release.outputs.tag`
|
||||
- Builds the Python package
|
||||
- Publishes to the Gitea PyPI registry
|
||||
- Creates a Gitea release with git-cliff-generated release notes
|
||||
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
|
||||
|
||||
### git-cliff Commit Preprocessing
|
||||
|
||||
Merge commits on master have the format `GRM-N: <conventional commit>`. The
|
||||
`GRM-N: ` prefix is not a valid conventional commit prefix, so `cliff.toml`
|
||||
includes a `commit_preprocessors` entry that strips it before parsing. This
|
||||
ensures all merged work appears in the changelog.
|
||||
|
||||
### Version Bumping Rules (git-cliff)
|
||||
|
||||
| Commit type | Version bump |
|
||||
|-------------|-------------|
|
||||
| `feat:` | minor (0.X.0) |
|
||||
| `fix:` | patch (0.0.X) |
|
||||
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
||||
| `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) |
|
||||
|
||||
The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version.
|
||||
|
||||
### Title Format Summary
|
||||
|
||||
| What | Format | Example |
|
||||
|------|--------|---------|
|
||||
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
|
||||
| Branch commits | `<conventional commit>` | `feat: add review script` |
|
||||
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
|
||||
| Merge commit | `GRM-N: <conventional commit>` | `GRM-33: feat: add review script` |
|
||||
|
||||
### Configuration
|
||||
|
||||
The devx package is configured via `DEVX_*` environment variables:
|
||||
- `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers
|
||||
- `DEVX_VIKUNJA_PROJECT_ID=6` — Vikunja project ID for task tracking
|
||||
- `DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py` — Path to the version source file
|
||||
|
||||
Change classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`, which defines the workflow-only and user-facing path patterns.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Python 3.12+ required (ruff/pyright target `py312`)
|
||||
- 100% test coverage required (`--cov-fail-under=100`)
|
||||
- Conventional commits on feature branches (no `GRM-N:` prefix)
|
||||
- Branch names must include `GRM-N` task ID
|
||||
- Line length: 120 chars
|
||||
- Secrets are passed via temp JSON files, never on the command line (CWE-214)
|
||||
- CI triggers only on `opened` and `synchronize` PR events (not `labeled`)
|
||||
|
||||
## Ansible Role Structure
|
||||
|
||||
```
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
||||
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files)
|
||||
|
||||
## Molecule Scenarios
|
||||
|
||||
6 scenarios: `default`, `multi-instance`, `lifecycle`, `template-content`, `deregister`, `update`
|
||||
4 platforms: `ubuntu-2204`, `ubuntu-2404`, `debian-12`, `archlinux`
|
||||
Platform list is defined in `devx.molecule.platforms` (single source of truth)
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user` calls — this is expected (systemd module doesn't support user services) and skipped in `.ansible-lint`
|
||||
- Molecule Docker driver may print "Event loop is closed" warnings on interrupt — harmless
|
||||
|
||||
## Documentation-as-Code
|
||||
|
||||
All documentation lives in `/docs/` and is synced to the Gitea wiki automatically.
|
||||
|
||||
### Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.md # Wiki homepage
|
||||
├── mapping.json # File-to-wiki-page title mapping
|
||||
├── user/ # User documentation
|
||||
│ ├── getting-started.md
|
||||
│ ├── installation.md
|
||||
│ ├── cli-commands.md
|
||||
│ ├── troubleshooting.md
|
||||
│ └── faq.md
|
||||
└── tech/ # Technical documentation
|
||||
├── architecture.md
|
||||
├── development-setup.md
|
||||
├── ci-cd-workflow.md
|
||||
├── testing-strategy.md
|
||||
├── decision-log.md
|
||||
└── contributing.md
|
||||
```
|
||||
|
||||
### Wiki Sync
|
||||
|
||||
- **On merge to master**: `sync-wiki.yml` workflow runs `devx.ci.sync_wiki` which pushes all `/docs/` content to the Gitea wiki via API
|
||||
- **On release tag**: Same sync runs, plus the wiki is tagged with the release version
|
||||
- `mapping.json` maps each file path to a wiki page title (e.g., `user/getting-started.md` → `Getting-Started`)
|
||||
- README.md is a lean entry point with links to the wiki — no detailed content
|
||||
|
||||
### Documentation Coverage
|
||||
|
||||
- `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)
|
||||
- Enforced: 100% coverage for public CLI commands and major architectural components
|
||||
|
||||
### Updating Documentation
|
||||
|
||||
1. Edit files in `/docs/`
|
||||
2. If adding a new page, add it to `docs/mapping.json`
|
||||
3. Commit and create a PR (standard PR workflow)
|
||||
4. On merge, wiki is automatically synced
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.12.2] - 2026-06-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Bump devx to 0.26.3 (latest with pinned deps)
|
||||
|
||||
## [0.12.1] - 2026-06-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add approval step to auto-merge workflow using REVIEW_GITEA_TOKEN
|
||||
|
||||
## [0.12.0] - 2026-06-28
|
||||
|
||||
### Features
|
||||
|
||||
- Upgrade all dependencies, add trigger-workflow command
|
||||
|
||||
## [0.11.1] - 2026-06-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Makefile HOST/NAME requirement errors, add restart and list targets
|
||||
|
||||
## [0.11.0] - 2026-06-28
|
||||
|
||||
### Features
|
||||
|
||||
- Unified --become-password-file, --verbose, --no-status, labels fix
|
||||
|
||||
## [0.10.3] - 2026-06-27
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Install hadolint on-the-fly in setup-image
|
||||
- Revert EXTRAS=ci default in setup-image
|
||||
- Add EXTRAS=ci to all setup-image calls, workflow-level CI_GITEA_TOKEN
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove hadolint on-the-fly install workaround
|
||||
- Use devx Makefile aliases, bump devx>=0.23.0
|
||||
|
||||
## [0.10.2] - 2026-06-27
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Setup-image configures Gitea PyPI registry and shows pip errors
|
||||
- Gate auto-merge on release-dry-run and unmask failures
|
||||
- Bump devx>=0.22.0 and remove REPO_TOKEN alias
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename REPO_TOKEN to CI_GITEA_TOKEN, consolidate env vars
|
||||
|
||||
## [0.10.1] - 2026-06-27
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Set PYTHONPATH=src in publish Install CI tools step
|
||||
|
||||
### Refactor
|
||||
|
||||
- Replace duplicated Makefile targets with devx.mak aliases
|
||||
- Consolidate publish.yml into post-merge.yml
|
||||
|
||||
## [0.10.0] - 2026-06-26
|
||||
|
||||
### Features
|
||||
|
||||
- Adopt devx tools, devx.mak fragment, ci extra, remove legacy install-devx
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Always run publish in post-merge (idempotent)
|
||||
|
||||
## [0.9.0] - 2026-06-24
|
||||
|
||||
### Features
|
||||
|
||||
- Adopt devx v0.11.1 across Makefile and workflows
|
||||
- Add Polish as officially supported language
|
||||
|
||||
## [0.8.1] - 2026-06-24
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Repair publish workflow and add publish step to post-merge
|
||||
- Add build/twine to ci deps, activate venv in notify_failure
|
||||
|
||||
## [0.8.0] - 2026-06-24
|
||||
|
||||
### Features
|
||||
|
||||
- Remove .taskid file, use branch name only for task ID
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add workflow_dispatch to publish workflow and update devx to 0.9.12
|
||||
|
||||
## [0.7.0] - 2026-06-24
|
||||
|
||||
### Features
|
||||
|
||||
- Switch devx installation from git to Gitea PyPI registry
|
||||
- Adopt per-test timing quality gate from devx 0.7.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update devx to v0.4.2 and fix workflow env vars
|
||||
- Pin devx to v0.4.3 to fix post-merge workflow failures
|
||||
- Pin devx to v0.4.4 to fix validate-commit-msg and sync-wiki
|
||||
- Rewrite CHANGELOG with correct version ordering and missing sections
|
||||
- Lower test speed threshold to 4s and update devx to v0.8.2
|
||||
- Retrospective fixes for CI/CD friction
|
||||
- Replace stale badge SHA URLs with raw/branch/badges/
|
||||
|
||||
## [0.6.4] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update devx to v0.4.2 and fix workflow env vars
|
||||
- Pin devx to v0.4.3 to fix post-merge workflow failures
|
||||
- Pin devx to v0.4.4 to fix validate-commit-msg and sync-wiki
|
||||
|
||||
## [0.6.3] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add scripts/** to infrastructure classification config
|
||||
|
||||
## [0.6.2] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Include lint extras in setup-ci and setup-release
|
||||
- Use commit SHA URLs for badges to bypass Gitea cache
|
||||
- Make sync-wiki and vikunja depend on release
|
||||
- Pin devx to v0.4.0, fix cliff.toml preprocessor, bump to v0.7.0
|
||||
|
||||
### Refactor
|
||||
|
||||
- Fully automate PR merge — no manual label/review needed
|
||||
- Require tea CLI everywhere, fail on missing Vikunja task
|
||||
- Separate GRM and CI translations with validation
|
||||
- Migrate from scripts/ to devx package
|
||||
|
||||
## [0.6.1] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Badges always update on release commits + fix configure-repo PYTHONPATH
|
||||
- Enforce commit message convention on master with CI validation
|
||||
- Post-merge workflow failures (4 jobs)
|
||||
|
||||
## [0.6.0] - 2026-06-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Molecule-tests matrix runner-index renders as empty for 0
|
||||
- Use 1-based runner indices for Gitea Actions compatibility
|
||||
- Molecule-tests static matrix and role_dir path fix
|
||||
- Auto-merge label condition uses pull_request.labels
|
||||
- Revert review_pr.py to GiteaClient (tea v0.14.1 is interactive-only) (#70)
|
||||
|
||||
### Revert
|
||||
|
||||
- Remove v0.6.0 release (no user-facing changes)
|
||||
|
||||
## [0.5.0] - 2026-06-22
|
||||
|
||||
### Features
|
||||
|
||||
- Enforce commit naming conventions and workflow discipline
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Clean up infrastructure-only releases and fix release classification
|
||||
- Rewrite changelog and re-tag releases at user-facing milestones
|
||||
|
||||
## [0.4.0] - 2026-06-21
|
||||
|
||||
### Features
|
||||
|
||||
- Replace inline workflow scripts with tested Python modules
|
||||
- User-friendly click errors with i18n in configure_repo
|
||||
- Bandit integration (#1)
|
||||
- Auto-delete branch after merge in configure_repo script
|
||||
- Add runner labels support and refactor i18n to JSON
|
||||
- Parallel molecule runner with kill-on-first-failure
|
||||
- Cross-runner molecule cancellation via Gitea API polling
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Set runner_mode to binary in multi-instance converge
|
||||
- Skip systemd operations in lifecycle molecule when unavailable
|
||||
- Improve make setup with version guard, pre-push hooks and commit-msg validator
|
||||
- Enforce GRM-N: conventional on master commits and PR titles
|
||||
- Remove molecule tests from pre-push hooks
|
||||
- Resolve bandit security warnings in source code and tests
|
||||
- CI pipeline for rootless Docker runners
|
||||
- CI workflows for rootless runner compatibility
|
||||
- Vikunja task resolution pagination in post_merge.py
|
||||
- Use PUT instead of POST for Vikunja task comments
|
||||
- Parse pytest output with warnings in check_test_speed
|
||||
- Use systemd as container command for rootless molecule tests
|
||||
- Add Docker APT repository before installing docker-ce
|
||||
- Use deb822_repository for Docker APT repo (proper GPG handling)
|
||||
- Dearmor Docker GPG key with gpg --dearmor for apt_repository
|
||||
- Use bash for gpg dearmor (pipefail not available in sh)
|
||||
- Install curl, gpg, ca-certificates in molecule prepare
|
||||
- Separate apt update after adding Docker repo, use variable for repo string
|
||||
- Add apt source debug tasks, fix arch mapping for Docker repo
|
||||
- Fail-fast CI, write Docker apt source directly, fix arch mapping
|
||||
- Skip rootless Docker daemon startup in molecule tests
|
||||
- Gate all Docker-dependent tasks behind docker_rootless_setup
|
||||
- Catch TimeoutExpired in parallel runner wait loop
|
||||
- Stream molecule subprocess output to CI logs
|
||||
- Run molecule pairs sequentially within each CI runner
|
||||
- Guard all systemctl --user tasks with docker_rootless_setup
|
||||
- Guard handler systemctl --user calls with docker_rootless_setup
|
||||
- Make user_setup and download tasks idempotent
|
||||
- Use gnupg instead of gpg package name on Arch Linux
|
||||
- Add default(0) to gitea_runner_uid in environment blocks
|
||||
- Set runner_name in deregister verify.yml
|
||||
- Security, dead code, idempotence, and documentation cleanup
|
||||
- Use content_base64 for Gitea wiki API, add --verify flag (#33)
|
||||
- Wiki links, add --strict integrity check for wiki sync (#34)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Standardise pre-commit hooks on make targets
|
||||
- Use http.HTTPStatus constants instead of magic numbers
|
||||
- Rework all scripts to use click and i18n
|
||||
- *(scripts)* Centralize constants, API clients, and HTTP status codes
|
||||
- Rootless Docker, fix auto-merge, molecule platform matrix
|
||||
|
||||
## [0.3.2] - 2026-06-21
|
||||
|
||||
### Features
|
||||
|
||||
- Smart CI and release skipping for workflow-only changes
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Set PYTHONPATH=. for release.py to find scripts.ci module (#32)
|
||||
|
||||
### Refactor
|
||||
|
||||
- Split CI scripts, fix release PYTHONPATH, dynamic runner discovery
|
||||
|
||||
## [0.3.1] - 2026-06-21
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Use correct Gitea 1.26 wiki API endpoints
|
||||
|
||||
## [0.3.0] - 2026-06-21
|
||||
|
||||
### Features
|
||||
|
||||
- Implement documentation-as-code with wiki sync and doc-coverage
|
||||
|
||||
## [0.2.2] - 2026-06-21
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Bypass commit-msg hook for release commits
|
||||
- Enforce tests pass before tagging a release
|
||||
|
||||
## [0.2.1] - 2026-06-21
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Strip git-cliff header from CHANGELOG.md updates
|
||||
|
||||
## [0.2.0] - 2026-06-18
|
||||
|
||||
### Features
|
||||
|
||||
- Parameterize all hardcoded configuration values as Ansible variables
|
||||
- Add GITEA_ADMIN_TOKEN support for integration test
|
||||
- Add AnsibleExecutor and i18n modules
|
||||
- Integrate AnsibleExecutor and i18n into CLI and RunnerManager
|
||||
- Add systemd template units and multi-instance Ansible support
|
||||
- Add lifecycle CLI commands and RunnerManager extensions
|
||||
- Add runner registry for simplified CLI UX
|
||||
- Add translated operation report for success and failure cases
|
||||
- Replace print() with stdlib logging module
|
||||
- Use click.echo() for user-facing messages with dual logging
|
||||
- Add colorized output for better visual feedback
|
||||
- Add --force flag to grm remove for unreachable runners
|
||||
- Make --ask-become-pass the default behavior
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Resolve idempotence issues and testing infrastructure
|
||||
- Remove recursive variable definitions in install and update playbooks
|
||||
- Add timeout to runner registration to prevent indefinite hangs
|
||||
- Override Docker container entrypoint to bypass run.sh wrapper
|
||||
- Set Docker working dir to /data for .runner persistence
|
||||
- Make integration test conditional on admin API accessibility
|
||||
- Remove recursive var definitions from install-runner.yml
|
||||
- Convert runner config from TOML to YAML format
|
||||
- Rewrite integration test to verify .runner file and container health instead of unreliable API checks
|
||||
- Eliminate duplicate console output, restore GRM_LOG_LEVEL filtering
|
||||
- Make grm list retrieve runner status correctly
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove dead code and legacy artifacts
|
||||
- Migrate source terminology from act_runner to gitea_runner
|
||||
- Consolidate systemd checks and deduplicate role structure
|
||||
- Deduplicate CLI, remove dead code, move validation to business layer
|
||||
- Resolve_runner returns gitea_url, add --url CLI option, force remove improvements, code quality fixes
|
||||
|
||||
## [0.1.0] - 2026-06-17
|
||||
|
||||
### Features
|
||||
|
||||
- Initial implementation of Gitea Runner Manager
|
||||
@@ -0,0 +1,58 @@
|
||||
# Contributing to GRM
|
||||
|
||||
Thank you for contributing to Gitea Runner Manager (GRM)!
|
||||
|
||||
## Branch Naming
|
||||
|
||||
All feature branches **must** include a `GRM-N` prefix corresponding to the Vikunja task identifier. Examples:
|
||||
|
||||
- `GRM-19`
|
||||
- `GRM-19-fix-bug`
|
||||
- `GRM-42-add-update-command`
|
||||
|
||||
The `GRM-N` prefix is mandatory — CI extracts it for merge messages and Vikunja updates.
|
||||
|
||||
## Commit Format
|
||||
|
||||
### Feature branches
|
||||
Use **conventional commits** on feature branches:
|
||||
|
||||
```
|
||||
feat: add new command
|
||||
fix: resolve timeout issue
|
||||
chore: update dependencies
|
||||
docs: improve README
|
||||
```
|
||||
|
||||
Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`, `build`, `revert`, `BREAKING CHANGE`.
|
||||
|
||||
**Do NOT** include the `GRM-N:` prefix in commit messages on feature branches.
|
||||
|
||||
### Master branch (squash merges)
|
||||
Squash commits on `master` must follow:
|
||||
|
||||
```
|
||||
GRM-N: <conventional commit message>
|
||||
```
|
||||
|
||||
Example: `GRM-24: fix: resolve molecule idempotence`.
|
||||
|
||||
This format is enforced by the auto-merge workflow, which validates the PR title is a conventional commit before squash-merging and prepending the task ID.
|
||||
|
||||
## Local Testing
|
||||
|
||||
```bash
|
||||
make test-all # Runs pytest-cov + molecule
|
||||
make lint-all # Runs ruff, pyright, bandit, ansible-lint, checkmake
|
||||
make lint-bandit # Security scan with bandit
|
||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||
make molecule # All 6 molecule scenarios
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
- **ruff**: Line length 120
|
||||
- **pyright**: Strict mode
|
||||
- **bandit**: Security scan for Python code (no high/medium severity issues)
|
||||
- **Test coverage**: 100% required
|
||||
- **ansible-lint**: For all Ansible content
|
||||
@@ -1,4 +1,6 @@
|
||||
.PHONY: all setup install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule test-all clean
|
||||
.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
|
||||
.PHONY: configure-gitea-pypi
|
||||
.PHONY: create-task create-pr push-with-pr git-push
|
||||
|
||||
PYTHON := python3
|
||||
VENV := .venv
|
||||
@@ -7,11 +9,85 @@ CHECKMAKE := $(shell command -v checkmake 2>/dev/null || echo $(HOME)/go/bin/che
|
||||
|
||||
all: setup
|
||||
|
||||
setup: $(VENV)/bin/activate .env activate-scripts checkmake
|
||||
$(BIN)/pip install -e ".[dev]"
|
||||
$(BIN)/ansible-galaxy collection install -r ansible/requirements.yml
|
||||
$(BIN)/pre-commit install
|
||||
@echo "Setup complete. Activate the virtual environment with: source .venv/bin/activate"
|
||||
# --- devx.mak include (shared Makefile targets) -------------------------------
|
||||
# Set DEVX_PYTHON before including devx.mak so it uses the venv Python.
|
||||
DEVX_PYTHON := $(BIN)/python
|
||||
DEVX_VENV := $(VENV)
|
||||
DEVX_BIN := $(BIN)
|
||||
DEVX_COV_PKG := src/gitea_runner_manager
|
||||
DEVX_TEST_PATHS := tests/ scripts/tests/
|
||||
DEVX_LINT_PATHS := src/ scripts/ tests/
|
||||
|
||||
# Include shared targets from devx package (create-task, create-pr, push-with-pr,
|
||||
# check-config, workflow-lint, lint-ruff, clean, venv, .env, activate-scripts,
|
||||
# install-hooks, install-tools, configure-gitea-pypi, checkmake, etc.)
|
||||
# Silent if devx not installed yet — run 'make setup' first.
|
||||
DEVX_MAK := $(shell $(BIN)/python -c \
|
||||
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
|
||||
2>/dev/null)
|
||||
-include $(DEVX_MAK)
|
||||
|
||||
# Full setup for local development (all deps, tools, collections, hooks)
|
||||
# devx is installed via pip install -e .[dev] (devx is in dev extra)
|
||||
setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
|
||||
@$(PIP_INSTALL) install -e '.[dev]'
|
||||
@$(BIN)/python -m devx.tools.install_checkmake
|
||||
@$(BIN)/python -m devx.tools.install_tools
|
||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
|
||||
|
||||
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
|
||||
# (detect-changes, discover-runners, pr-review, sync-wiki, badges)
|
||||
# badges job runs generate_badges.py which needs ruff, pyright, bandit
|
||||
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||
@$(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
|
||||
|
||||
# Setup for the quality job (lint + test deps, actionlint tool)
|
||||
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||
@$(BIN)/python -m devx.tools.install_tools
|
||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
|
||||
|
||||
# Full setup for molecule testing (needs ansible, molecule, collections)
|
||||
setup-molecule: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||
@$(PIP_INSTALL) install -e '.[ci,molecule]'
|
||||
@$(BIN)/python -m devx.tools.install_tools
|
||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-pre-commit --no-tea-login
|
||||
|
||||
# Setup for release jobs (needs git-cliff, tea, and lint tools for release.py)
|
||||
setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||
@$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea
|
||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit
|
||||
|
||||
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
|
||||
# Usage: make setup-image (runtime deps only, devx from image)
|
||||
# make setup-image EXTRAS=lint (runtime + lint deps, e.g. ansible-lint)
|
||||
# make setup-image EXTRAS=ci,lint (runtime + ci + lint deps, upgrades devx)
|
||||
# NOTE: Cannot alias to devx-setup-image because the venv must exist before
|
||||
# devx.mak can be included (chicken-and-egg). This standalone target creates
|
||||
# the venv symlink first, then installs the project.
|
||||
setup-image:
|
||||
@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; \
|
||||
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
||||
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||
|
||||
# Helper: run pip install with Gitea registry configured
|
||||
# Usage: $(PIP_INSTALL) install -e '.[ci,lint]'
|
||||
PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
|
||||
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
|
||||
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; \
|
||||
$(BIN)/pip
|
||||
|
||||
$(VENV)/bin/activate:
|
||||
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
|
||||
$(PYTHON) -m venv $(VENV)
|
||||
$(BIN)/pip install --upgrade pip setuptools wheel
|
||||
|
||||
.env:
|
||||
@if [ ! -f .env ]; then \
|
||||
@@ -19,53 +95,109 @@ setup: $(VENV)/bin/activate .env activate-scripts checkmake
|
||||
echo "Created .env from .env.example — please edit it with your credentials."; \
|
||||
fi
|
||||
|
||||
$(VENV)/bin/activate:
|
||||
$(PYTHON) -m venv $(VENV)
|
||||
$(BIN)/pip install --upgrade pip setuptools wheel
|
||||
|
||||
activate-scripts: $(VENV)/bin/activate
|
||||
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
|
||||
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
|
||||
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
|
||||
|
||||
checkmake:
|
||||
@which checkmake >/dev/null 2>&1 || (which go >/dev/null 2>&1 && go install github.com/mrtazz/checkmake/cmd/checkmake@latest) || (echo "Warning: checkmake not installed. Install Go and run: go install github.com/mrtazz/checkmake/cmd/checkmake@latest" && exit 0)
|
||||
|
||||
install:
|
||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
|
||||
$(BIN)/python grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),)
|
||||
$(BIN)/grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
update:
|
||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make update HOST=192.168.1.10"; exit 1; fi
|
||||
$(BIN)/python grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),)
|
||||
$(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
lint:
|
||||
$(BIN)/ruff check src/ tests/
|
||||
$(BIN)/ruff format --check src/ tests/
|
||||
$(BIN)/pyright
|
||||
start:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make start NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm start $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
stop:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make stop NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm stop $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
restart:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make restart NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm restart $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
enable:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make enable NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm enable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
disable:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make disable NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm disable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
status:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make status NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm status $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
remove:
|
||||
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make remove NAME=runner1"; exit 1; fi
|
||||
$(BIN)/grm remove $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(FORCE),--force,) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
list:
|
||||
$(BIN)/grm list $(if $(NO_STATUS),--no-status,) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||
|
||||
# --- Aliases to devx.mak targets ----------------------------------------------
|
||||
lint-ruff: devx-lint-ruff
|
||||
lint-format: devx-lint-format
|
||||
typecheck: devx-typecheck
|
||||
lint-bandit: devx-lint-bandit
|
||||
lint-deps: devx-lint-deps
|
||||
lint: devx-lint
|
||||
checkmake: devx-checkmake
|
||||
install-tools: devx-install-tools
|
||||
install-hooks: devx-install-hooks
|
||||
clean: devx-clean
|
||||
test-unit: devx-test-unit
|
||||
|
||||
# Override devx-pytest-cov to cover both src/ and scripts/
|
||||
pytest-cov:
|
||||
@$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100
|
||||
workflow-lint: devx-workflow-lint
|
||||
workflow-dryrun: devx-workflow-dryrun
|
||||
workflow-check: devx-workflow-check
|
||||
|
||||
configure-gitea-pypi:
|
||||
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
|
||||
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
|
||||
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 (CI_GITEA_TOKEN present)."
|
||||
|
||||
ansible-lint:
|
||||
$(BIN)/ansible-lint ansible/
|
||||
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
|
||||
|
||||
makefile-lint:
|
||||
@$(CHECKMAKE) Makefile
|
||||
@if command -v $(CHECKMAKE) >/dev/null 2>&1 || [ -x "$(CHECKMAKE)" ]; then \
|
||||
$(CHECKMAKE) Makefile; \
|
||||
else \
|
||||
echo "checkmake not found, skipping Makefile lint"; \
|
||||
fi
|
||||
|
||||
lint-all: lint ansible-lint makefile-lint
|
||||
lint-all: lint ansible-lint makefile-lint workflow-lint
|
||||
|
||||
test-unit:
|
||||
$(BIN)/pytest tests/unit/ -v
|
||||
test-integration:
|
||||
$(BIN)/pytest tests/integration/ -v --no-cov
|
||||
|
||||
pytest-cov:
|
||||
$(BIN)/pytest tests/unit/ -v --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100
|
||||
MOLECULE := $(realpath $(BIN))/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
|
||||
molecule:
|
||||
cd ansible/roles/gitea-runner && $(BIN)/molecule test
|
||||
@set -e; for s in default multi-instance lifecycle template-content deregister update; do if [ "$$s" = "default" ]; then $(MOLECULE_BASE) test; else $(MOLECULE_BASE) test -s $$s; fi; done
|
||||
|
||||
# All scenarios on all supported platforms (sequential; use CI matrix for parallel execution)
|
||||
molecule-all:
|
||||
@$(BIN)/python -m devx.molecule.molecule_all --bin "$(BIN)"
|
||||
|
||||
test: test-all
|
||||
|
||||
test-all: pytest-cov molecule
|
||||
|
||||
clean:
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
rm -rf .coverage htmlcov/ .molecule/
|
||||
# --- Vikunja task and PR management (via devx.mak fragment) -------------------
|
||||
# Aliases for project-specific target names
|
||||
create-task: devx-create-task
|
||||
create-pr: devx-create-pr
|
||||
push-with-pr: devx-push-with-pr
|
||||
git-push: devx-push
|
||||
|
||||
@@ -2,80 +2,404 @@
|
||||
|
||||
A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts.
|
||||
|
||||
> **Pronunciation note:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM" in Latin letters) — the Bulgarian word for **thunder**. Wherever there are clouds, there may be thunders. This is an open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||
Each runner runs in an isolated **rootless Docker** environment under a dedicated system user, enabling multiple runners to operate in parallel on the same host without conflicts. GRM handles the entire runner lifecycle — from initial installation and registration with Gitea, through start/stop/enable/disable operations, to clean removal with deregistration.
|
||||
|
||||
> **Pronunciation:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM") — the Bulgarian word for **thunder**. An open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why GRM?
|
||||
|
||||
Managing Gitea Actions runners manually is tedious and error-prone: you need to create system users, set up rootless Docker, download and configure the runner binary, register it with Gitea, create systemd services, and set up Docker prune timers — all per runner instance. GRM automates this entire process with a single command, and ensures it is idempotent (safe to re-run).
|
||||
|
||||
Key problems GRM solves:
|
||||
|
||||
- **Isolation without root**: Each runner operates under a dedicated system user with its own rootless Docker daemon, so runners on the same host never interfere with each other or with the host's Docker installation.
|
||||
- **Reproducible setup**: The Ansible role is idempotent — running `grm install` twice produces zero changes on the second run, so it is safe for CI/CD pipelines and configuration management.
|
||||
- **Full lifecycle management**: Install, start, stop, enable (boot persistence), disable (deregister), update the binary, check status, and remove — all from one CLI.
|
||||
- **Local registry**: GRM stores connection metadata locally, so after installation you manage runners by name alone without repeating SSH credentials.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple and focused** — no unnecessary features.
|
||||
- **Secure** — no hardcoded secrets, uses scoped tokens.
|
||||
- **Idempotent** — can be run multiple times safely.
|
||||
- **Flexible** — accepts a plain IP address or hostname, and allows specifying the SSH user and private key.
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
- Arch Linux
|
||||
- Ubuntu 22.04 / 24.04 / 26.04
|
||||
- Debian 12 / 13
|
||||
- **Rootless Docker isolation** — Each runner gets its own rootless Docker daemon under a dedicated system user (`grm-<name>`), with its own Docker socket at `/run/user/<UID>/docker.sock`.
|
||||
- **Multi-instance support** — Install and manage multiple isolated runners on the same host, each with independent users, data directories, and systemd user services.
|
||||
- **Idempotent Ansible role** — Safe to re-run; the role detects existing state and only applies changes when needed.
|
||||
- **Full lifecycle CLI** — `install`, `update`, `start`, `stop`, `enable`, `disable`, `status`, `remove`, `list` — all from a single `grm` command.
|
||||
- **Automatic integration testing** — Every installation runs an integration test that verifies the `.runner` registration file and systemd service state.
|
||||
- **Docker prune automation** — A systemd user timer automatically prunes old Docker images and volumes on a daily schedule.
|
||||
- **Local runner registry** — Connection details are stored in `~/.local/share/grm/runners.json`, so lifecycle commands work by runner name alone.
|
||||
- **Internationalisation** — Console messages support English, Bulgarian, German, Russian, Chinese, and Polish via the `GRM_LANG` environment variable.
|
||||
- **Security-conscious** — Secrets (registration tokens) are passed via temporary JSON files with `0600` permissions, never on the command line (CWE-214).
|
||||
- **Comprehensive CI/CD** — 100% test coverage, automated releases via conventional commits and git-cliff, Molecule tests across 4 OS platforms.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Developer Setup
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git
|
||||
cd gitea-runner-manager
|
||||
pyenv install 3.11.11
|
||||
pyenv local 3.11.11
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Checkout latest stable release
|
||||
make setup
|
||||
cp .env.example .env # Edit with your Gitea URL and tokens
|
||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
```
|
||||
|
||||
### Configure Gitea Credentials
|
||||
> **Important:** Always checkout the latest release tag before running `make setup`. The `master` branch may contain unreleased changes that are not yet stable. The command above automatically selects the most recent tagged release. To see all available releases, run `git tag --sort=-version:refname` or check the [releases page](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases).
|
||||
|
||||
> **Tokens:** You need two tokens from your Gitea instance — a **registration token** to register runners, and an **admin API token** for optional post-install verification. See [Getting Started](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Getting-Started.-) for detailed setup instructions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### On your local machine (where you run `grm`)
|
||||
|
||||
- **Python 3.12+** — GRM targets Python 3.12 and requires it for development setup.
|
||||
- **Ansible** — Installed automatically by `make setup` (via pip). GRM delegates all remote operations to `ansible-playbook`.
|
||||
- **SSH access** — A private key that grants access to the target host(s) as a user with sudo privileges.
|
||||
|
||||
### On the target host(s) (where runners will be installed)
|
||||
|
||||
- **SSH server** — Reachable via the key specified with `--key`.
|
||||
- **Sudo access** — The SSH user must have sudo privileges for creating system users, installing packages, and configuring rootless Docker. By default, you will be prompted for the sudo password interactively. For automation, configure passwordless sudo and pass `--no-ask-become-pass`.
|
||||
- **Docker** — Installed automatically by the Ansible role (rootless mode). No pre-existing Docker installation is required.
|
||||
- **systemd** — Required for user services and lingering. All supported OSes ship with systemd.
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: From source (recommended for full control)
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Latest stable release
|
||||
make setup
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
`make setup` performs the following:
|
||||
|
||||
1. Verifies Python 3.12+ is installed
|
||||
2. Creates a virtualenv in `.venv`
|
||||
3. Installs all Python dependencies (including Ansible, Click, python-dotenv)
|
||||
4. Creates `.env` from `.env.example` if not present
|
||||
5. Installs development tools (actionlint, git-cliff, act_runner, checkmake)
|
||||
6. Sets up pre-commit hooks
|
||||
|
||||
### Option 2: Via pip (for using GRM without the full repo)
|
||||
|
||||
GRM is published to the Gitea PyPI registry at
|
||||
`https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple`.
|
||||
The registry is publicly readable — no authentication required to install.
|
||||
|
||||
**Quick install (one-off):**
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
**Persistent configuration (recommended):**
|
||||
|
||||
Add the registry to `~/.pip/pip.conf` so future `pip install` commands find
|
||||
GRM automatically:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
Then install normally:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks
|
||||
and role are bundled with the package, so `grm install` works out of the box.
|
||||
For development or access to Make targets, clone the repository (Option 1).
|
||||
|
||||
### Post-install configuration
|
||||
|
||||
After installation, create your `.env` file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# GITEA_URL=https://git.example.com
|
||||
# GITEA_TOKEN=your-personal-access-token
|
||||
# Edit .env with your Gitea URL and registration token
|
||||
```
|
||||
|
||||
The token needs `admin:runner` scope.
|
||||
See the [Configuration](#configuration) section below for details.
|
||||
|
||||
### Install a Runner
|
||||
## CLI Commands Overview
|
||||
|
||||
Using the CLI:
|
||||
GRM provides a single `grm` command with subcommands for the full runner lifecycle:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `grm install <host>` | Install and configure a runner on a remote host |
|
||||
| `grm update <host>` | Update the Gitea Runner binary on a remote host |
|
||||
| `grm start <name>` | Start a registered runner |
|
||||
| `grm stop <name>` | Stop a registered runner |
|
||||
| `grm restart <name>` | Restart a runner (stop, prune Docker images, start) |
|
||||
| `grm enable <name>` | Enable a runner to start on boot |
|
||||
| `grm disable <name>` | Disable and deregister a runner |
|
||||
| `grm status <name>` | Check the status of a registered runner |
|
||||
| `grm remove <name>` | Remove a runner completely (with 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 --no-status` | List registered runners without SSH status checks |
|
||||
| `grm trigger-workflow <workflow_id>` | Trigger a Gitea Actions workflow via the API |
|
||||
| `grm trigger-workflow --list` | List available workflows in the repository |
|
||||
| `grm --version` | Show the installed version |
|
||||
|
||||
All lifecycle commands (`start`, `stop`, `restart`, `enable`, `disable`, `status`, `remove`) work by runner name and pull connection details from the local registry. You can override any stored value with `--host`, `--user`, or `--key`.
|
||||
|
||||
See the [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/CLI-Commands.-) wiki page for full argument and option reference.
|
||||
|
||||
## Configuration
|
||||
|
||||
GRM reads configuration from a `.env` file in the current directory (loaded automatically via python-dotenv). You can also set environment variables directly.
|
||||
|
||||
### Required variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `GITEA_URL` | Your Gitea instance URL (e.g., `https://git.example.com`) |
|
||||
| `GITEA_REGISTRATION_TOKEN` | Runner registration token from Gitea (starts with `GR`) |
|
||||
|
||||
### Optional variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CI_GITEA_TOKEN` | — | Gitea admin API token for optional post-install API verification |
|
||||
| `GITEA_INTEGRATION_RETRIES` | `3` | Number of API check retries during integration test |
|
||||
| `GITEA_RUNNER_USER` | current login | Default SSH user (overrides `--user`) |
|
||||
| `GITEA_RUNNER_KEY` | — | Default SSH key path (overrides `--key`) |
|
||||
| `GITEA_RUNNER_LABELS` | — | Default runner labels (overrides `--labels`) |
|
||||
| `GRM_LANG` | `en` | UI language: `en`, `bg`, `de`, `ru`, `zh`, `pl` |
|
||||
| `GRM_LOG_LEVEL` | `INFO` | Console log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||
| `GRM_BECOME_PASSWORD_FILE` | — | Path to file containing sudo password (see [Sudo Password Handling](#sudo-password-handling)) |
|
||||
| `ANSIBLE_BECOME_PASSWORD_FILE` | — | Fallback sudo password file path (Ansible-native env var) |
|
||||
|
||||
### 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:
|
||||
|
||||
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.
|
||||
3. **`ANSIBLE_BECOME_PASSWORD_FILE`** (env var) — Fallback, Ansible-native env var.
|
||||
4. **Interactive prompt** — If none of the above are set, GRM prompts for the sudo password (hidden input).
|
||||
5. **Piped stdin** — When stdin is not a TTY, reads the first line: `echo 'password' | grm list`.
|
||||
6. **`--no-ask-become-pass`** — Skip sudo password entirely (use when the target user has passwordless sudo).
|
||||
|
||||
For `grm list` specifically, the password is collected once and reused for all runner status checks via `--become-password-file`, avoiding stdin consumption issues when checking multiple runners.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
./grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
# Interactive prompt (default)
|
||||
grm install 192.168.1.10 --user ubuntu
|
||||
|
||||
# Password file (recommended for automation)
|
||||
echo 'my-sudo-pass' > ~/.grm-sudo-pass
|
||||
chmod 600 ~/.grm-sudo-pass
|
||||
grm --become-password-file ~/.grm-sudo-pass install 192.168.1.10 --user ubuntu
|
||||
|
||||
# Env var (set in .env)
|
||||
GRM_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||
grm list # uses the file automatically
|
||||
|
||||
# Piped stdin (for scripts)
|
||||
echo 'my-sudo-pass' | grm list
|
||||
|
||||
# Passwordless sudo on target
|
||||
grm install 192.168.1.10 --user ubuntu --no-ask-become-pass
|
||||
```
|
||||
|
||||
Using Make:
|
||||
### Verbose Output
|
||||
|
||||
Pass `-v` / `--verbose` (global flag, before the subcommand) to enable Ansible verbose mode (`-v`):
|
||||
|
||||
```bash
|
||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
||||
grm --verbose install 192.168.1.10 --user ubuntu
|
||||
grm -v status prod-runner
|
||||
```
|
||||
|
||||
### Verify Runner
|
||||
### Runner Labels
|
||||
|
||||
Check Gitea admin UI under **Actions → Runners**. The runner should appear as **Online**.
|
||||
Runner labels control which jobs a runner accepts. They are set at installation time:
|
||||
|
||||
### View Logs
|
||||
- **`--labels "docker:docker://alpine:latest"`** — Set specific labels.
|
||||
- **`--labels ""`** — Explicitly set **no labels** (overrides `GITEA_RUNNER_LABELS` env var).
|
||||
- **No `--labels` flag** — Uses `GITEA_RUNNER_LABELS` env var if set, otherwise the Ansible role default.
|
||||
|
||||
```bash
|
||||
sudo journalctl -u act-runner-<name> -f
|
||||
# Custom labels
|
||||
grm install 192.168.1.10 --user ubuntu --labels "docker:docker://alpine:latest,ubuntu-22.04:docker://ubuntu:22.04"
|
||||
|
||||
# Explicitly no labels (overrides GITEA_RUNNER_LABELS env var)
|
||||
grm install 192.168.1.10 --user ubuntu --labels ""
|
||||
|
||||
# Use GITEA_RUNNER_LABELS from .env (or role default if unset)
|
||||
grm install 192.168.1.10 --user ubuntu
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
### Getting tokens
|
||||
|
||||
**Registration token** (required): Navigate to your Gitea instance:
|
||||
|
||||
- **Instance-level**: Site Administration → Actions → Runners → Create Registration Token
|
||||
- **Organization-level**: Organization → Settings → Actions → Runners → Create Registration Token
|
||||
- **Repository-level**: Repository → Settings → Actions → Runners → Create Registration Token
|
||||
|
||||
Use instance-level tokens for shared runners, and repo-level tokens for dedicated runners.
|
||||
|
||||
**Admin API token** (optional): Settings → Applications → Generate New Token, with the `admin` scope (or at minimum: `read:user`, `read:repository`, `read:admin`). When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational and does not affect pass/fail.
|
||||
|
||||
## Multi-Instance Support
|
||||
|
||||
One of GRM's core features is the ability to run multiple isolated runners on the same host. Each runner instance gets:
|
||||
|
||||
- **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`
|
||||
- **Data directory**: `/var/lib/gitea-runner/<name>/`
|
||||
- **Config directory**: `/etc/gitea-runner/<name>/`
|
||||
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
||||
- **Docker prune timer**: Per-instance daily cleanup
|
||||
|
||||
```bash
|
||||
# Install two runners on the same host
|
||||
grm install 192.168.1.10 --user ubuntu --name workflow-runner
|
||||
grm install 192.168.1.10 --user ubuntu --name build-runner
|
||||
|
||||
# Manage them independently by name
|
||||
grm stop workflow-runner
|
||||
grm status build-runner
|
||||
grm list
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
GRM is designed with security as a first-class concern:
|
||||
|
||||
- **Rootless Docker**: Each runner operates under a dedicated unprivileged system user. The Docker daemon runs in rootless mode, so containers never have root access to the host. User namespaces (`subuid`/`subgid`) are configured automatically.
|
||||
- **Dedicated users**: Each runner gets its own system user (`grm-<name>`) with lingering enabled, so the user's systemd services run without an active login session.
|
||||
- **Secret handling**: Registration tokens and admin tokens are never passed on the command line. They are written to temporary JSON files with `0600` permissions and passed to Ansible via `--extra-vars @tempfile`. The temp file is deleted immediately after execution. This prevents secrets from being visible in the process list (`ps aux`), addressing CWE-214.
|
||||
- **No shell injection**: The CLI never uses `shell=True` with subprocess. All Ansible commands are constructed as argument lists.
|
||||
- **Bandit security scan**: The CI pipeline runs Bandit on every PR to catch common Python security issues.
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
GRM supports and tests the following operating systems:
|
||||
|
||||
| OS | Versions | Package manager |
|
||||
|----|----------|-----------------|
|
||||
| Arch Linux | rolling | pacman |
|
||||
| Ubuntu | 22.04, 24.04 | apt |
|
||||
| Debian | 12 | apt |
|
||||
|
||||
All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files. The platform matrix is defined in `devx.molecule.platforms` as the single source of truth.
|
||||
|
||||
## Development Setup
|
||||
|
||||
GRM uses a comprehensive development setup with 100% test coverage enforcement, multiple linters, and Molecule integration tests.
|
||||
|
||||
### Quick development setup
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Latest stable release
|
||||
make setup
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### Make targets
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `setup` | Full environment setup |
|
||||
| `install` | Installs a runner on a host |
|
||||
| `lint` | Runs Python linters |
|
||||
| `ansible-lint` | Runs `ansible-lint` |
|
||||
| `test-unit` | Runs unit tests with coverage |
|
||||
| `molecule` | Runs Ansible Molecule tests |
|
||||
| `test-all` | Runs all tests |
|
||||
| `make setup` | Full setup: venv, deps, hooks, CI tools |
|
||||
| `make lint-all` | ruff + pyright + bandit + ansible-lint + checkmake + actionlint |
|
||||
| `make pytest-cov` | Unit tests with 100% coverage enforcement |
|
||||
| `make test-unit` | Unit tests without coverage |
|
||||
| `make molecule` | All 6 Molecule scenarios on Ubuntu 22.04 |
|
||||
| `make molecule-all` | All 6 scenarios on all 4 supported OSes |
|
||||
| `make test-all` | pytest-cov + molecule |
|
||||
| `make workflow-lint` | Static lint of workflow YAML (actionlint) |
|
||||
| `make workflow-dryrun` | Dry-run all workflows in Docker |
|
||||
| `make workflow-check` | workflow-lint + workflow-dryrun |
|
||||
|
||||
See the [Development Setup](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Development-Setup.-) wiki page for full details.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
GRM consists of two layers:
|
||||
|
||||
1. **Python CLI** (`src/gitea_runner_manager/`) — 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.
|
||||
|
||||
```
|
||||
grm install <host>
|
||||
└── RunnerManager.install()
|
||||
└── ansible-playbook ansible/install-runner.yml
|
||||
└── role: gitea-runner
|
||||
├── user_setup.yml (create per-runner system user + lingering)
|
||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||
├── install_runner.yml (download binary, config, register, service)
|
||||
├── prune.yml (Docker prune timer)
|
||||
└── integration_test.yml (validate service is active)
|
||||
```
|
||||
|
||||
### Python modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `cli.py` | Click-based CLI entry point — defines all commands |
|
||||
| `runner_manager.py` | Ansible orchestration + registry integration |
|
||||
| `executor.py` | Ansible subprocess execution with log capture |
|
||||
| `registry.py` | Local JSON runner registry at `~/.local/share/grm/runners.json` |
|
||||
| `i18n.py` | Internationalisation (en, bg, de, ru, zh, pl) |
|
||||
| `exceptions.py` | Custom exceptions (`GRMError`, `AnsibleError`) |
|
||||
| `logging_config.py` | Logging to `~/.local/state/grm/logs/grm.log` |
|
||||
| `report.py` | Operation report tracking with step status |
|
||||
| `ui.py` | Colorised console output via Click |
|
||||
|
||||
See the [Architecture](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Architecture) wiki page for the full component diagram and data flow.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation lives on the [**GRM Wiki**](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki).
|
||||
|
||||
### User Documentation
|
||||
|
||||
- [Getting Started](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Getting-Started.-) — Installation, quick start, token setup, first run
|
||||
- [Installation](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Installation) — Prerequisites, setup, multiple instances
|
||||
- [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/CLI-Commands.-) — All commands with arguments and options
|
||||
- [Troubleshooting](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Troubleshooting) — Common issues and solutions
|
||||
- [FAQ](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/FAQ) — Frequently asked questions
|
||||
|
||||
### Technical Documentation
|
||||
|
||||
- [Architecture](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Architecture) — High-level design, component interactions, data flow
|
||||
- [Development Setup](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Development-Setup.-) — Environment setup, dependencies, local testing
|
||||
- [CI/CD Workflow](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/CI-CD-Workflow.-) — How CI works, release process, branch protection
|
||||
- [Testing Strategy](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Testing-Strategy.-) — Unit, integration, and Molecule tests
|
||||
- [Decision Log](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Decision-Log.-) — Key technical decisions and rationale
|
||||
- [Contributing Guide](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Contributing-Guide.-) — Coding standards, PR workflow, commit rules
|
||||
|
||||
## Links
|
||||
|
||||
- [Repository](https://git.oblachno.oblachno.fyi/oblachno-oss/grm)
|
||||
- [Releases](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
- [Issues](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/issues)
|
||||
- [CI/CD Pipeline](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
- [Changelog](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/CHANGELOG.md)
|
||||
- [Wiki](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0
|
||||
GPL-3.0 — See [LICENSE](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE) for the full text.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Troubleshooting
|
||||
|
||||
| Symptom | Likely Cause | Solution |
|
||||
|---------|-------------|----------|
|
||||
| Pre-commit rejects commit message | Missing conventional format or GRM-N prefix present | Use `feat: description` format without `GRM-N:` |
|
||||
| `make molecule` fails with `runner_name is undefined` | Verify playbook missing variable | Fixed in Phase 1.1; ensure you're on latest master |
|
||||
| CI molecule job fails | Docker not available on runner host | Ensure Gitea runner host has Docker installed and running |
|
||||
| Auto-merge doesn't trigger | Label not exactly `ready-to-merge` or CI checks not all green | Verify label spelling; check CI status |
|
||||
| Vikunja task not updated after merge | VIKUNJA_TOKEN expired or task ID missing from commit | Regenerate token; verify merge commit has `GRM-N:` prefix |
|
||||
| Post-merge can't find Vikunja task | Task not in project 6 or identifier mismatch | Verify task exists in Vikunja project 6 with correct identifier |
|
||||
| `make pytest-cov` fails | Coverage below 100% | Add tests for new code paths |
|
||||
| `devx.tools.configure_repo` fails | CI_GITEA_TOKEN missing or invalid | Set token with repo admin scope and re-run |
|
||||
| `configure_repo` sets wrong status checks | Stale `BRANCH_PROTECTION_CONFIG` | Updated to include `(pull_request)` suffix; re-run `configure_repo` |
|
||||
| Token visible in `ps aux` during install | Old version passed tokens via command line | Fixed: tokens now passed via temp file with `0600` permissions |
|
||||
| `remove-runner.yml` leaves lingering enabled | Old version didn't disable lingering | Fixed: now runs `loginctl disable-linger` and removes subuid/subgid |
|
||||
| apt cache update always reports `changed` | `cache_valid_time: 0` forced update every run | Fixed: changed to `cache_valid_time: 3600` |
|
||||
| Prune/service templates created even when `docker_rootless_setup: false` | Template tasks not guarded | Fixed: template creation now guarded by `docker_rootless_setup` |
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env fish
|
||||
# Activate the Python virtual environment for fish
|
||||
# Usage: source activate.fish
|
||||
|
||||
set -l script_dir (dirname (status --current-filename))
|
||||
source "$script_dir/.venv/bin/activate.fish"
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Activate the Python virtual environment for bash/zsh
|
||||
# Usage: source activate.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" && pwd)"
|
||||
source "$SCRIPT_DIR/.venv/bin/activate"
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env zsh
|
||||
# Activate the Python virtual environment for zsh
|
||||
# Usage: source activate.zsh
|
||||
|
||||
0="${ZERO:-${0:#$ZSH_ARGZERO}}"
|
||||
0="${${(M)0:#/*}:-$PWD/$0}"
|
||||
source "${0:A:h}/.venv/bin/activate"
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
- name: Disable Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Stop gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Include deregistration
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: deregister.yml
|
||||
when: not skip_runner_registration | default(false)
|
||||
|
||||
- name: Disable gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Enable Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Enable gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user enable gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Start gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user start gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
@@ -2,9 +2,5 @@
|
||||
- name: Install Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
gitea_url: "{{ gitea_url | mandatory }}"
|
||||
registration_token: "{{ registration_token | mandatory }}"
|
||||
runner_name: "{{ runner_name | default(inventory_hostname) }}"
|
||||
roles:
|
||||
- role: gitea-runner
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
# Gitea Runner Manager inventory example
|
||||
# Each line represents a target host for runner installation.
|
||||
#
|
||||
# Required variables per host:
|
||||
# ansible_user — SSH login user
|
||||
# ansible_ssh_private_key_file — Path to SSH private key
|
||||
#
|
||||
# Optional variables per host:
|
||||
# gitea_runner_version=1.0.8 — Runner binary version
|
||||
|
||||
[runners]
|
||||
192.168.1.10 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||
runner.example.com ansible_user=arch
|
||||
runner.example.com ansible_user=arch ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
- name: Remove Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Get runner user UID
|
||||
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
register: runner_uid_result
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Set runner UID fact
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_uid: "{{ runner_uid_result.stdout }}"
|
||||
when: runner_uid_result.rc == 0
|
||||
|
||||
- name: Stop gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Disable gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Force-remove all Docker containers (rootless)
|
||||
ansible.builtin.shell: |
|
||||
set -o pipefail
|
||||
docker ps -aq 2>/dev/null | xargs -r docker rm -f 2>/dev/null || true
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Prune all Docker images, volumes, and build cache (rootless)
|
||||
ansible.builtin.command: docker system prune -af --volumes
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Stop rootless Docker daemon
|
||||
ansible.builtin.command: systemctl --user stop docker
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Include deregistration
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: deregister.yml
|
||||
when: not skip_runner_registration | default(false)
|
||||
|
||||
- name: Remove docker-prune user service file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.service"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove docker-prune user timer file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.timer"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove systemd user unit file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service"
|
||||
state: absent
|
||||
when: remove_systemd_template | default(true)
|
||||
|
||||
- name: Kill remaining processes of runner user
|
||||
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for processes to terminate
|
||||
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Disable lingering for runner user
|
||||
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: Remove runner user and home directory
|
||||
ansible.builtin.user:
|
||||
name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
state: absent
|
||||
remove: true
|
||||
when: remove_runner_user | default(true)
|
||||
failed_when: false
|
||||
|
||||
- name: Remove Docker data root when user is kept
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.local/share/docker"
|
||||
state: absent
|
||||
when: not (remove_runner_user | default(true))
|
||||
failed_when: false
|
||||
|
||||
- name: Remove act cache when user is kept
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.cache/act"
|
||||
state: absent
|
||||
when: not (remove_runner_user | default(true))
|
||||
failed_when: false
|
||||
|
||||
- name: Remove systemd user config dir when user is kept
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user"
|
||||
state: absent
|
||||
when: not (remove_runner_user | default(true))
|
||||
failed_when: false
|
||||
|
||||
- name: Remove subuid entry for runner user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subuid
|
||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove subgid entry for runner user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subgid
|
||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove runner data directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}"
|
||||
state: absent
|
||||
|
||||
- name: Remove runner config directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}"
|
||||
state: absent
|
||||
@@ -1,5 +1,7 @@
|
||||
collections:
|
||||
- name: community.general
|
||||
version: ">=13.0.1"
|
||||
version: "==13.1.0"
|
||||
- name: ansible.posix
|
||||
version: ">=1.5.4"
|
||||
version: "==2.2.0"
|
||||
- name: community.docker
|
||||
version: "==5.2.1"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
- name: Restart Gitea Actions runner (stop, prune images, start)
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
prune_images: true
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Resolve runner UID
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: resolve_uid.yml
|
||||
|
||||
- name: Stop gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Prune stale runner images from rootless Docker
|
||||
ansible.builtin.command:
|
||||
cmd: python3 {{ playbook_dir }}/../scripts/prune_runner_images.py
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- prune_images | default(true)
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Start gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user start gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
gitea_runner_version: "1.0.8"
|
||||
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-22.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"
|
||||
|
||||
# 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,9 +1,12 @@
|
||||
---
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart act-runner
|
||||
ansible.builtin.systemd:
|
||||
name: "act-runner-{{ runner_name }}"
|
||||
state: restarted
|
||||
- name: Restart gitea-runner
|
||||
ansible.builtin.command: systemctl --user restart gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- ansible_facts is defined
|
||||
- ansible_facts['service_mgr'] | default('') == 'systemd'
|
||||
- docker_rootless_setup
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
- name: Prepare
|
||||
hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 0
|
||||
when: ansible_facts['os_family'] == 'Debian'
|
||||
|
||||
- name: Install prerequisites for rootless Docker role (Debian/Ubuntu)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- curl
|
||||
- gpg
|
||||
- python3-debian
|
||||
- ca-certificates
|
||||
state: present
|
||||
when: ansible_facts['os_family'] == 'Debian'
|
||||
|
||||
- name: Update pacman cache
|
||||
community.general.pacman:
|
||||
update_cache: true
|
||||
when: ansible_facts['os_family'] == 'Archlinux'
|
||||
|
||||
- name: Install prerequisites for rootless Docker role (Arch Linux)
|
||||
community.general.pacman:
|
||||
name:
|
||||
- curl
|
||||
- gnupg
|
||||
- ca-certificates
|
||||
state: present
|
||||
when: ansible_facts['os_family'] == 'Archlinux'
|
||||
@@ -6,5 +6,7 @@
|
||||
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
|
||||
|
||||
@@ -3,19 +3,37 @@ driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: instance
|
||||
image: geerlingguy/docker-ubuntu2204-ansible:latest
|
||||
command: ""
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
- name: Prepare
|
||||
hosts: all
|
||||
become: true
|
||||
tasks:
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
when: ansible_facts['os_family'] == 'Debian'
|
||||
@@ -2,47 +2,61 @@
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "molecule-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check act_runner binary exists
|
||||
ansible.builtin.stat:
|
||||
path: /usr/local/bin/act_runner
|
||||
register: act_runner_stat
|
||||
- name: Check runner user exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ gitea_runner_service_user }}"
|
||||
register: user_info
|
||||
check_mode: true
|
||||
|
||||
- name: Assert act_runner binary exists
|
||||
- name: Assert runner user exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- act_runner_stat.stat.exists
|
||||
fail_msg: "act_runner binary is missing"
|
||||
- user_info.state == "present"
|
||||
fail_msg: "Runner system user was not created"
|
||||
|
||||
- name: Check Docker is installed
|
||||
ansible.builtin.command: docker --version
|
||||
changed_when: false
|
||||
|
||||
- name: Check systemd service file exists
|
||||
- name: Check runner binary exists
|
||||
ansible.builtin.stat:
|
||||
path: "/etc/systemd/system/act-runner-molecule-test-runner.service"
|
||||
path: "{{ gitea_runner_binary_path }}"
|
||||
register: binary_stat
|
||||
|
||||
- name: Assert runner binary exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- binary_stat.stat.exists
|
||||
fail_msg: "Gitea runner binary is missing"
|
||||
|
||||
- name: Check systemd user service exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert service file exists
|
||||
- name: Assert user service exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- service_stat.stat.exists
|
||||
fail_msg: "Systemd service file is missing"
|
||||
fail_msg: "Systemd user service is missing"
|
||||
|
||||
- name: Check prune timer exists
|
||||
- name: Check instance data directory exists
|
||||
ansible.builtin.stat:
|
||||
path: /etc/systemd/system/docker-prune.timer
|
||||
register: timer_stat
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
register: data_dir_stat
|
||||
|
||||
- name: Assert prune timer exists
|
||||
- name: Assert instance data directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- timer_stat.stat.exists
|
||||
fail_msg: "Docker prune timer is missing"
|
||||
- data_dir_stat.stat.exists
|
||||
fail_msg: "Instance data directory is missing"
|
||||
|
||||
- name: Check config file exists
|
||||
- name: Check config file exists in config directory
|
||||
ansible.builtin.stat:
|
||||
path: /etc/act-runner/config.toml
|
||||
path: "{{ gitea_runner_config_dir }}/config.yaml"
|
||||
register: config_stat
|
||||
|
||||
- name: Assert config file exists
|
||||
@@ -50,3 +64,14 @@
|
||||
that:
|
||||
- config_stat.stat.exists
|
||||
fail_msg: "Config file is missing"
|
||||
|
||||
- name: Check prune timer exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||
register: timer_stat
|
||||
|
||||
- name: Assert prune timer exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- timer_stat.stat.exists
|
||||
fail_msg: "Docker prune timer is missing"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
side_effect: side_effect.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: Create fake runner registration file
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "deregister-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Ensure fake .runner file exists
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ gitea_runner_data_dir }}/.runner"
|
||||
content: |
|
||||
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"}
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Deregister runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "deregister-test-runner"
|
||||
registration_token: "fake-token-for-testing"
|
||||
gitea_url: "http://localhost:3000"
|
||||
skip_runner_registration: false
|
||||
tasks:
|
||||
- name: Include deregistration tasks
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: deregister.yml
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "deregister-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check registration file was removed
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||
register: runner_file_stat
|
||||
|
||||
- name: Assert registration file no longer exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not runner_file_stat.stat.exists
|
||||
fail_msg: "Registration file (.runner) was not removed by deregistration"
|
||||
|
||||
- name: Check systemd user service still exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert user service still exists after deregister
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- service_stat.stat.exists
|
||||
fail_msg: "Systemd user service was incorrectly removed"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
side_effect: side_effect.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
- name: Stop runner instance
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "lifecycle-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Stop gitea-runner user service
|
||||
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user stop gitea-runner"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Disable gitea-runner user service
|
||||
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user disable gitea-runner"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Re-enable and start runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "lifecycle-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Enable gitea-runner user service
|
||||
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user enable gitea-runner"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Start gitea-runner user service
|
||||
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user start gitea-runner"
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "lifecycle-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check systemd user service exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert user service exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- service_stat.stat.exists
|
||||
fail_msg: "Systemd user service is missing"
|
||||
|
||||
- name: Check instance data directory exists after lifecycle
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
register: data_dir_stat
|
||||
|
||||
- name: Assert instance data directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- data_dir_stat.stat.exists
|
||||
fail_msg: "Instance data directory is missing after lifecycle"
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Converge first runner instance
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
gitea_url: "http://localhost:3000"
|
||||
registration_token: "fake-token-for-testing"
|
||||
runner_name: "molecule-runner-a"
|
||||
skip_runner_registration: true
|
||||
docker_rootless_setup: false
|
||||
roles:
|
||||
- role: gitea-runner
|
||||
|
||||
- name: Converge second runner instance
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
gitea_url: "http://localhost:3000"
|
||||
registration_token: "fake-token-for-testing"
|
||||
runner_name: "molecule-runner-b"
|
||||
skip_runner_registration: true
|
||||
docker_rootless_setup: false
|
||||
roles:
|
||||
- role: gitea-runner
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check first runner user exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_home }}/grm-molecule-runner-a"
|
||||
register: home_a_stat
|
||||
|
||||
- name: Assert first runner user home exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- home_a_stat.stat.exists
|
||||
fail_msg: "First runner user home is missing"
|
||||
|
||||
- name: Check second runner user exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_home }}/grm-molecule-runner-b"
|
||||
register: home_b_stat
|
||||
|
||||
- name: Assert second runner user home exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- home_b_stat.stat.exists
|
||||
fail_msg: "Second runner user home is missing"
|
||||
|
||||
- name: Check first instance data directory exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_data_dir }}/molecule-runner-a"
|
||||
register: data_a_stat
|
||||
|
||||
- name: Assert first instance data directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- data_a_stat.stat.exists
|
||||
fail_msg: "First instance data directory is missing"
|
||||
|
||||
- name: Check second instance data directory exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_data_dir }}/molecule-runner-b"
|
||||
register: data_b_stat
|
||||
|
||||
- name: Assert second instance data directory exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- data_b_stat.stat.exists
|
||||
fail_msg: "Second instance data directory is missing"
|
||||
|
||||
- name: Check first instance config exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_config_dir }}/molecule-runner-a/config.yaml"
|
||||
register: config_a_stat
|
||||
|
||||
- name: Assert first instance config exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- config_a_stat.stat.exists
|
||||
fail_msg: "First instance config file is missing"
|
||||
|
||||
- name: Check second instance config exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_base_config_dir }}/molecule-runner-b/config.yaml"
|
||||
register: config_b_stat
|
||||
|
||||
- name: Assert second instance config exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- config_b_stat.stat.exists
|
||||
fail_msg: "Second instance config file is missing"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
side_effect: side_effect.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Remove runner via remove-runner playbook
|
||||
ansible.builtin.import_playbook: "../../../../remove-runner.yml"
|
||||
vars:
|
||||
runner_name: "remove-test-runner"
|
||||
registration_token: "fake-token-for-testing"
|
||||
gitea_url: "http://localhost:3000"
|
||||
skip_runner_registration: true
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
- name: Verify runner was fully removed
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "remove-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check runner user is absent
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: "{{ gitea_runner_service_user }}"
|
||||
register: user_check
|
||||
failed_when: false
|
||||
|
||||
- name: Assert runner user is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- user_check is failed or
|
||||
gitea_runner_service_user not in (user_check.ansible_facts.getent_passwd | default({}))
|
||||
fail_msg: "Runner user still exists after removal"
|
||||
|
||||
- name: Check runner home directory is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}"
|
||||
register: home_stat
|
||||
|
||||
- name: Assert runner home directory is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not home_stat.stat.exists
|
||||
fail_msg: "Runner home directory still exists after removal"
|
||||
|
||||
- name: Check runner data directory is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
register: data_stat
|
||||
|
||||
- name: Assert runner data directory is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not data_stat.stat.exists
|
||||
fail_msg: "Runner data directory still exists after removal"
|
||||
|
||||
- name: Check runner config directory is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_config_dir }}"
|
||||
register: config_stat
|
||||
|
||||
- name: Assert runner config directory is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not config_stat.stat.exists
|
||||
fail_msg: "Runner config directory still exists after removal"
|
||||
|
||||
- name: Check gitea-runner service unit is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert gitea-runner service unit is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not service_stat.stat.exists
|
||||
fail_msg: "gitea-runner service unit still exists after removal"
|
||||
|
||||
- name: Check docker-prune service unit is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
||||
register: prune_service_stat
|
||||
|
||||
- name: Assert docker-prune service unit is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not prune_service_stat.stat.exists
|
||||
fail_msg: "docker-prune service unit still exists after removal"
|
||||
|
||||
- name: Check docker-prune timer unit is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||
register: prune_timer_stat
|
||||
|
||||
- name: Assert docker-prune timer unit is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not prune_timer_stat.stat.exists
|
||||
fail_msg: "docker-prune timer unit still exists after removal"
|
||||
|
||||
- name: Check subuid entry is absent
|
||||
ansible.builtin.command: "grep -c '^{{ gitea_runner_service_user }}:' /etc/subuid"
|
||||
register: subuid_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Assert subuid entry is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- subuid_check.rc != 0
|
||||
fail_msg: "subuid entry still exists after removal"
|
||||
|
||||
- name: Check subgid entry is absent
|
||||
ansible.builtin.command: "grep -c '^{{ gitea_runner_service_user }}:' /etc/subgid"
|
||||
register: subgid_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Assert subgid entry is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- subgid_check.rc != 0
|
||||
fail_msg: "subgid entry still exists after removal"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "template-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check systemd user service exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert user service exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- service_stat.stat.exists
|
||||
fail_msg: "Systemd user service is missing"
|
||||
|
||||
- name: Read rendered user service template
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_template
|
||||
|
||||
- name: Assert user service template contains expected directives
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'Type=simple' in service_template.content | b64decode"
|
||||
- "'ExecStart={{ gitea_runner_binary_path }}' in service_template.content | b64decode"
|
||||
- "'Restart=on-failure' in service_template.content | b64decode"
|
||||
- "'DOCKER_HOST=unix:///run/user' in service_template.content | b64decode"
|
||||
- "'XDG_RUNTIME_DIR=/run/user' in service_template.content | b64decode"
|
||||
fail_msg: "User service template is missing expected directives"
|
||||
|
||||
- name: Read rendered prune service template
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
||||
register: prune_service
|
||||
|
||||
- name: Assert prune service contains expected directives
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'Type=oneshot' in prune_service.content | b64decode"
|
||||
- "'docker system prune' in prune_service.content | b64decode"
|
||||
- "'docker volume prune' in prune_service.content | b64decode"
|
||||
fail_msg: "Prune service template is missing expected directives"
|
||||
|
||||
- name: Read rendered prune timer template
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||
register: prune_timer
|
||||
|
||||
- name: Assert prune timer contains expected directives
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'OnCalendar={{ gitea_runner_prune_schedule }}' in prune_timer.content | b64decode"
|
||||
- "'Persistent=true' in prune_timer.content | b64decode"
|
||||
fail_msg: "Prune timer template is missing expected directives"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroupns_mode: host
|
||||
privileged: true
|
||||
pre_build_image: false
|
||||
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
converge: converge.yml
|
||||
prepare: ../common/prepare.yml
|
||||
side_effect: side_effect.yml
|
||||
env:
|
||||
ANSIBLE_ROLES_PATH: "../../.."
|
||||
|
||||
scenario:
|
||||
test_sequence:
|
||||
- dependency
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- idempotence
|
||||
- side_effect
|
||||
- verify
|
||||
- cleanup
|
||||
- destroy
|
||||
|
||||
verifier:
|
||||
name: ansible
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Update runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "update-test-runner"
|
||||
tasks:
|
||||
- name: Include update tasks
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: update_runner.yml
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
- name: Verify
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
runner_name: "update-test-runner"
|
||||
pre_tasks:
|
||||
- name: Load role defaults
|
||||
ansible.builtin.include_vars:
|
||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||
tasks:
|
||||
- name: Check runner binary still exists after update
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_binary_path }}"
|
||||
register: binary_stat
|
||||
|
||||
- name: Assert binary executable exists after update
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- binary_stat.stat.exists
|
||||
fail_msg: "Runner binary missing after update"
|
||||
|
||||
- name: Check systemd user service still exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
register: service_stat
|
||||
|
||||
- name: Assert user service exists after update
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- service_stat.stat.exists
|
||||
fail_msg: "Systemd user service missing after update"
|
||||
|
||||
- name: Check instance data directory still exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
register: data_stat
|
||||
|
||||
- name: Assert data directory exists after update
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- data_stat.stat.exists
|
||||
fail_msg: "Runner data directory missing after update"
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
- name: Ensure config directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/act-runner
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Create act_runner config file
|
||||
ansible.builtin.template:
|
||||
src: act-runner-config.toml.j2
|
||||
dest: /etc/act-runner/config.toml
|
||||
mode: "0644"
|
||||
notify: Restart act-runner
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
- 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 with Gitea via CLI
|
||||
ansible.builtin.command: >
|
||||
{{ gitea_runner_binary_path }} delete
|
||||
--token {{ registration_token }}
|
||||
--name {{ runner_name }}
|
||||
--instance {{ gitea_url }}
|
||||
--no-interactive
|
||||
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
|
||||
register: deregister_output
|
||||
changed_when: deregister_output.rc == 0
|
||||
failed_when: false
|
||||
|
||||
- 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,63 +0,0 @@
|
||||
---
|
||||
- name: Install Docker (Debian/Ubuntu)
|
||||
when: ansible_facts['os_family'] == 'Debian'
|
||||
block:
|
||||
- name: Install prerequisite packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- apt-transport-https
|
||||
- ca-certificates
|
||||
- curl
|
||||
- gnupg
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Add Docker GPG key
|
||||
ansible.builtin.apt_key:
|
||||
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
|
||||
keyring: /etc/apt/keyrings/docker.gpg
|
||||
when: ansible_distribution != 'Ubuntu' or ansible_distribution_major_version | int >= 22
|
||||
|
||||
- name: Add Docker repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch={{ ansible_architecture }}
|
||||
signed-by=/etc/apt/keyrings/docker.gpg]
|
||||
https://download.docker.com/linux/{{ ansible_distribution | lower }}
|
||||
{{ ansible_distribution_release }} stable
|
||||
filename: docker
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Install Docker packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
|
||||
- name: Install Docker (Arch Linux)
|
||||
when: ansible_facts['os_family'] == 'Archlinux'
|
||||
block:
|
||||
- name: Install Docker packages
|
||||
community.general.pacman:
|
||||
name:
|
||||
- docker
|
||||
- docker-compose
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Ensure Docker service is running
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: started
|
||||
enabled: true
|
||||
|
||||
- name: Add user to docker group
|
||||
ansible.builtin.user:
|
||||
name: "{{ ansible_user | default(ansible_user_id) }}"
|
||||
groups: docker
|
||||
append: true
|
||||
when: ansible_user is defined or ansible_user_id is defined
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
- name: Get latest act_runner release info
|
||||
ansible.builtin.uri:
|
||||
url: https://gitea.com/gitea/act_runner/releases/latest
|
||||
return_content: true
|
||||
headers:
|
||||
Accept: application/json
|
||||
register: act_runner_release
|
||||
when: act_runner_version | default('latest') == 'latest'
|
||||
changed_when: false
|
||||
|
||||
- name: Set act_runner version from latest release
|
||||
ansible.builtin.set_fact:
|
||||
act_runner_version: "{{ act_runner_release.json.tag_name }}"
|
||||
when: act_runner_version | default('latest') == 'latest'
|
||||
|
||||
- name: Set act_runner download URL
|
||||
ansible.builtin.set_fact:
|
||||
act_runner_url: >-
|
||||
https://gitea.com/gitea/act_runner/releases/download/{{ act_runner_version }}/
|
||||
act_runner-{{ act_runner_version }}-linux-{{ ansible_architecture | regex_replace('x86_64', 'amd64') }}
|
||||
|
||||
- name: Ensure /usr/local/bin directory exists
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/bin
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Download act_runner binary
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ act_runner_url }}"
|
||||
dest: /usr/local/bin/act_runner
|
||||
mode: "0755"
|
||||
force: true
|
||||
notify: Restart act-runner
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
- name: Get latest gitea_runner release info
|
||||
ansible.builtin.uri:
|
||||
url: https://gitea.com/api/v1/repos/gitea/runner/releases/latest
|
||||
return_content: true
|
||||
body_format: json
|
||||
headers:
|
||||
Accept: application/json
|
||||
register: gitea_runner_release
|
||||
when: gitea_runner_version | default('latest') == 'latest'
|
||||
changed_when: false
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: gitea_runner_release is not failed
|
||||
|
||||
- name: Set gitea_runner version from latest release
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_version: "{{ gitea_runner_release.json.tag_name }}"
|
||||
when: gitea_runner_version | default('latest') == 'latest'
|
||||
|
||||
- name: Set gitea_runner download version (strip v prefix)
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_download_version: "{{ gitea_runner_version | regex_replace('^v', '') }}"
|
||||
|
||||
- name: Set gitea_runner download URL
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_url: >-
|
||||
{{ 'https://gitea.com/gitea/runner/releases/download/v' ~ gitea_runner_download_version
|
||||
~ '/gitea-runner-' ~ gitea_runner_download_version ~ '-linux-'
|
||||
~ (ansible_facts['architecture'] | regex_replace('x86_64', 'amd64')) }}
|
||||
|
||||
- name: Ensure /usr/local/bin directory exists
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/bin
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Download gitea_runner binary
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ gitea_runner_url }}"
|
||||
dest: "{{ gitea_runner_binary_path }}"
|
||||
mode: "0755"
|
||||
force: false
|
||||
register: gitea_runner_download
|
||||
notify: Restart gitea-runner
|
||||
retries: 3
|
||||
delay: 5
|
||||
until: gitea_runner_download is not failed
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
- name: Include gitea_runner download
|
||||
ansible.builtin.include_tasks: download_gitea_runner.yml
|
||||
|
||||
- name: Create gitea_runner config file
|
||||
ansible.builtin.template:
|
||||
src: gitea-runner-config.yaml.j2
|
||||
dest: "{{ gitea_runner_config_dir }}/config.yaml"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Include validation
|
||||
ansible.builtin.include_tasks: validate.yml
|
||||
|
||||
- name: Include registration
|
||||
ansible.builtin.include_tasks: register.yml
|
||||
when: not skip_runner_registration
|
||||
|
||||
- name: Include service setup
|
||||
ansible.builtin.include_tasks: service.yml
|
||||
@@ -1,38 +1,102 @@
|
||||
---
|
||||
- name: Wait for runner to appear in Gitea API
|
||||
ansible.builtin.uri:
|
||||
url: "{{ gitea_url }}/api/v1/admin/runners"
|
||||
headers:
|
||||
Authorization: "token {{ registration_token }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
return_content: true
|
||||
register: runners_response
|
||||
until: >
|
||||
runners_response.json.runners | default([]) |
|
||||
selectattr('name', 'equalto', runner_name) | list | length > 0
|
||||
retries: 12
|
||||
delay: 10
|
||||
when: gitea_url is defined and registration_token is defined
|
||||
- name: Check runner registration file exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||
register: runner_file_stat
|
||||
|
||||
- name: Verify runner is online
|
||||
ansible.builtin.uri:
|
||||
url: "{{ gitea_url }}/api/v1/admin/runners"
|
||||
headers:
|
||||
Authorization: "token {{ registration_token }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
return_content: true
|
||||
register: runners_check
|
||||
when: gitea_url is defined and registration_token is defined
|
||||
- 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: Fail if runner is not online
|
||||
- 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 online in Gitea"
|
||||
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
|
||||
- registration_token is defined
|
||||
- >
|
||||
runners_check.json.runners | default([]) |
|
||||
selectattr('name', 'equalto', runner_name) |
|
||||
selectattr('status', 'equalto', 'online') | list | length == 0
|
||||
- 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,24 +1,19 @@
|
||||
---
|
||||
- name: Include OS-specific Docker installation
|
||||
ansible.builtin.include_tasks: docker.yml
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_tasks: systemd_check.yml
|
||||
|
||||
- name: Include act_runner download
|
||||
ansible.builtin.include_tasks: download_act_runner.yml
|
||||
- name: Include user setup
|
||||
ansible.builtin.include_tasks: user_setup.yml
|
||||
|
||||
- name: Include validation
|
||||
ansible.builtin.include_tasks: validate.yml
|
||||
- name: Include rootless Docker setup
|
||||
ansible.builtin.include_tasks: rootless_docker.yml
|
||||
|
||||
- name: Include config creation
|
||||
ansible.builtin.include_tasks: config.yml
|
||||
|
||||
- name: Include registration
|
||||
ansible.builtin.include_tasks: register.yml
|
||||
|
||||
- name: Include service setup
|
||||
ansible.builtin.include_tasks: service.yml
|
||||
- name: Include runner install
|
||||
ansible.builtin.include_tasks: install_runner.yml
|
||||
|
||||
- name: Include prune setup
|
||||
ansible.builtin.include_tasks: prune.yml
|
||||
|
||||
- name: Include integration test
|
||||
ansible.builtin.include_tasks: integration_test.yml
|
||||
when: not skip_runner_registration
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
---
|
||||
- name: Create docker-prune service file
|
||||
- name: Create docker-prune user service file
|
||||
ansible.builtin.template:
|
||||
src: docker-prune.service.j2
|
||||
dest: /etc/systemd/system/docker-prune.service
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Create docker-prune timer file
|
||||
- name: Create docker-prune user timer file
|
||||
ansible.builtin.template:
|
||||
src: docker-prune.timer.j2
|
||||
dest: /etc/systemd/system/docker-prune.timer
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Enable and start docker-prune timer
|
||||
ansible.builtin.systemd:
|
||||
name: docker-prune.timer
|
||||
state: started
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
- name: Reload systemd user daemon for prune timer
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
|
||||
- name: Enable and start docker-prune user timer
|
||||
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
---
|
||||
- name: Ensure work directory exists
|
||||
ansible.builtin.file:
|
||||
path: /var/lib/gitea-runner
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Check if runner is already registered
|
||||
ansible.builtin.stat:
|
||||
path: /var/lib/gitea-runner/.runner
|
||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||
register: runner_registered
|
||||
|
||||
- name: Register runner with Gitea
|
||||
ansible.builtin.command: >
|
||||
/usr/local/bin/act_runner register
|
||||
{{ gitea_runner_binary_path }} register
|
||||
--token {{ registration_token }}
|
||||
--name {{ runner_name }}
|
||||
--instance {{ gitea_url }}
|
||||
--labels ubuntu-latest:docker://node:16-bullseye
|
||||
--labels {{ runner_labels }}
|
||||
--no-interactive
|
||||
args:
|
||||
chdir: /var/lib/gitea-runner
|
||||
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: not runner_registered.stat.exists
|
||||
register: register_output
|
||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
||||
timeout: 60
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Resolve runner identity facts for stop/start/status/restart playbooks.
|
||||
# These playbooks use include_role with tasks_from, which does NOT expose
|
||||
# role defaults to the playbook's task-level keywords (become_user, etc).
|
||||
# We set the facts explicitly here so they're available everywhere.
|
||||
|
||||
- name: Resolve runner service user
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ runner_name }}"
|
||||
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
||||
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
||||
|
||||
- name: Resolve runner data and config dirs
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
|
||||
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
|
||||
|
||||
- name: Resolve runner service user UID
|
||||
ansible.builtin.getent:
|
||||
database: passwd
|
||||
key: "{{ gitea_runner_service_user }}"
|
||||
|
||||
- name: Set runner UID fact
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_uid: "{{ getent_passwd[gitea_runner_service_user][1] }}"
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
- 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
|
||||
@@ -1,16 +1,30 @@
|
||||
---
|
||||
- name: Create systemd service file
|
||||
- name: Create systemd user service file
|
||||
ansible.builtin.template:
|
||||
src: act-runner.service.j2
|
||||
dest: "/etc/systemd/system/act-runner-{{ runner_name }}.service"
|
||||
src: gitea-runner-user.service.j2
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
notify:
|
||||
- Reload systemd
|
||||
- Restart act-runner
|
||||
|
||||
- name: Enable and start act-runner service
|
||||
ansible.builtin.systemd:
|
||||
name: "act-runner-{{ runner_name }}"
|
||||
state: started
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
- name: Reload systemd user daemon
|
||||
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:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
|
||||
- name: Enable and start gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user enable --now gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Check if systemd is available
|
||||
ansible.builtin.stat:
|
||||
path: /run/systemd/system
|
||||
register: systemd_available
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Include gitea_runner download
|
||||
ansible.builtin.include_tasks: download_gitea_runner.yml
|
||||
|
||||
- name: Restart gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user restart gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
when:
|
||||
- systemd_available.stat.exists | default(false) | bool
|
||||
- docker_rootless_setup
|
||||
changed_when: true
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
- name: Create per-runner system user
|
||||
ansible.builtin.user:
|
||||
name: "{{ gitea_runner_service_user }}"
|
||||
home: "{{ gitea_runner_home }}"
|
||||
shell: /bin/bash
|
||||
system: true
|
||||
create_home: true
|
||||
register: runner_user
|
||||
|
||||
- name: Set runner UID fact
|
||||
ansible.builtin.set_fact:
|
||||
gitea_runner_uid: "{{ runner_user.uid }}"
|
||||
|
||||
- name: Check if lingering is already enabled
|
||||
ansible.builtin.stat:
|
||||
path: "/var/lib/systemd/linger/{{ gitea_runner_service_user }}"
|
||||
register: linger_stat
|
||||
|
||||
- name: Enable lingering for runner user
|
||||
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
|
||||
changed_when: not linger_stat.stat.exists
|
||||
when: systemd_available.stat.exists
|
||||
|
||||
- name: Ensure subuid entry for runner user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subuid
|
||||
regexp: "^{{ gitea_runner_service_user }}:"
|
||||
line: "{{ gitea_runner_service_user }}:100000:65536"
|
||||
create: true
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure subgid entry for runner user
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/subgid
|
||||
regexp: "^{{ gitea_runner_service_user }}:"
|
||||
line: "{{ gitea_runner_service_user }}:100000:65536"
|
||||
create: true
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure XDG_RUNTIME_DIR exists
|
||||
ansible.builtin.file:
|
||||
path: "/run/user/{{ gitea_runner_uid }}"
|
||||
state: directory
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: Ensure runner data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
state: directory
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure runner config directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_config_dir }}"
|
||||
state: directory
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure systemd user directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user"
|
||||
state: directory
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
@@ -1,24 +1,26 @@
|
||||
---
|
||||
- name: Check act_runner binary exists
|
||||
- name: Check gitea_runner binary exists
|
||||
ansible.builtin.stat:
|
||||
path: /usr/local/bin/act_runner
|
||||
register: act_runner_stat
|
||||
path: "{{ gitea_runner_binary_path }}"
|
||||
register: gitea_runner_stat
|
||||
|
||||
- name: Fail if act_runner binary is missing
|
||||
- name: Fail if gitea_runner binary is missing
|
||||
ansible.builtin.fail:
|
||||
msg: "act_runner binary not found at /usr/local/bin/act_runner"
|
||||
when: not act_runner_stat.stat.exists
|
||||
msg: "gitea_runner binary not found at {{ gitea_runner_binary_path }}"
|
||||
when: not gitea_runner_stat.stat.exists
|
||||
|
||||
- name: Verify act_runner is executable
|
||||
ansible.builtin.command: /usr/local/bin/act_runner --version
|
||||
register: act_runner_version_output
|
||||
- name: Verify gitea_runner is executable
|
||||
ansible.builtin.command: "{{ gitea_runner_binary_path }} --version"
|
||||
register: gitea_runner_version_output
|
||||
changed_when: false
|
||||
|
||||
- name: Verify Docker connectivity
|
||||
- name: Verify rootless Docker connectivity
|
||||
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_version_output
|
||||
changed_when: false
|
||||
|
||||
- name: Set runner_validated fact
|
||||
ansible.builtin.set_fact:
|
||||
runner_validated: true
|
||||
when: docker_rootless_setup
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
log.level = "info"
|
||||
runner.file = ".runner"
|
||||
container.label = "gitea-runner=true"
|
||||
@@ -1,16 +0,0 @@
|
||||
[Unit]
|
||||
Description=Gitea Actions Runner ({{ runner_name }})
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/act_runner daemon --config /etc/act-runner/config.toml
|
||||
WorkingDirectory=/var/lib/gitea-runner
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User={{ ansible_user | default('root') }}
|
||||
Group=docker
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,9 +1,9 @@
|
||||
[Unit]
|
||||
Description=Docker prune for Gitea runner resources
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/docker system prune -f --filter "label=gitea-runner=true" --filter "until=24h"
|
||||
ExecStart=/usr/bin/docker volume prune -f --filter "label=gitea-runner=true" --filter "until=24h"
|
||||
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
ExecStart=/usr/bin/docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
||||
ExecStart=/usr/bin/docker volume prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=Daily Docker prune for Gitea runner resources
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
OnCalendar={{ gitea_runner_prune_schedule }}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
log:
|
||||
level: "{{ gitea_runner_log_level }}"
|
||||
|
||||
runner:
|
||||
file: "{{ gitea_runner_file }}"
|
||||
fetch_timeout: 50s
|
||||
fetch_interval: 2s
|
||||
|
||||
container:
|
||||
label: "{{ gitea_runner_container_label }}"
|
||||
docker_host: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Gitea Actions Runner (rootless)
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={{ gitea_runner_binary_path }} daemon --config {{ gitea_runner_config_dir }}/config.yaml
|
||||
WorkingDirectory={{ gitea_runner_data_dir }}
|
||||
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
ExecStop=/bin/kill -TERM $MAINPID
|
||||
TimeoutStopSec=30
|
||||
Restart=on-failure
|
||||
RestartSec={{ gitea_runner_service_restart_sec }}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
act_runner_version: "latest"
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
- name: Start Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Resolve runner UID
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: resolve_uid.yml
|
||||
|
||||
- name: Check if runner is already registered
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||
register: runner_registered
|
||||
|
||||
- name: Include registration if not registered
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: register.yml
|
||||
when:
|
||||
- not runner_registered.stat.exists
|
||||
- not skip_runner_registration | default(false)
|
||||
|
||||
- name: Start gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user start gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
- name: Status of Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Resolve runner UID
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: resolve_uid.yml
|
||||
|
||||
- name: Check systemd user service status
|
||||
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_status
|
||||
changed_when: false
|
||||
when: systemd_available.stat.exists
|
||||
|
||||
- name: Report service status
|
||||
ansible.builtin.debug:
|
||||
msg: "Service gitea-runner: {{ service_status.stdout | default('unknown') | trim }}"
|
||||
when: systemd_available.stat.exists
|
||||
|
||||
- name: Check runner registration file
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||
register: runner_file_stat
|
||||
|
||||
- name: Report runner registration
|
||||
ansible.builtin.debug:
|
||||
msg: "Runner registration file exists: {{ runner_file_stat.stat.exists | default(false) }}"
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Stop Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include systemd availability check
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: systemd_check.yml
|
||||
|
||||
- name: Resolve runner UID
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: resolve_uid.yml
|
||||
|
||||
- name: Stop gitea-runner user service
|
||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
@@ -1,17 +1,10 @@
|
||||
---
|
||||
- name: Update Gitea Actions runner binary
|
||||
- name: Update Gitea Actions runner
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
act_runner_version: "{{ act_runner_version | default('latest') }}"
|
||||
vars: {}
|
||||
tasks:
|
||||
- name: Include download and validate tasks
|
||||
- name: Update runner
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
tasks_from: download_act_runner.yml
|
||||
|
||||
- name: Restart act-runner service
|
||||
ansible.builtin.systemd:
|
||||
name: "act-runner-{{ runner_name | default(inventory_hostname) }}"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
tasks_from: update_runner.yml
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# git-cliff configuration for GRM
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
"""
|
||||
trim = true
|
||||
render_always = true
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
require_conventional = false
|
||||
split_commits = false
|
||||
protect_breaking_commits = false
|
||||
filter_commits = false
|
||||
fail_on_unmatched_commit = false
|
||||
use_branch_tags = false
|
||||
topo_order = false
|
||||
topo_order_commits = true
|
||||
sort_commits = "oldest"
|
||||
recurse_submodules = false
|
||||
|
||||
commit_preprocessors = [
|
||||
# Strip GRM-N: task ID prefix from squash-merge commits so git-cliff sees conventional commits
|
||||
{ pattern = "^GRM-\\d+:\\s+", replace = "" },
|
||||
]
|
||||
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->Bug Fixes" },
|
||||
{ message = "^perf", group = "<!-- 4 -->Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->Refactor" },
|
||||
# Skip infrastructure-only commits — they don't affect users
|
||||
{ message = "^doc", skip = true },
|
||||
{ message = "^test", skip = true },
|
||||
{ message = "^style", skip = true },
|
||||
{ message = "^chore", skip = true },
|
||||
{ message = "^ci", skip = true },
|
||||
# Skip release commits — they are release artifacts, not features
|
||||
{ message = "^release:", skip = true },
|
||||
{ body = ".*security", group = "<!-- 8 -->Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->Revert" },
|
||||
# Skip anything that doesn't match above — safe default
|
||||
{ message = ".*", skip = true },
|
||||
]
|
||||
|
||||
[bump]
|
||||
features_always_bump_minor = true
|
||||
breaking_always_bump_major = false
|
||||
initial_tag = "0.1.0"
|
||||
# Refactor commits bump patch — structural changes to src/ or pyproject.toml
|
||||
# affect users even though no new feature was added.
|
||||
refactor_always_bump_patch = true
|
||||
@@ -0,0 +1,67 @@
|
||||
# GRM — Gitea Runner Manager
|
||||
|
||||
A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts.
|
||||
|
||||
Each runner runs in an isolated **rootless Docker** environment under a dedicated system user, enabling multiple runners to operate in parallel on the same host without conflicts. GRM handles the entire runner lifecycle — from initial installation and registration with Gitea, through start/stop/enable/disable operations, to clean removal with deregistration.
|
||||
|
||||
> **Pronunciation:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM") — the Bulgarian word for **thunder**. An open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
GRM is a two-layer tool: a Python CLI (built with Click) that delegates to an idempotent Ansible role for all remote operations. The CLI handles argument parsing, environment loading, internationalisation, and local registry management. The Ansible role handles the actual runner setup — creating dedicated system users, configuring rootless Docker, downloading and registering the runner binary, creating systemd user services, and setting up Docker prune timers.
|
||||
|
||||
### Key capabilities
|
||||
|
||||
- **Rootless Docker isolation** — Each runner gets its own rootless Docker daemon under a dedicated system user (`grm-<name>`).
|
||||
- **Multi-instance support** — Multiple isolated runners on the same host, each with independent users, data directories, and systemd services.
|
||||
- **Full lifecycle CLI** — `install`, `update`, `start`, `stop`, `enable`, `disable`, `status`, `remove`, `list`.
|
||||
- **Idempotent Ansible role** — Safe to re-run; second run produces zero changes.
|
||||
- **Automatic integration testing** — Every installation verifies the `.runner` registration file and systemd service state.
|
||||
- **Local runner registry** — Connection details stored locally; manage runners by name after installation.
|
||||
- **Internationalisation** — Console messages in English, Bulgarian, German, Russian, Chinese, and Polish.
|
||||
- **Security-conscious** — Secrets passed via temporary JSON files with `0600` permissions (CWE-214).
|
||||
|
||||
### Supported operating systems
|
||||
|
||||
| OS | Versions | Package manager |
|
||||
|----|----------|-----------------|
|
||||
| Arch Linux | rolling | pacman |
|
||||
| Ubuntu | 22.04, 24.04 | apt |
|
||||
| Debian | 12 | apt |
|
||||
|
||||
All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files.
|
||||
|
||||
## User Documentation
|
||||
|
||||
- [Getting Started](Getting-Started.-) — Installation, quick start, token setup, first run, log viewing
|
||||
- [Installation](Installation) — Prerequisites, setup methods, multiple instances, runner registry
|
||||
- [CLI Commands](CLI-Commands.-) — All commands with arguments, options, and examples
|
||||
- [Troubleshooting](Troubleshooting) — Common issues, diagnostics, and solutions
|
||||
- [FAQ](FAQ) — Frequently asked questions
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
- [Architecture](Architecture) — High-level design, component diagram, data flow, security model, per-runner isolation
|
||||
- [Development Setup](Development-Setup.-) — Environment setup, project structure, dependencies, linting, testing
|
||||
- [CI/CD Workflow](CI-CD-Workflow.-) — PR workflow, branch protection, release pipeline, change classification, badge generation
|
||||
- [Testing Strategy](Testing-Strategy.-) — Unit tests, Molecule scenarios, integration tests, CI distribution
|
||||
- [Decision Log](Decision-Log.-) — Key technical decisions and rationale (ADRs)
|
||||
- [Contributing Guide](Contributing-Guide.-) — Coding standards, PR workflow, commit conventions, Ansible role conventions
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Repository](https://git.oblachno.oblachno.fyi/oblachno-oss/grm)
|
||||
- [Releases](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
- [Issues](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/issues)
|
||||
- [CI/CD Pipeline](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
- [Changelog](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/CHANGELOG.md)
|
||||
- [License (GPL-3.0)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"index.md": "Home",
|
||||
"user/getting-started.md": "Getting-Started",
|
||||
"user/installation.md": "Installation",
|
||||
"user/cli-commands.md": "CLI-Commands",
|
||||
"user/troubleshooting.md": "Troubleshooting",
|
||||
"user/faq.md": "FAQ",
|
||||
"tech/architecture.md": "Architecture",
|
||||
"tech/development-setup.md": "Development-Setup",
|
||||
"tech/ci-cd-workflow.md": "CI-CD-Workflow",
|
||||
"tech/testing-strategy.md": "Testing-Strategy",
|
||||
"tech/decision-log.md": "Decision-Log",
|
||||
"tech/contributing.md": "Contributing-Guide"
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
# Architecture
|
||||
|
||||
GRM consists of two layers:
|
||||
|
||||
1. **Python CLI** (`src/gitea_runner_manager/`) — 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.
|
||||
|
||||
## High-Level Design
|
||||
|
||||
The CLI is a thin orchestration layer. It does not perform any remote operations itself — every action (install, update, start, stop, etc.) is delegated to an Ansible playbook. The CLI's responsibilities are:
|
||||
|
||||
- Parsing command-line arguments and options
|
||||
- Loading configuration from `.env` (via python-dotenv)
|
||||
- Resolving runner connection details from the local registry
|
||||
- Writing secrets to temporary JSON files (CWE-214 mitigation)
|
||||
- Constructing the `ansible-playbook` command with appropriate inventory, user, key, and extra-vars
|
||||
- Capturing and streaming Ansible output to log files
|
||||
- Maintaining the local runner registry (`~/.local/share/grm/runners.json`)
|
||||
- Providing colorised console output and operation reports
|
||||
|
||||
The Ansible role handles all remote state: user creation, package installation, Docker configuration, binary download, runner registration, systemd service management, and Docker prune timers.
|
||||
|
||||
## Component Tree
|
||||
|
||||
```
|
||||
grm install <host>
|
||||
└── RunnerManager.install()
|
||||
└── ansible-playbook ansible/install-runner.yml
|
||||
└── role: gitea-runner
|
||||
├── user_setup.yml (create per-runner system user + lingering)
|
||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||
├── install_runner.yml (download binary, config, register, service)
|
||||
├── prune.yml (Docker prune timer)
|
||||
└── integration_test.yml (validate service is active)
|
||||
```
|
||||
|
||||
The Ansible role task execution order (from `AGENTS.md`):
|
||||
|
||||
```
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
||||
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files)
|
||||
|
||||
### Ansible task files
|
||||
|
||||
| Task file | Responsibility |
|
||||
|-----------|---------------|
|
||||
| `main.yml` | Entry point — includes all other task files in order |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `validate.yml` | Validates the downloaded binary |
|
||||
| `register.yml` | Registers the runner with Gitea using the registration token |
|
||||
| `service.yml` | Creates the systemd user service file and starts/enables the service |
|
||||
| `prune.yml` | Creates a systemd user timer for daily Docker image and volume pruning |
|
||||
| `integration_test.yml` | Verifies the `.runner` file exists and the systemd service is active; optionally queries the Gitea API |
|
||||
| `deregister.yml` | Deregisters the runner from Gitea and removes the `.runner` file |
|
||||
| `update_runner.yml` | Downloads a new version of the gitea_runner binary |
|
||||
|
||||
### Ansible templates
|
||||
|
||||
| Template | Purpose |
|
||||
|----------|---------|
|
||||
| `gitea-runner-user.service.j2` | Systemd user service for the gitea_runner daemon |
|
||||
| `gitea-runner-config.yaml.j2` | Runner configuration file (labels, capacity, log level) |
|
||||
| `docker-prune.service.j2` | Systemd user service for Docker pruning (oneshot) |
|
||||
| `docker-prune.timer.j2` | Systemd user timer triggering daily Docker prune |
|
||||
|
||||
## Per-Runner Isolation
|
||||
|
||||
Each runner runs as a systemd user service under a dedicated system user (`grm-<name>`). Each instance has fully isolated resources:
|
||||
|
||||
- **User**: `grm-<name>` (dedicated system user with lingering enabled)
|
||||
- **Home**: `/home/grm-<name>/`
|
||||
- **Data**: `/var/lib/gitea-runner/<name>/`
|
||||
- **Config**: `/etc/gitea-runner/<name>/`
|
||||
- **Service**: `gitea-runner.service` (systemd user service)
|
||||
- **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner)
|
||||
- **subuid/subgid**: `grm-<name>:100000:65536` (user namespace mapping)
|
||||
|
||||
Lingering is enabled via `loginctl enable-linger` so the user's systemd services run without an active login session. This is essential for runners that need to operate continuously.
|
||||
|
||||
## Component Interactions
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI["Python CLI<br/>src/gitea_runner_manager/<br/>(Click)"]
|
||||
RM["RunnerManager<br/>runner_manager.py"]
|
||||
EXEC["Executor<br/>executor.py"]
|
||||
REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"]
|
||||
ANS["ansible-playbook subprocess"]
|
||||
ROLE["Ansible Role<br/>ansible/roles/gitea-runner/"]
|
||||
USER["user_setup.yml<br/>create system user + lingering"]
|
||||
DOCKER["rootless_docker.yml<br/>rootless Docker setup"]
|
||||
INSTALL["install_runner.yml<br/>download, config, register, service"]
|
||||
PRUNE["prune.yml<br/>Docker prune timer"]
|
||||
TEST["integration_test.yml<br/>validate service active"]
|
||||
GITEA["Gitea instance<br/>registration + API"]
|
||||
SYSTEMD["systemd user service<br/>gitea-runner.service"]
|
||||
LOG["Log files<br/>~/.local/state/grm/logs/"]
|
||||
|
||||
CLI --> RM
|
||||
RM --> REG
|
||||
RM --> EXEC
|
||||
EXEC -->|subprocess| ANS
|
||||
EXEC -->|stream output| LOG
|
||||
ANS --> ROLE
|
||||
ROLE --> USER
|
||||
ROLE --> DOCKER
|
||||
ROLE --> INSTALL
|
||||
ROLE --> PRUNE
|
||||
ROLE --> TEST
|
||||
INSTALL -->|register| GITEA
|
||||
INSTALL --> SYSTEMD
|
||||
DOCKER --> SYSTEMD
|
||||
TEST -->|optional API check| GITEA
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Installation flow
|
||||
|
||||
1. User runs `grm install <host> --user <user> --key <key> --name <name>`
|
||||
2. CLI loads `.env` for `GITEA_URL` and `GITEA_REGISTRATION_TOKEN`
|
||||
3. `RunnerManager.install()` constructs extra-vars dict with registration token, runner name, Gitea URL, and optional admin token/labels
|
||||
4. Extra-vars are written to a temporary JSON file with `0600` permissions
|
||||
5. `AnsibleExecutor.run()` invokes `ansible-playbook ansible/install-runner.yml` with the temp file via `--extra-vars @tempfile`
|
||||
6. Ansible connects to the remote host via SSH and executes the role:
|
||||
- Creates system user `grm-<name>` with lingering
|
||||
- Installs Docker packages and sets up rootless Docker
|
||||
- Downloads the gitea_runner binary
|
||||
- Creates the runner config file
|
||||
- Registers the runner with Gitea
|
||||
- Creates and starts the systemd user service
|
||||
- Sets up the Docker prune timer
|
||||
- Runs the integration test (verifies `.runner` file and service state)
|
||||
7. Ansible output is streamed to a timestamped log file at `~/.local/state/grm/logs/ansible-<timestamp>.log`
|
||||
8. On success, the runner is added to the local registry at `~/.local/share/grm/runners.json`
|
||||
9. The temporary extra-vars file is deleted
|
||||
|
||||
### Lifecycle command flow
|
||||
|
||||
1. User runs `grm <command> <runner_name>` (e.g., `grm stop prod-runner`)
|
||||
2. `RunnerManager._resolve_runner()` looks up the runner in the local registry
|
||||
3. If `--host` and `--user` are provided, they override registry values
|
||||
4. The corresponding playbook is executed (e.g., `stop-runner.yml`)
|
||||
5. Ansible connects to the remote host and performs the action
|
||||
|
||||
### List command flow
|
||||
|
||||
1. User runs `grm list`
|
||||
2. `RunnerManager.list_runners()` reads all entries from the local registry
|
||||
3. For each runner, an Ansible ad-hoc command checks `systemctl --user is-active gitea-runner`
|
||||
4. Results are displayed in a table with columns: NAME, HOST, USER, LABELS, STATUS
|
||||
|
||||
## Security Model
|
||||
|
||||
### Rootless Docker
|
||||
|
||||
Each runner operates under a dedicated unprivileged system user. The Docker daemon runs in rootless mode via `dockerd-rootless-setuptool.sh install`, which configures:
|
||||
|
||||
- User namespace mapping via `/etc/subuid` and `/etc/subgid` (range: 100000-165535)
|
||||
- Rootless Docker socket at `/run/user/<UID>/docker.sock`
|
||||
- `slirp4netns` for user-mode networking
|
||||
- `fuse-overlayfs` for rootless container storage
|
||||
|
||||
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.
|
||||
|
||||
### Secret handling
|
||||
|
||||
Registration tokens and admin API tokens are never exposed on the command line. The `RunnerManager._extra_vars_file()` context manager:
|
||||
|
||||
1. Creates a temporary file via `tempfile.mkstemp()`
|
||||
2. Writes the extra-vars JSON to the file
|
||||
3. Sets permissions to `0600` (owner read/write only)
|
||||
4. Passes the file to Ansible via `--extra-vars @tempfile`
|
||||
5. Deletes the file in a `finally` block, even if an exception occurs
|
||||
|
||||
This prevents secrets from appearing in the process list (`ps aux`), addressing CWE-214.
|
||||
|
||||
### No shell injection
|
||||
|
||||
The CLI never uses `shell=True` with subprocess. All Ansible commands are constructed as argument lists (`list[str]`), preventing shell injection attacks. The `subprocess.Popen` and `subprocess.run` calls are marked with `nosec` comments after security review.
|
||||
|
||||
### Bandit security scanning
|
||||
|
||||
The CI pipeline runs Bandit on every PR to catch common Python security issues. The scan covers all source code in `src/`.
|
||||
|
||||
## Additional Components
|
||||
|
||||
From `AGENTS.md`, the project also includes:
|
||||
|
||||
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications. This package is not part of the GRM tool itself — it provides the CI/CD automation infrastructure.
|
||||
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits.
|
||||
|
||||
## Python Modules
|
||||
|
||||
The Python CLI layer (`src/gitea_runner_manager/`) consists of the following modules:
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `cli.py` | Click-based CLI entry point — defines all commands (install, update, start, stop, enable, disable, status, remove, list) |
|
||||
| `runner_manager.py` | Ansible orchestration + registry integration — delegates to executor and manages runner lifecycle |
|
||||
| `executor.py` | Ansible subprocess execution — runs `ansible-playbook` with extra-vars via temp JSON files, streams output to log files |
|
||||
| `registry.py` | Local JSON runner registry at `~/.local/share/grm/runners.json` — stores connection metadata |
|
||||
| `i18n.py` | Internationalisation translations (en, bg, de, ru, zh, pl) — opt-in via `GRM_LANG` environment variable |
|
||||
| `exceptions.py` | Custom exceptions (`GRMError`, `AnsibleError`) |
|
||||
| `logging_config.py` | Logging configuration — writes all messages to `~/.local/state/grm/logs/grm.log` at DEBUG level |
|
||||
| `report.py` | Operation report tracking — prints a step-by-step report with status icons after each command |
|
||||
| `ui.py` | User-facing output utilities — colorised console output via `click.style`, with log file always receiving plain text |
|
||||
| `translations.json` | Translation strings for all supported languages |
|
||||
|
||||
## Logging
|
||||
|
||||
GRM writes to two destinations:
|
||||
|
||||
| Destination | Level | Content |
|
||||
|-------------|-------|---------|
|
||||
| Console (stdout) | `GRM_LOG_LEVEL` (default: INFO) | Colorised user-facing messages and operation reports |
|
||||
| `~/.local/state/grm/logs/grm.log` | DEBUG | All messages with timestamps and severity |
|
||||
| `~/.local/state/grm/logs/ansible-<timestamp>.log` | — | Full Ansible playbook output per execution |
|
||||
|
||||
Console output is automatically colorised via `click.style`: operation headers in bright cyan, completed steps in green, failures in red, and status updates in yellow. The log file always captures plain text (no ANSI codes) at DEBUG level regardless of the console setting.
|
||||
|
||||
Set `GRM_LOG_LEVEL` to one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` to control console verbosity.
|
||||
@@ -0,0 +1,334 @@
|
||||
# CI/CD Workflow
|
||||
|
||||
GRM uses a fully automated CI/CD pipeline built on Gitea Actions. Every change to master goes through a mandatory PR workflow with branch protection, automated review, and auto-merge. Releases are automated via git-cliff and conventional commits.
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `ci.yml` | PR opened/synchronized | Quality checks (lint, test, coverage) + molecule tests |
|
||||
| `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 |
|
||||
| `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.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
### 1. Create Vikunja Task
|
||||
|
||||
Create a task in Vikunja project 6 to get a `GRM-N` identifier.
|
||||
|
||||
### 2. Create Branch
|
||||
|
||||
```bash
|
||||
git checkout master && git pull
|
||||
git checkout -b GRM-N-short-description
|
||||
```
|
||||
|
||||
### 3. Implement Changes
|
||||
|
||||
- Write code following conventions
|
||||
- Write/update tests (100% coverage required)
|
||||
- Update documentation (CHANGELOG, README, AGENTS.md as needed)
|
||||
|
||||
### 4. Commit (Conventional Commits)
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
```
|
||||
|
||||
### 5. Push and Create PR
|
||||
|
||||
- **PR title format**: `GRM-N: <vikunja task title>` (must match the Vikunja task title exactly)
|
||||
- PR body: summary of changes, `Closes GRM-N`
|
||||
- Add `ready-to-merge` label **only after review is complete**
|
||||
|
||||
### 6. Review the PR (Mandatory — Before Adding ready-to-merge Label)
|
||||
|
||||
Review the full diff (`git diff master...HEAD`) focusing on:
|
||||
|
||||
- **Functional completeness**: Does the code do what it claims? Are all requirements met?
|
||||
- **Edge cases**: Are boundary conditions, empty inputs, error paths handled?
|
||||
- **Technical excellence**:
|
||||
- Architecture compliance and evolution
|
||||
- Single Responsibility Principle (SRP)
|
||||
- Deduplication (no copy-paste, single source of truth)
|
||||
- Code smells detection and removal
|
||||
- Best industry practices
|
||||
- Industry-grade code quality
|
||||
- Reusability
|
||||
- Clean code
|
||||
- Readability
|
||||
- Maintainability
|
||||
- Extensibility
|
||||
- **Performance**: No unnecessary allocations, O(n) vs O(n²), efficient data structures
|
||||
- **Security**: No secrets in logs/process list, input validation, no injection vectors
|
||||
- **User experience**: Clear error messages, intuitive CLI flags, helpful output
|
||||
- **Documentation**: Completeness and relevance of docs, CHANGELOG entries, AGENTS.md updates
|
||||
|
||||
Post review comments using `devx.ci.pr_review`:
|
||||
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||
--event REQUEST_CHANGES \
|
||||
--body "Review summary" \
|
||||
--comments-json comments.json
|
||||
```
|
||||
|
||||
### 7. Address Review Comments
|
||||
|
||||
Fix each comment one by one, commit, and push. Re-review until satisfied.
|
||||
|
||||
### 8. Approve and Merge
|
||||
|
||||
Once all comments are addressed:
|
||||
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||
--event APPROVE \
|
||||
--body "All comments addressed. LGTM."
|
||||
```
|
||||
|
||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||
|
||||
1. **Validate** PR title format and match against Vikunja task title
|
||||
2. **Check** that at least one APPROVE review exists
|
||||
3. Wait for all CI checks to pass
|
||||
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
|
||||
5. The post-merge workflow marks the Vikunja task as done
|
||||
6. The release workflow automatically versions, tags, and publishes
|
||||
|
||||
### 9. Post-Merge Automation
|
||||
|
||||
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 **release workflow** (`.gitea/workflows/release.yml`) triggers on push to `master` and automatically versions, tags, and publishes (see below).
|
||||
|
||||
## Branch Protection (Required Gitea Settings)
|
||||
|
||||
Configure the following branch protection rules for `master` in Gitea repo settings:
|
||||
|
||||
- **Require pull request**: No direct pushes to master
|
||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||
- **Require status checks**: CI quality + molecule tests must pass
|
||||
- **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.
|
||||
|
||||
## 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 `detect-changes` job:
|
||||
|
||||
- For pull requests: compares `origin/master` against the PR head SHA
|
||||
- For pushes to master: compares `HEAD~1` against `HEAD`
|
||||
- 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'`.
|
||||
|
||||
CI triggers only on `opened` and `synchronize` PR events (not `labeled`).
|
||||
|
||||
## CI Quality Job
|
||||
|
||||
The `quality` job in `.gitea/workflows/ci.yml` runs:
|
||||
|
||||
1. `make setup` — full environment setup
|
||||
2. `make lint-all` — ruff + pyright + bandit + ansible-lint + checkmake
|
||||
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
|
||||
5. `PYTHONPATH=src python -m devx.ci.release --dry-run` — release dry-run validation
|
||||
|
||||
## Automated Release Pipeline
|
||||
|
||||
After a PR is merged to master, the release pipeline runs automatically.
|
||||
|
||||
### Release Workflow (`.gitea/workflows/release.yml`)
|
||||
|
||||
- Triggers on push to `master`
|
||||
- Sets up full dev environment (`make setup`) so lint and tests can run
|
||||
- Installs git-cliff (version 2.13.0)
|
||||
- Configures git as `grm-ci-bot`
|
||||
- Runs `devx.ci.release` which uses **git-cliff** to:
|
||||
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only workflow/infrastructure files changed, the release is **skipped entirely** — no version bump, no tag, no publish
|
||||
- Calculate the next semver version from conventional commits since the last tag
|
||||
- Update `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
||||
- Update `CHANGELOG.md` with the new version section
|
||||
- **Run `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
||||
- If lint or tests fail, **abort immediately** — no commit, no tag
|
||||
- Commit with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents re-triggering post-merge on the release commit)
|
||||
- Create an annotated tag `vX.Y.Z` on the release commit
|
||||
- Push both the commit and tag to master
|
||||
- `--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
|
||||
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
|
||||
|
||||
### Publish Workflow (`.gitea/workflows/publish.yml`)
|
||||
|
||||
- Triggers on tag push (`v*`)
|
||||
- Installs git-cliff (version 2.13.0)
|
||||
- Installs build tools (`build`, `twine`, `requests`, `python-dotenv`, `click`)
|
||||
- Validates `PYPI_TOKEN` is set (warns if missing)
|
||||
- Builds the Python package
|
||||
- Optionally publishes to PyPI (if `PYPI_TOKEN` is set)
|
||||
- Creates a Gitea release with git-cliff-generated release notes
|
||||
- Uses `devx.ci.publish` for build and publish orchestration
|
||||
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
|
||||
|
||||
### Auto-Merge Workflow (`.gitea/workflows/auto-merge.yml`)
|
||||
|
||||
- Triggers on `pull_request` labeled events
|
||||
- Runs `devx.ci.auto_merge` with the branch name, PR title, repository, PR number, and label name
|
||||
- Validates PR title format, checks for APPROVE review, waits for CI, and squash-merges
|
||||
|
||||
### Post-Merge Workflow (`.gitea/workflows/post-merge.yml`)
|
||||
|
||||
- Triggers on push to `master`
|
||||
- Consolidates release, wiki sync, badge generation, and Vikunja task updates into a single workflow
|
||||
- **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).
|
||||
- **release** — Runs `devx.ci.release` (see Automated Release Pipeline below)
|
||||
- **sync-wiki** — Syncs documentation to the Gitea wiki via `devx.ci.sync_wiki`
|
||||
- **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.
|
||||
- **vikunja** — Marks the corresponding Vikunja task as done via `devx.ci.post_merge`
|
||||
|
||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||
|
||||
Not all changes require the full CI pipeline or a new release. The project uses
|
||||
`devx.ci.classify_changes` to classify changed files into two categories.
|
||||
|
||||
**Classification strategy (safe-by-default):** Any file NOT in the explicit
|
||||
workflow-only allowlist is treated as user-facing. This prevents new file types
|
||||
from accidentally skipping releases. Classification is config-driven via
|
||||
`[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**User-facing paths** (tool changes → release needed):
|
||||
- `src/gitea_runner_manager/**` — Python CLI source
|
||||
- `ansible/**` — Ansible role
|
||||
- `pyproject.toml` — Package metadata
|
||||
|
||||
**Workflow-only paths** (infrastructure → no release needed):
|
||||
- `.gitea/workflows/**`, `docs/**`, `tests/**`
|
||||
- `AGENTS.md`, `README.md`, `CHANGELOG.md`, `Makefile`, `cliff.toml`, etc.
|
||||
|
||||
**CI behavior based on classification:**
|
||||
- **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)
|
||||
- **Quality job** (lint, unit tests, coverage, doc-coverage): Always runs
|
||||
- **Release workflow**: `release.py` calls `classify_changes` to check if any
|
||||
user-facing files changed since the last tag. If not, the release is skipped
|
||||
entirely — no version bump, no tag, no publish.
|
||||
|
||||
### Dynamic Runner Discovery
|
||||
|
||||
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
|
||||
registered runners at three levels (repo, org, instance) and generates
|
||||
a matrix of runner indices. If the API query fails (e.g., no admin
|
||||
access for instance-level runners), it falls back to the
|
||||
`MOLECULE_RUNNERS` repo variable, then to a default of 3.
|
||||
|
||||
The `molecule-tests` job uses `fromJSON()` to consume the dynamic
|
||||
matrix, and passes the runner count to `python -m devx.molecule.distribute_molecule
|
||||
--max-runners` so test pairs are evenly distributed.
|
||||
|
||||
When adding or removing Gitea runners:
|
||||
1. If runners are registered at the repo/org level, they're auto-detected
|
||||
2. If runners are at the instance level, update the `MOLECULE_RUNNERS` repo variable
|
||||
3. The workflow automatically scales the matrix to match available runners
|
||||
|
||||
### Molecule Test Distribution
|
||||
|
||||
`devx.molecule.distribute_molecule` discovers all molecule scenarios
|
||||
under `ansible/roles/*/molecule/` and crosses them with the supported
|
||||
OS platform matrix (defined in `devx.molecule.platforms`), then splits
|
||||
the resulting test pairs evenly across the requested number of runners.
|
||||
Each pair is encoded as `scenario|platform_name|platform_image|platform_command`.
|
||||
|
||||
`devx.molecule.molecule_ci_guard` runs the actual molecule test for a
|
||||
given test pair, with CI context (Gitea URL, token, run ID) for
|
||||
reporting results back to the commit status API.
|
||||
|
||||
### Commit Message Validation
|
||||
|
||||
`devx.ci.validate_commit_msg` validates that commit messages
|
||||
follow the conventional commit format (`feat:`, `fix:`, `docs:`, etc.).
|
||||
It is used by the pre-commit hook to enforce conventional commits on
|
||||
feature branches.
|
||||
|
||||
### Release Commit Detection
|
||||
|
||||
The `detect-type` job in the post-merge workflow runs
|
||||
`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 detected, all post-merge jobs (release, sync-wiki, badges, vikunja)
|
||||
are skipped — the tag push triggers the publish workflow instead.
|
||||
|
||||
### Badge Generation and Push
|
||||
|
||||
The `badges` job in the post-merge workflow runs
|
||||
`devx.ci.push_badges` which:
|
||||
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`
|
||||
3. Creates an orphan `badges` branch
|
||||
4. Copies SVG files to the branch root
|
||||
5. Force-pushes the branch to the remote
|
||||
|
||||
The badges job depends on the `release` job and uses `if: always()` so it
|
||||
runs even if release fails or is skipped. This ensures the version badge
|
||||
always reflects the actual state of the repository after any release
|
||||
commits have been pushed.
|
||||
|
||||
## git-cliff Commit Preprocessing
|
||||
|
||||
Merge commits on master have the format `GRM-N <conventional commit>`. The `GRM-N ` prefix is not a valid conventional commit prefix, so `cliff.toml` includes a `commit_preprocessors` entry that strips it before parsing:
|
||||
|
||||
```toml
|
||||
commit_preprocessors = [
|
||||
# Strip GRM-N task ID prefix from merge commits so git-cliff sees conventional commits
|
||||
{ pattern = "^GRM-\\d+\\s+", replace = "" },
|
||||
]
|
||||
```
|
||||
|
||||
This ensures all merged work appears in the changelog.
|
||||
|
||||
### git-cliff Configuration Highlights (`cliff.toml`)
|
||||
|
||||
- `conventional_commits = true` — parse conventional commit format
|
||||
- `filter_unconventional = true` — skip non-conventional commits
|
||||
- `render_always = true` — always render the changelog
|
||||
- `trim = true` — trim whitespace
|
||||
- Commit parsers group commits into: Features, Bug Fixes, Documentation, Performance, Refactor, Styling, Testing, Miscellaneous Tasks, Security, Revert, Other
|
||||
- `chore(release): prepare for`, `chore(deps.*)`, `chore(pr)`, `chore(pull)` commits are skipped
|
||||
- `sort_commits = "oldest"` — oldest commits first
|
||||
|
||||
## Version Bumping Rules (git-cliff)
|
||||
|
||||
| Commit type | Version bump |
|
||||
|-------------|-------------|
|
||||
| `feat:` | minor (0.X.0) |
|
||||
| `fix:` | patch (0.0.X) |
|
||||
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
||||
| `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) |
|
||||
|
||||
From `cliff.toml` `[bump]` section:
|
||||
|
||||
- `features_always_bump_minor = true`
|
||||
- `breaking_always_bump_major = false`
|
||||
- `initial_tag = "0.1.0"`
|
||||
|
||||
The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version.
|
||||
|
||||
## Title Format Summary
|
||||
|
||||
| What | Format | Example |
|
||||
|------|--------|---------|
|
||||
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
|
||||
| Branch commits | `<conventional commit>` | `feat: add review script` |
|
||||
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
|
||||
| Merge commit | `GRM-N <conventional commit>` | `GRM-33 feat: add review script` |
|
||||
@@ -0,0 +1,228 @@
|
||||
# Contributing Guide
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Python 3.12+ required (ruff/pyright target `py312`)
|
||||
- 100% test coverage required (`--cov-fail-under=100`)
|
||||
- Conventional commits on feature branches (no `GRM-N:` prefix)
|
||||
- Branch names must include `GRM-N` task ID
|
||||
- Line length: 120 chars
|
||||
- Secrets are passed via temp JSON files, never on the command line (CWE-214)
|
||||
- CI triggers only on `opened` and `synchronize` PR events (not `labeled`)
|
||||
- No `print()` — use `click.echo()` via `ui.say()` for console output
|
||||
- No bare `except` — catch specific exceptions
|
||||
- No `TODO`/`FIXME` comments in committed code
|
||||
- No functions longer than 50 lines
|
||||
- No `shell=True` with subprocess
|
||||
- No `eval()` or `exec()`
|
||||
- No raw strings in `click.echo()` without `_()` wrapper (i18n)
|
||||
- No `open()` without `with` statement
|
||||
- No `Popen()` without cleanup
|
||||
|
||||
## Code Style Rules
|
||||
|
||||
- **Python version**: 3.12+ (ruff and pyright target `py312`)
|
||||
- **Line length**: 120 characters
|
||||
- **Test coverage**: 100% required (`--cov-fail-under=100`)
|
||||
- **Secrets handling**: Secrets are passed via temp JSON files with `0600` permissions, never on the command line (CWE-214). Extra-vars are written to a temporary JSON file and passed via `--extra-vars @tempfile`, which is deleted after execution. This prevents secrets from being visible in the process list (`ps aux`).
|
||||
- **Linting**: `make lint-all` runs ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
- **Formatting**: `ruff format` with double quotes and space indentation
|
||||
- **Type checking**: `pyright` in strict mode for `src/gitea_runner_manager/`
|
||||
- **Security scanning**: `bandit -r src/` on every PR
|
||||
- **Import rules**: `src/gitea_runner_manager/` NEVER imports from devx — the GRM tool is self-contained
|
||||
|
||||
## Commit Rules
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
ci: update workflow
|
||||
refactor: simplify executor
|
||||
test: add molecule scenario
|
||||
chore: update dependencies
|
||||
```
|
||||
|
||||
The pre-commit hook validates that commit messages follow the conventional commit format. Non-conventional commits are rejected.
|
||||
|
||||
### Version Bumping Rules
|
||||
|
||||
| Commit type | Version bump |
|
||||
|-------------|-------------|
|
||||
| `feat:` | minor (0.X.0) |
|
||||
| `fix:` | patch (0.0.X) |
|
||||
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
||||
| `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) |
|
||||
|
||||
## Branch Naming
|
||||
|
||||
| What | Format | Example |
|
||||
|------|--------|---------|
|
||||
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
|
||||
| Branch commits | `<conventional commit>` | `feat: add review script` |
|
||||
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
|
||||
| Merge commit | `GRM-N <conventional commit>` | `GRM-33 feat: add review script` |
|
||||
|
||||
## PR Workflow Summary
|
||||
|
||||
Every change to master goes through this workflow. No exceptions.
|
||||
|
||||
1. **Create Vikunja task** — get a `GRM-N` identifier (Vikunja project 6)
|
||||
2. **Create branch** — `GRM-N-short-description`
|
||||
3. **Implement** — write code, tests (100% coverage), update docs
|
||||
4. **Commit** — conventional commits (no `GRM-N:` prefix on branch)
|
||||
5. **Push & create PR** — title: `GRM-N: <vikunja task title>`, body: summary + `Closes GRM-N`
|
||||
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
|
||||
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
|
||||
### 1. Create Vikunja task
|
||||
|
||||
Create a task in Vikunja project 6 to get a `GRM-N` identifier.
|
||||
|
||||
### 2. Create branch
|
||||
|
||||
```bash
|
||||
git checkout master && git pull
|
||||
git checkout -b GRM-N-short-description
|
||||
```
|
||||
|
||||
### 3. Implement changes
|
||||
|
||||
- Write code following conventions above
|
||||
- Write/update tests (100% coverage required)
|
||||
- Update documentation (CHANGELOG, README, AGENTS.md, docs/ as needed)
|
||||
|
||||
### 4. Commit (conventional commits)
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
```
|
||||
|
||||
### 5. Push and create PR
|
||||
|
||||
- **PR title format**: `GRM-N: <vikunja task title>` (must match the Vikunja task title exactly)
|
||||
- PR body: summary of changes, `Closes GRM-N`
|
||||
- Add `ready-to-merge` label **only after review is complete**
|
||||
|
||||
### 6. Review the PR
|
||||
|
||||
Review the full diff (`git diff master...HEAD`) focusing on:
|
||||
|
||||
- **Functional completeness**: Does the code do what it claims? Are all requirements met?
|
||||
- **Edge cases**: Are boundary conditions, empty inputs, error paths handled?
|
||||
- **Technical excellence**:
|
||||
- Architecture compliance and evolution
|
||||
- Single Responsibility Principle (SRP)
|
||||
- Deduplication (no copy-paste, single source of truth)
|
||||
- Code smells detection and removal
|
||||
- Best industry practices
|
||||
- Industry-grade code quality
|
||||
- Reusability
|
||||
- Clean code
|
||||
- Readability
|
||||
- Maintainability
|
||||
- Extensibility
|
||||
- **Performance**: No unnecessary allocations, O(n) vs O(n^2), efficient data structures
|
||||
- **Security**: No secrets in logs/process list, input validation, no injection vectors
|
||||
- **User experience**: Clear error messages, intuitive CLI flags, helpful output
|
||||
- **Documentation**: Completeness and relevance of docs, CHANGELOG entries, AGENTS.md updates
|
||||
|
||||
Post review comments using `devx.ci.review_pr`:
|
||||
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
|
||||
--event REQUEST_CHANGES \
|
||||
--body "Review summary" \
|
||||
--comments-json comments.json
|
||||
```
|
||||
|
||||
### 7. Address review comments
|
||||
|
||||
Fix each comment one by one, commit, and push. Re-review until satisfied.
|
||||
|
||||
### 8. Approve and merge
|
||||
|
||||
Once all comments are addressed, post an approval review:
|
||||
|
||||
```bash
|
||||
CI_GITEA_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
|
||||
--event APPROVE --checklist-confirmed \
|
||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||
--body "All 13 checklist categories verified."
|
||||
```
|
||||
|
||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||
|
||||
1. Validate PR title format and match against Vikunja task title
|
||||
2. Check that at least one APPROVE review exists
|
||||
3. Wait for all CI checks to pass
|
||||
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated)
|
||||
5. The post-merge workflow marks the Vikunja task as done
|
||||
6. The release workflow 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.
|
||||
|
||||
### 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`:
|
||||
|
||||
- **Require pull request**: No direct pushes to master
|
||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||
- **Require status checks**: CI quality + molecule tests must pass
|
||||
- **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.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps, set up hooks, install CI tools
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||
make test-unit # Unit tests without coverage
|
||||
make molecule # All 6 scenarios on Ubuntu 22.04
|
||||
make molecule-all # All 6 scenarios on all 4 supported OSes
|
||||
make test-all # pytest-cov + molecule
|
||||
make workflow-check # Static lint + dry-run of workflow YAML
|
||||
```
|
||||
|
||||
## Ansible Role Conventions
|
||||
|
||||
```
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
||||
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files)
|
||||
- `apt` tasks use `cache_valid_time: 3600` to avoid unnecessary cache updates
|
||||
- `remove-runner.yml` runs `loginctl disable-linger` and removes subuid/subgid entries
|
||||
|
||||
## Change Classification
|
||||
|
||||
Not all changes require a new release. The project classifies changes using `devx.ci.classify_changes`:
|
||||
|
||||
**Workflow-only paths** (no release needed):
|
||||
- `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`
|
||||
- `AGENTS.md`, `README.md`, `CHANGELOG.md`, `Makefile`, `cliff.toml`
|
||||
- Lint config files, `.env.example`, `.gitignore`
|
||||
|
||||
**User-facing paths** (release needed):
|
||||
- `src/gitea_runner_manager/**` (except `__init__.py`)
|
||||
- `ansible/**`
|
||||
- `pyproject.toml`
|
||||
|
||||
When working on workflow/CI/docs-only changes, use `ci:` or `docs:` commit prefixes. Do NOT bump the version or create tags for workflow-only changes.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user` calls — this is expected (systemd module doesn't support user services) and skipped in `.ansible-lint`
|
||||
- Molecule Docker driver may print "Event loop is closed" warnings on interrupt — harmless
|
||||
@@ -0,0 +1,123 @@
|
||||
# Decision Log
|
||||
|
||||
Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `AGENTS.md`.
|
||||
|
||||
---
|
||||
|
||||
## ADR-001: Dynamic Versioning via `__init__.py`
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** Use `dynamic = ["version"]` in `pyproject.toml` with setuptools `attr` to source the version from `__version__` in `src/gitea_runner_manager/__init__.py`.
|
||||
|
||||
**Rationale:** `__init__.py` is the single source of truth for the version. The release script (`devx.ci.release`) only updates `__init__.py` — there is no need to touch `pyproject.toml`. `grm --version` reports this version directly. This eliminates version duplication across files and ensures the runtime version always matches the tagged release.
|
||||
|
||||
**Source:** `CHANGELOG.md` (Unreleased — Added), `AGENTS.md` (Version Bumping Rules)
|
||||
|
||||
---
|
||||
|
||||
## ADR-002: Rootless Docker per Runner
|
||||
|
||||
**Date:** Project inception (documented in README Architecture)
|
||||
|
||||
**Decision:** Each runner instance runs in an isolated rootless Docker environment under a dedicated system user (`grm-<name>`), with its own Docker socket at `/run/user/<UID>/docker.sock`.
|
||||
|
||||
**Rationale:** Rootless Docker per-runner avoids conflicts with the host's Docker installation and enables true parallel execution of multiple runners on the same host. Each instance has fully isolated resources: user, home, data directory, config directory, systemd user service, and Docker socket. This is a core feature of GRM — enabling multiple isolated runners on the same host. User namespace mapping is configured via `/etc/subuid` and `/etc/subgid` entries (range: 100000-165535). Lingering is enabled so the user's systemd services run without an active login session.
|
||||
|
||||
**Source:** `README.md` (Architecture, Features), `AGENTS.md` (Architecture)
|
||||
|
||||
---
|
||||
|
||||
## ADR-003: Conventional Commits + git-cliff for Automated Versioning
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** Use conventional commits on feature branches and git-cliff (`cliff.toml`) to calculate the next semver version from commit history, generate the changelog, and automate releases.
|
||||
|
||||
**Rationale:** `devx.ci.release` uses git-cliff to calculate the next version from conventional commits since the last tag. Merge commits on master have the format `GRM-N <conventional commit>`, so `cliff.toml` includes a `commit_preprocessors` entry that strips the `GRM-N ` prefix before parsing. Version bumping rules: `feat:` → minor, `fix:` → patch, `feat!:`/`BREAKING CHANGE` → minor (pre-1.0), `chore:`/`ci:`/`docs:` → no bump. This fully automates versioning and changelog generation — no manual version bumps are needed.
|
||||
|
||||
**Source:** `CHANGELOG.md` (Unreleased — Added), `AGENTS.md` (Automated Release Pipeline, git-cliff Commit Preprocessing, Version Bumping Rules), `cliff.toml`
|
||||
|
||||
---
|
||||
|
||||
## ADR-004: Enforce Tests Pass Before Tagging a Release
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.2)
|
||||
|
||||
**Decision:** The release workflow runs `make lint-ruff` and `make pytest-cov` before creating a release commit or tag. If lint or tests fail, the release aborts immediately — no commit, no tag.
|
||||
|
||||
**Rationale:** This ensures every tagged release is healthy. A `--skip-tests` flag exists for emergency use only but is not recommended. This decision was made as a bug fix after identifying that releases could be tagged without verifying test health. Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits.
|
||||
|
||||
**Source:** `CHANGELOG.md` (0.2.2 — Bug Fixes: "Enforce tests pass before tagging a release"), `AGENTS.md` (Automated Release Pipeline)
|
||||
|
||||
---
|
||||
|
||||
## ADR-005: Branch Protection + Auto-Merge Workflow
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** Require branch protection on `master` (require pull request, require approval review, require status checks, block force pushes) and use an auto-merge workflow that programmatically enforces the APPROVE review check.
|
||||
|
||||
**Rationale:** Branch protection is the primary gate — no direct pushes to master, at least 1 APPROVE review before merge, CI quality + molecule tests must pass, and no history rewriting. The auto-merge workflow (`devx.ci.auto_merge`) enforces the APPROVE review check programmatically as a defense-in-depth measure. When the `ready-to-merge` label is added, the workflow validates PR title format, checks for APPROVE review, waits for CI, and squash-merges with title `GRM-N <conventional commit message>`. The post-merge workflow then marks the Vikunja task as done. Branch protection is automatically configured by `devx.tools.configure_repo`.
|
||||
|
||||
**Source:** `CHANGELOG.md` (Unreleased — Added: mandatory PR review step, auto_merge.py), `AGENTS.md` (Branch Protection, PR Workflow step 8)
|
||||
|
||||
---
|
||||
|
||||
## ADR-006: Path-Based CI Filtering for Molecule Tests
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** The CI workflow 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.
|
||||
|
||||
**Rationale:** This prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. Molecule tests are only relevant when Ansible files change. The `molecule-tests` job depends on both `quality` and `detect-changes`, and only runs if `ansible-changed == 'true'`. CI triggers only on `opened` and `synchronize` PR events (not `labeled`) to avoid redundant runs.
|
||||
|
||||
**Source:** `AGENTS.md` (CI Path Filtering), `.gitea/workflows/ci.yml` (detect-changes job)
|
||||
|
||||
---
|
||||
|
||||
## ADR-007: Secrets via Temporary JSON Files (CWE-214)
|
||||
|
||||
**Date:** Project inception
|
||||
|
||||
**Decision:** Pass secrets (registration tokens, admin API tokens) to Ansible via temporary JSON files with `0600` permissions, never on the command line.
|
||||
|
||||
**Rationale:** Passing secrets as command-line arguments (e.g., `--extra-vars '{"token": "..."}'`) makes them visible in the process list (`ps aux`), which is a known security weakness (CWE-214). The `RunnerManager._extra_vars_file()` context manager writes extra-vars to a temporary file via `tempfile.mkstemp()`, sets permissions to `0600`, passes the file to Ansible via `--extra-vars @tempfile`, and deletes the file in a `finally` block — even if an exception occurs. This ensures secrets are never visible in the process list.
|
||||
|
||||
**Source:** `AGENTS.md` (Key Conventions), `src/gitea_runner_manager/runner_manager.py` (`_extra_vars_file` method)
|
||||
|
||||
---
|
||||
|
||||
## ADR-008: Smart CI — User-Facing vs Workflow-Only Change Classification
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** Classify changed files into user-facing and workflow-only categories using `devx.ci.classify_changes`. Only user-facing changes trigger a release; workflow-only changes (CI, docs, tests, lint config) do not.
|
||||
|
||||
**Rationale:** Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include `src/gitea_runner_manager/**` (except `__init__.py`) and `ansible/**`. Workflow-only paths include `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`, and various config files.
|
||||
|
||||
**Source:** `AGENTS.md` (Smart CI: User-Facing vs Workflow-Only Changes), `pyproject.toml` (`[tool.devx.classify]`)
|
||||
|
||||
---
|
||||
|
||||
## ADR-009: devx Package Separation
|
||||
|
||||
**Date:** 2026-06-21 (v0.6.2)
|
||||
|
||||
**Decision:** Separate CI/CD and development tooling into the `devx` package (installed from git), keeping the GRM tool itself self-contained in `src/gitea_runner_manager/`.
|
||||
|
||||
**Rationale:** The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from `gitea_runner_manager` (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the `scripts/` directory to the devx package in GRM-64.
|
||||
|
||||
**Source:** `AGENTS.md` (Source Code Separation and devx Integration), `CHANGELOG.md` (0.6.2 — Refactor: "Migrate from scripts/ to devx package")
|
||||
|
||||
---
|
||||
|
||||
## ADR-010: Dynamic Runner Discovery for Molecule CI
|
||||
|
||||
**Date:** 2026-06-21 (v0.5.0+)
|
||||
|
||||
**Decision:** Molecule tests are distributed across available Gitea Actions runners dynamically via `devx.molecule.discover_runners`, which queries the Gitea API for runners at all levels (repo, org, instance) and generates a dynamic matrix.
|
||||
|
||||
**Rationale:** Hardcoding the number of CI runners would require manual updates when runners are added or removed. Dynamic discovery auto-detects repo/org-level runners via the API. For instance-level runners (which may not be visible without admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable, then to a default of 3. The workflow automatically scales the matrix to match available runners, distributing test pairs evenly.
|
||||
|
||||
**Source:** `AGENTS.md` (Dynamic Runner Discovery), `.gitea/workflows/ci.yml` (discover-runners job)
|
||||
@@ -0,0 +1,240 @@
|
||||
# Development Setup
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── src/gitea_runner_manager/ # Python CLI source
|
||||
│ ├── cli.py # Click commands
|
||||
│ ├── runner_manager.py # Ansible orchestration + registry integration
|
||||
│ ├── executor.py # Ansible subprocess execution
|
||||
│ ├── registry.py # Local JSON runner registry
|
||||
│ ├── i18n.py # Translations (en, bg, de, ru, zh, pl)
|
||||
│ ├── exceptions.py # Custom exceptions
|
||||
│ ├── logging_config.py # Logging to ~/.local/state/grm/logs/
|
||||
│ ├── report.py # Operation report tracking
|
||||
│ ├── ui.py # Colorised console output
|
||||
│ └── translations.json # Translation strings
|
||||
├── ansible/
|
||||
│ ├── roles/gitea-runner/ # Main Ansible role
|
||||
│ │ ├── defaults/main.yml # Default variables
|
||||
│ │ ├── tasks/ # Task files (13 files)
|
||||
│ │ ├── templates/ # Jinja2 templates (4 files)
|
||||
│ │ └── molecule/ # Test scenarios (7 scenarios)
|
||||
│ ├── install-runner.yml # Install playbook
|
||||
│ ├── update-runner.yml # Update playbook
|
||||
│ ├── start-runner.yml # Start playbook
|
||||
│ ├── stop-runner.yml # Stop playbook
|
||||
│ ├── enable-runner.yml # Enable playbook
|
||||
│ ├── disable-runner.yml # Disable playbook
|
||||
│ ├── status-runner.yml # Status playbook
|
||||
│ └── remove-runner.yml # Remove playbook
|
||||
├── tests/
|
||||
│ ├── unit/ # Unit tests
|
||||
│ └── integration/ # Integration tests
|
||||
├── .gitea/workflows/ # CI/CD workflows
|
||||
├── docs/ # Documentation (synced to wiki)
|
||||
├── Makefile # Build & test automation
|
||||
├── pyproject.toml # Python project metadata
|
||||
├── cliff.toml # git-cliff configuration
|
||||
└── .env.example # Environment variable template
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.12+** — Required. The Makefile verifies this before creating the venv. Use `pyenv` to manage Python versions if needed.
|
||||
- **Git** — For cloning the repository and checking out release tags.
|
||||
- **Docker** — Only needed for running Molecule tests locally (`make molecule`).
|
||||
- **Go** — Only needed if you want to install `checkmake` manually (alternatively, `make setup` installs it via `devx.tools.install_checkmake`).
|
||||
|
||||
## Setup Development Environment
|
||||
|
||||
### Step 1: Clone and checkout latest release
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Checkout latest stable release
|
||||
```
|
||||
|
||||
> **Important:** Always checkout the latest release tag before running `make setup`. The `master` branch may contain unreleased changes that are not yet stable. To see all available releases, run `git tag --sort=-version:refname` or check the [releases page](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases).
|
||||
|
||||
### Step 2: Ensure Python 3.12+ is available
|
||||
|
||||
If you use pyenv:
|
||||
|
||||
```bash
|
||||
pyenv install 3.12
|
||||
pyenv local 3.12
|
||||
```
|
||||
|
||||
Verify your Python version:
|
||||
|
||||
```bash
|
||||
python3 --version # Must be 3.12 or higher
|
||||
```
|
||||
|
||||
### Step 3: Run make setup
|
||||
|
||||
```bash
|
||||
make setup
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
The `make setup` target performs the following:
|
||||
|
||||
1. Verifies Python 3.12+ is installed
|
||||
2. Creates a virtualenv in `.venv`
|
||||
3. Installs/updates `pip`, `setuptools`, and `wheel`
|
||||
4. Creates `.env` from `.env.example` if not present
|
||||
5. Generates shell activation scripts (`activate.sh`, `activate.fish`, `activate.zsh`)
|
||||
6. Installs the `devx` package from the Oblachno PyPI registry (provides CI/CD tools)
|
||||
7. Installs `checkmake` via `devx.tools.install_checkmake` (Makefile linter)
|
||||
8. Installs CI/CD tools via `devx.tools.install_tools` (actionlint, git-cliff, act_runner, tea) to `~/.local/bin`
|
||||
9. Runs `python -m devx.tools.setup` to install Python dependencies, Ansible Galaxy collections, pre-commit hooks, and configure tea CLI login
|
||||
|
||||
### Step 4: Configure Gitea credentials
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# GITEA_URL=https://git.example.com
|
||||
# GITEA_REGISTRATION_TOKEN=your-registration-token
|
||||
```
|
||||
|
||||
`GITEA_REGISTRATION_TOKEN` is the runner registration token obtained from your Gitea instance (Admin → Actions → Runners → Create Registration Token).
|
||||
|
||||
#### Admin API token (optional)
|
||||
|
||||
Set `CI_GITEA_TOKEN` to enable informational API checks during integration test. This is **optional** — the test primarily verifies the runner by checking:
|
||||
|
||||
1. **`.runner` registration file** exists and contains valid JSON (proves successful registration)
|
||||
2. **Systemd user service** is active (proves daemon is polling for jobs)
|
||||
|
||||
API checks, if enabled, are purely informational and do not affect pass/fail.
|
||||
|
||||
### Step 5: Verify the setup
|
||||
|
||||
```bash
|
||||
grm --version # Should print the version
|
||||
make lint-all # Should pass with no errors
|
||||
make pytest-cov # Should pass with 100% coverage
|
||||
```
|
||||
|
||||
## Shell activation scripts
|
||||
|
||||
`make setup` generates convenience activation scripts for different shells:
|
||||
|
||||
```bash
|
||||
source activate.sh # bash
|
||||
source activate.fish # fish
|
||||
source activate.zsh # zsh
|
||||
```
|
||||
|
||||
These scripts activate the `.venv` virtualenv from the project root.
|
||||
|
||||
## Running Linters
|
||||
|
||||
```bash
|
||||
make lint # Python (ruff + format check + pyright + bandit)
|
||||
make lint-bandit # Security scan only
|
||||
make ansible-lint # Ansible
|
||||
make makefile-lint # Makefile
|
||||
make workflow-lint # Gitea Actions workflows (actionlint)
|
||||
```
|
||||
|
||||
The full lint target (`make lint-all`) runs all of the above:
|
||||
|
||||
```bash
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
```
|
||||
|
||||
Individual lint targets from the `Makefile`:
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `lint-ruff` | `ruff check src/ tests/` |
|
||||
| `lint-format` | `ruff format --check src/ tests/` |
|
||||
| `typecheck` | `pyright` |
|
||||
| `lint-bandit` | `bandit -r src/` |
|
||||
| `lint-deps` | `pip-audit` — checks dependencies for known vulnerabilities |
|
||||
| `ansible-lint` | `ansible-lint ansible/` |
|
||||
| `makefile-lint` | `checkmake Makefile` |
|
||||
| `lint` | ruff + format check + pyright + bandit |
|
||||
| `lint-all` | lint + ansible-lint + makefile-lint + workflow-lint |
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Unit tests
|
||||
|
||||
```bash
|
||||
make test-unit # Without coverage
|
||||
make pytest-cov # With 100% coverage enforcement
|
||||
```
|
||||
|
||||
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/gitea_runner_manager/`.
|
||||
|
||||
### Integration tests
|
||||
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
Tests the full CLI lifecycle commands end-to-end (mocked executor boundary).
|
||||
|
||||
### Molecule tests
|
||||
|
||||
```bash
|
||||
make molecule # Quick: all 6 scenarios on Ubuntu 22.04
|
||||
make molecule-all # Full: all 6 scenarios on all 4 supported OSes
|
||||
```
|
||||
|
||||
Requires Docker to be installed and running on your machine. Molecule creates Docker containers as test hosts, applies the Ansible role, and verifies the results.
|
||||
|
||||
### Full test suite
|
||||
|
||||
```bash
|
||||
make test-all # pytest-cov + molecule
|
||||
```
|
||||
|
||||
## Workflow verification
|
||||
|
||||
GRM includes Gitea Actions workflow files in `.gitea/workflows/`. These are verified with two tools:
|
||||
|
||||
```bash
|
||||
make workflow-lint # Static lint via actionlint
|
||||
make workflow-dryrun # Dry-run via act_runner exec --dryrun
|
||||
make workflow-check # Both of the above
|
||||
```
|
||||
|
||||
The pre-commit hook runs actionlint automatically when workflow files change.
|
||||
|
||||
## Pre-commit hooks
|
||||
|
||||
`make setup` installs pre-commit hooks that run:
|
||||
|
||||
- **pre-commit**: `ruff check`, `ruff format --check`, conventional commit message validation
|
||||
- **pre-push**: `make pytest-cov` (ensures tests pass before pushing)
|
||||
|
||||
## Make targets reference
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `make setup` | Full setup: venv, deps, hooks, CI tools |
|
||||
| `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-molecule` | Full setup for molecule testing |
|
||||
| `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-devx` | Install the devx package from the Oblachno PyPI registry |
|
||||
| `make lint-all` | ruff + pyright + bandit + ansible-lint + checkmake + actionlint |
|
||||
| `make pytest-cov` | Unit tests with 100% coverage enforcement |
|
||||
| `make test-unit` | Unit tests without coverage |
|
||||
| `make test-integration` | Integration tests |
|
||||
| `make molecule` | All 6 Molecule scenarios on Ubuntu 22.04 |
|
||||
| `make molecule-all` | All 6 scenarios on all 4 supported OSes |
|
||||
| `make test-all` | pytest-cov + molecule |
|
||||
| `make workflow-lint` | Static lint of workflow YAML (actionlint) |
|
||||
| `make workflow-dryrun` | Dry-run all workflows in Docker |
|
||||
| `make workflow-check` | workflow-lint + workflow-dryrun |
|
||||
| `make clean` | Remove `__pycache__`, `.pyc`, `.coverage`, `htmlcov/`, `.molecule/` |
|
||||
@@ -0,0 +1,128 @@
|
||||
# Testing Strategy
|
||||
|
||||
GRM employs a multi-layered testing strategy: unit tests with 100% coverage enforcement, integration tests for the CLI lifecycle, and Molecule scenarios for Ansible role validation across multiple OS platforms.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
```bash
|
||||
make test-unit # Without coverage
|
||||
make pytest-cov # With 100% coverage enforcement
|
||||
```
|
||||
|
||||
Runs pytest with 100% coverage requirement.
|
||||
|
||||
From the `Makefile`:
|
||||
|
||||
- `test-unit` — `pytest tests/unit/ -v --no-cov` (unit tests without coverage)
|
||||
- `pytest-cov` — `pytest tests/ -v --cov=src/gitea_runner_manager --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/gitea_runner_manager/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release.
|
||||
|
||||
### 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.
|
||||
|
||||
## Integration Tests
|
||||
|
||||
```bash
|
||||
make test-integration
|
||||
```
|
||||
|
||||
Tests the full CLI lifecycle commands end-to-end with a mocked executor boundary. This verifies that the CLI correctly parses arguments, resolves runners from the registry, constructs the right Ansible commands, and handles errors — all without actually connecting to remote hosts.
|
||||
|
||||
From the `Makefile`:
|
||||
|
||||
- `test-integration` — `pytest tests/integration/ -v --no-cov`
|
||||
|
||||
Integration tests are marked with `@pytest.mark.integration` and are not counted toward coverage.
|
||||
|
||||
## Molecule Tests
|
||||
|
||||
```bash
|
||||
make molecule # Quick: all 6 scenarios on Ubuntu 22.04
|
||||
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.
|
||||
|
||||
### Scenarios
|
||||
|
||||
Seven Molecule scenarios are defined under `ansible/roles/gitea-runner/molecule/`:
|
||||
|
||||
| Scenario | Description | What it verifies |
|
||||
|----------|-------------|------------------|
|
||||
| `default` | Rootless Docker runner installation | Basic role convergence — user creation, directory structure, binary download, config file, systemd service template, prune timer templates |
|
||||
| `multi-instance` | Two isolated runner instances on the same host | Two separate converge plays with different runner names; verifies both instances coexist with independent users, data directories, and service files |
|
||||
| `lifecycle` | Stop, disable, re-enable, and start sequence | Converge, then side_effect stops and disables the service, then re-enables and starts it; verify confirms the service is active again |
|
||||
| `template-content` | Verify rendered systemd and prune templates | Checks that the systemd user service file contains expected directives (`Type=simple`, `ExecStart`, `Restart=on-failure`, `DOCKER_HOST`, `XDG_RUNTIME_DIR`), and that the prune service and timer templates are correctly rendered |
|
||||
| `deregister` | Runner deregistration | Creates a fake `.runner` file, then runs the deregister tasks; verifies the `.runner` file is removed |
|
||||
| `update` | Runner binary update | Converge, then side_effect runs the update playbook; verifies the binary is updated |
|
||||
| `remove` | Runner removal | Converge, then side_effect runs the remove playbook; verifies the user, directories, and service files are cleaned up |
|
||||
|
||||
All scenarios test idempotence (second run produces zero changes), which is a core requirement of the Ansible role.
|
||||
|
||||
### Common scenario configuration
|
||||
|
||||
All scenarios use `docker_rootless_setup: false` and `skip_runner_registration: true` in their converge playbooks. This is because:
|
||||
|
||||
- **Rootless Docker** requires kernel user namespace support, which is not available in all Docker-in-Docker CI environments. The role handles this gracefully via the `docker_rootless_setup` guard.
|
||||
- **Runner registration** requires a real Gitea instance. The role handles this via the `skip_runner_registration` flag, which skips the `register.yml` and `integration_test.yml` tasks.
|
||||
|
||||
### Platforms
|
||||
|
||||
4 platforms are tested:
|
||||
|
||||
| Platform | Docker image |
|
||||
|----------|-------------|
|
||||
| `ubuntu-2204` | `geerlingguy/docker-ubuntu2204-ansible` |
|
||||
| `ubuntu-2404` | `geerlingguy/docker-ubuntu2404-ansible` |
|
||||
| `debian-12` | `geerlingguy/docker-debian12-ansible` |
|
||||
| `archlinux` | `archlinux:latest` |
|
||||
|
||||
The platform list is defined in `devx.molecule.platforms` (single source of truth), shared between `devx.molecule.distribute_molecule` (CI) and `devx.molecule.molecule_all` (local dev tool).
|
||||
|
||||
### CI Test Distribution
|
||||
|
||||
CI runs all 6 scenarios x 4 platforms (24 test pairs) distributed across available Gitea Actions runners.
|
||||
|
||||
The `discover-runners` job runs `devx.molecule.discover_runners` which queries the Gitea API for registered runners at three levels (repo, org, instance) and generates a dynamic matrix. If the API query fails (e.g., no admin access for instance-level runners), it falls back to the `MOLECULE_RUNNERS` repo variable, then to a default of 3.
|
||||
|
||||
The `molecule-tests` job uses `fromJSON()` to consume the dynamic matrix, and passes the runner count to `python -m devx.molecule.distribute_molecule --max-runners` so test pairs are evenly distributed.
|
||||
|
||||
`devx.molecule.distribute_molecule` discovers all molecule scenarios under `ansible/roles/*/molecule/` and crosses them with the supported OS platform matrix, then splits the resulting test pairs evenly across the requested number of runners. Each pair is encoded as `scenario|platform_name|platform_image|platform_command`.
|
||||
|
||||
`devx.molecule.molecule_ci_guard` runs the actual molecule test for a given test pair, with CI context (Gitea URL, token, run ID) for reporting results back to the commit status API.
|
||||
|
||||
### Path-based CI filtering
|
||||
|
||||
The CI workflow 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.
|
||||
|
||||
## Full Test Suite
|
||||
|
||||
```bash
|
||||
make test-all # Runs pytest-cov + molecule (Ubuntu 22.04)
|
||||
```
|
||||
|
||||
From the `Makefile`:
|
||||
|
||||
- `test-all` — `pytest-cov + molecule` (unit tests with coverage + all 6 molecule scenarios on Ubuntu 22.04)
|
||||
|
||||
For a complete test across all platforms, use `make molecule-all` separately.
|
||||
|
||||
## Build & Test Commands Summary
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps, set up hooks, install CI tools
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||
make test-unit # Unit tests without coverage
|
||||
make test-integration # Integration tests
|
||||
make molecule # All 6 scenarios on Ubuntu 22.04
|
||||
make molecule-all # All 6 scenarios on all 4 supported OSes
|
||||
make test-all # pytest-cov + molecule
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user` calls — this is expected (systemd module doesn't support user services) and skipped in `.ansible-lint`
|
||||
- Molecule Docker driver may print "Event loop is closed" warnings on interrupt — harmless
|
||||
@@ -0,0 +1,303 @@
|
||||
# CLI Commands
|
||||
|
||||
GRM provides the following CLI commands for managing Gitea Actions runners. The base command is `grm`.
|
||||
|
||||
## Command Summary
|
||||
|
||||
| Command | Arguments | Description |
|
||||
|---------|-----------|-------------|
|
||||
| `grm install` | `<host>` | Install and configure a runner on a remote host |
|
||||
| `grm update` | `<host>` | Update the gitea_runner binary on a remote host |
|
||||
| `grm start` | `<runner_name>` | Start a registered runner |
|
||||
| `grm stop` | `<runner_name>` | Stop a registered runner |
|
||||
| `grm enable` | `<runner_name>` | Enable a runner to start on boot |
|
||||
| `grm disable` | `<runner_name>` | Disable and deregister a runner |
|
||||
| `grm status` | `<runner_name>` | Check the status of a registered runner |
|
||||
| `grm remove` | `<runner_name>` | Remove a runner completely |
|
||||
| `grm list` | — | List all registered runners with live status |
|
||||
| `grm --version` | — | Show the installed version |
|
||||
|
||||
### Common lifecycle options
|
||||
|
||||
The `start`, `stop`, `enable`, `status`, `disable`, and `remove` commands all accept these override options. By default, connection details are read from the local registry (`~/.local/share/grm/runners.json`).
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## install
|
||||
|
||||
Install and configure a Gitea Runner on a remote host.
|
||||
|
||||
```bash
|
||||
grm install <host> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `host` | Remote host (IP address or hostname) |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--user` | `-u` | `GITEA_RUNNER_USER` env or current login | SSH user |
|
||||
| `--key` | `-k` | `GITEA_RUNNER_KEY` env | Path to SSH private key |
|
||||
| `--name` | `-n` | hostname | Gitea Runner name |
|
||||
| `--token` | `-t` | `GITEA_REGISTRATION_TOKEN` env | Registration token |
|
||||
| `--url` | — | `GITEA_URL` env | Gitea URL |
|
||||
| `--admin-token` | `-a` | `CI_GITEA_TOKEN` env | Gitea admin API token for integration test |
|
||||
| `--integration-retries` | `-r` | `3` (`GITEA_INTEGRATION_RETRIES` env) | Integration test API retries |
|
||||
| `--labels` | `-l` | `GITEA_RUNNER_LABELS` env | Runner labels for Gitea Actions. Example: `docker:docker://alpine:latest` |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | `--ask-become-pass` | Prompt for sudo password (default) or skip it |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
```
|
||||
|
||||
## update
|
||||
|
||||
Update the Gitea Runner binary on a remote host.
|
||||
|
||||
```bash
|
||||
grm update <host> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `host` | Remote host (IP address or hostname) |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--user` | `-u` | `GITEA_RUNNER_USER` env or current login | SSH user |
|
||||
| `--key` | `-k` | `GITEA_RUNNER_KEY` env | Path to SSH private key |
|
||||
| `--version` | `-v` | — | Specific Gitea Runner version |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | `--ask-become-pass` | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## start
|
||||
|
||||
Start a registered Gitea Runner.
|
||||
|
||||
```bash
|
||||
grm start <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm start prod-runner
|
||||
# Override stored values:
|
||||
grm start prod-runner --host 192.168.1.11 --user root
|
||||
```
|
||||
|
||||
## stop
|
||||
|
||||
Stop a registered Gitea Runner.
|
||||
|
||||
```bash
|
||||
grm stop <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## enable
|
||||
|
||||
Enable a registered Gitea Runner to start on boot.
|
||||
|
||||
```bash
|
||||
grm enable <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## disable
|
||||
|
||||
Disable a registered Gitea Runner and deregister it.
|
||||
|
||||
```bash
|
||||
grm disable <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--host` | — | from registry | Override host from registry |
|
||||
| `--user` | `-u` | from registry | Override user from registry |
|
||||
| `--key` | `-k` | from registry | Override SSH key from registry |
|
||||
| `--token` | `-t` | `GITEA_REGISTRATION_TOKEN` env | Registration token |
|
||||
| `--url` | — | `GITEA_URL` env | Gitea URL |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | `--ask-become-pass` | Prompt for sudo password (default) or skip it |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm disable prod-runner --token <token>
|
||||
```
|
||||
|
||||
## status
|
||||
|
||||
Check the status of a registered Gitea Runner.
|
||||
|
||||
```bash
|
||||
grm status <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## remove
|
||||
|
||||
Remove a registered Gitea Runner completely.
|
||||
|
||||
```bash
|
||||
grm remove <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Short | Default | Description |
|
||||
|--------|-------|---------|-------------|
|
||||
| `--host` | — | from registry | Override host from registry |
|
||||
| `--user` | `-u` | from registry | Override user from registry |
|
||||
| `--key` | `-k` | from registry | Override SSH key from registry |
|
||||
| `--token` | `-t` | `GITEA_REGISTRATION_TOKEN` env | Registration token |
|
||||
| `--url` | — | `GITEA_URL` env | Gitea URL |
|
||||
| `--force` | `-f` | — | Skip remote cleanup and only remove the local registry entry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | `--ask-become-pass` | Prompt for sudo password (default) or skip it |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --token <token>
|
||||
```
|
||||
|
||||
## list
|
||||
|
||||
List all registered runners with live status.
|
||||
|
||||
```bash
|
||||
grm list
|
||||
```
|
||||
|
||||
This command takes no arguments or options. It displays a table with columns: NAME, HOST, USER, LABELS, STATUS for all runners stored in the local registry at `~/.local/share/grm/runners.json`.
|
||||
|
||||
The status is checked live by running an Ansible ad-hoc command on each remote host (`systemctl --user is-active gitea-runner`). Possible status values: `active`, `inactive`, `failed`, `unknown`.
|
||||
|
||||
**Example output:**
|
||||
|
||||
```
|
||||
NAME HOST USER LABELS STATUS
|
||||
------------------------------------------------------------------------------------------
|
||||
prod-runner 192.168.1.10 ubuntu docker:docker://gitea/... active
|
||||
build-runner 192.168.1.10 ubuntu docker:docker://gitea/... active
|
||||
test-runner 192.168.1.20 ubuntu inactive
|
||||
```
|
||||
|
||||
If no runners are registered:
|
||||
|
||||
```
|
||||
No runners registered. Use 'grm install' to add one.
|
||||
```
|
||||
|
||||
## --version
|
||||
|
||||
Show the installed GRM version.
|
||||
|
||||
```bash
|
||||
grm --version
|
||||
```
|
||||
|
||||
This reports the version from `__version__` in `src/gitea_runner_manager/__init__.py`, which is the single source of truth set by the automated release pipeline.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All CLI options can be set via environment variables (loaded from `.env` via python-dotenv). Command-line flags take precedence over environment variables.
|
||||
|
||||
| Variable | Used by | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GITEA_URL` | `install`, `disable`, `remove` | Gitea instance URL |
|
||||
| `GITEA_REGISTRATION_TOKEN` | `install`, `disable`, `remove` | Runner registration token |
|
||||
| `CI_GITEA_TOKEN` | `install` | Admin API token for integration test |
|
||||
| `GITEA_INTEGRATION_RETRIES` | `install` | API check retries (default: 3) |
|
||||
| `GITEA_RUNNER_USER` | `install`, `update` | Default SSH user |
|
||||
| `GITEA_RUNNER_KEY` | `install`, `update` | Default SSH key path |
|
||||
| `GITEA_RUNNER_LABELS` | `install` | Default runner labels |
|
||||
| `GRM_LANG` | all | UI language: `en`, `bg`, `de`, `ru`, `zh` |
|
||||
| `GRM_LOG_LEVEL` | all | Console log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||
@@ -0,0 +1,173 @@
|
||||
# FAQ
|
||||
|
||||
### How do I obtain the Gitea registration token?
|
||||
|
||||
There are three levels of registration tokens, depending on which repositories the runner should serve:
|
||||
|
||||
- **Instance-level** — Site Administration → Actions → Runners → Create Registration Token. The runner will handle jobs from all repositories.
|
||||
- **Organization-level** — Organization → Settings → Actions → Runners → Create Registration Token. The runner will only handle jobs from repositories in that organization.
|
||||
- **Repository-level** — Repository → Settings → Actions → Runners → Create Registration Token. The runner will only handle jobs from that specific repository.
|
||||
|
||||
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?
|
||||
|
||||
`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`).
|
||||
|
||||
If you skip it, GRM will still verify the runner correctly — it just won't show the extra API confirmation.
|
||||
|
||||
### How do I 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.
|
||||
|
||||
On the remote host, add a sudoers entry:
|
||||
|
||||
```bash
|
||||
echo "ubuntu ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/grm
|
||||
```
|
||||
|
||||
Then use:
|
||||
|
||||
```bash
|
||||
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?
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --name workflow-runner
|
||||
grm install 192.168.1.10 --user ubuntu --name build-runner
|
||||
grm list
|
||||
```
|
||||
|
||||
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?
|
||||
|
||||
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**.
|
||||
|
||||
Common causes:
|
||||
- Registration token expired — generate a new one from Gitea
|
||||
- Network connectivity issue between the runner host and Gitea
|
||||
- Rootless Docker daemon not running — check `sudo -u grm-<name> systemctl --user status docker`
|
||||
- Lingering not enabled — check `loginctl show-user grm-<name> | grep Linger`
|
||||
|
||||
### What does the "Event loop is closed" warning mean?
|
||||
|
||||
This is a harmless cleanup traceback from Molecule's Docker driver when the test process is interrupted. It does not indicate a test failure.
|
||||
|
||||
### Where are runner connection details stored?
|
||||
|
||||
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?
|
||||
|
||||
Use the `grm update` command:
|
||||
|
||||
```bash
|
||||
grm update 192.168.1.10 --user ubuntu
|
||||
```
|
||||
|
||||
To update to a specific version:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### How do I completely remove a runner?
|
||||
|
||||
Use the `grm remove` command:
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --token <registration-token>
|
||||
```
|
||||
|
||||
This deregisters the runner from Gitea, stops and disables the systemd service, removes the system user, deletes data and config directories, removes subuid/subgid entries, disables lingering, and removes the entry from the local registry.
|
||||
|
||||
If the remote host is already gone or unreachable, use `--force` to skip remote cleanup and only remove the local registry entry:
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --force
|
||||
```
|
||||
|
||||
### 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 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### How do I 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).
|
||||
|
||||
```bash
|
||||
GRM_LANG=bg grm install 192.168.1.10 --user ubuntu --name prod-runner
|
||||
```
|
||||
|
||||
Or set it in your `.env` file:
|
||||
|
||||
```bash
|
||||
GRM_LANG=bg
|
||||
```
|
||||
|
||||
### How do I enable debug logging?
|
||||
|
||||
Set the `GRM_LOG_LEVEL` environment variable to `DEBUG`:
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
### What runner labels should I use?
|
||||
|
||||
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.
|
||||
|
||||
Use an official Gitea runner image with Node.js, Python, and Docker CLI. Avoid bare OS images like `alpine:latest` because `actions/checkout@v4` needs Node.js.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --name prod-runner \
|
||||
--labels "docker:docker://gitea/runner-images:ubuntu-latest"
|
||||
```
|
||||
|
||||
### Is GRM secure?
|
||||
|
||||
Yes. GRM is designed with security as a first-class concern:
|
||||
|
||||
- **Rootless Docker**: Each runner operates under a dedicated unprivileged system user. Containers never have root access to the host.
|
||||
- **Secret handling**: Registration tokens are passed via temporary JSON files with `0600` permissions, never on the command line (CWE-214).
|
||||
- **No shell injection**: The CLI never uses `shell=True` with subprocess.
|
||||
- **Bandit security scan**: The CI pipeline runs Bandit on every PR.
|
||||
|
||||
### Can I install GRM via pip?
|
||||
|
||||
Yes:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks and role are bundled with the package. For development or access to Make targets, clone the repository instead.
|
||||
|
||||
### How does GRM handle idempotence?
|
||||
|
||||
The Ansible role is idempotent — running `grm install` twice produces zero changes on the second run. Each task checks for existing state before making changes. For example:
|
||||
|
||||
- User creation uses `ansible.builtin.user` which only creates if the user doesn't exist
|
||||
- Package installation uses `state: present` which only installs if not already installed
|
||||
- Template creation uses `ansible.builtin.template` which only writes if the content changed
|
||||
- Rootless Docker setup uses `creates:` to skip if already configured
|
||||
|
||||
This makes GRM safe for CI/CD pipelines and configuration management.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Getting Started
|
||||
|
||||
This guide walks you through setting up GRM, configuring Gitea credentials, and installing your first runner.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
|
||||
- **Python 3.12+** on your local machine
|
||||
- **SSH access** to the target host(s) where runners will be installed
|
||||
- **Sudo privileges** on the target host(s) for the SSH user
|
||||
- **A Gitea instance** with admin access to create registration tokens
|
||||
- **Git** for cloning the repository
|
||||
|
||||
## Step 1: Clone and Setup
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Checkout latest stable release
|
||||
make setup
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
> **Important:** Always checkout the latest release tag before running `make setup`. The `master` branch may contain unreleased changes that are not yet stable. The `git describe --tags --abbrev=0` command automatically selects the most recent tagged release. To see all available releases, run `git tag --sort=-version:refname` or check the [releases page](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases).
|
||||
|
||||
`make setup` creates a virtualenv, installs all dependencies (including Ansible), creates `.env` from `.env.example`, and sets up pre-commit hooks.
|
||||
|
||||
If you use pyenv for Python version management:
|
||||
|
||||
```bash
|
||||
pyenv install 3.12
|
||||
pyenv local 3.12
|
||||
make setup
|
||||
```
|
||||
|
||||
## Step 2: Configure Gitea Credentials
|
||||
|
||||
GRM needs two tokens from your Gitea instance: a **registration token** (required) and an **admin API token** (optional, for post-install verification).
|
||||
|
||||
### Get the Registration Token
|
||||
|
||||
The registration token tells Gitea to accept the runner when it connects.
|
||||
|
||||
1. Log in to your Gitea instance as an administrator
|
||||
2. Navigate to **Site Administration → Actions → Runners**
|
||||
3. Click **Create Registration Token**
|
||||
4. Copy the token — it starts with `GR`
|
||||
|
||||
> **Note:** There are three levels of registration tokens:
|
||||
> - **Instance-level** (Site Administration → Actions → Runners) — registers a runner for all repositories
|
||||
> - **Organization-level** (Organization → Settings → Actions → Runners) — registers a runner for repos in that organization
|
||||
> - **Repository-level** (Repository → Settings → Actions → Runners) — registers a runner for a single repository
|
||||
>
|
||||
> Use instance-level tokens for shared runners, and repo-level tokens for dedicated runners.
|
||||
|
||||
### Get the Admin API Token (optional)
|
||||
|
||||
The admin API token enables post-install API checks that verify the runner appears in Gitea's runner list. This is purely informational — the integration test primarily verifies the runner by checking:
|
||||
|
||||
1. **`.runner` registration file** exists and contains valid JSON (proves successful registration)
|
||||
2. **Systemd user service** is active (proves daemon is polling for jobs)
|
||||
|
||||
To get an admin API token:
|
||||
|
||||
1. Go to **Settings → Applications → Generate New Token**
|
||||
2. Give it a name (e.g., "GRM Install Verification")
|
||||
3. Select the **admin** scope (or at minimum: `read:user`, `read:repository`, `read:admin`)
|
||||
4. Click **Generate Token** and copy it immediately (it won't be shown again)
|
||||
|
||||
### Create the `.env` File
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your tokens:
|
||||
|
||||
```bash
|
||||
# Your Gitea instance URL
|
||||
GITEA_URL=https://git.example.com
|
||||
|
||||
# Registration token from Step 1
|
||||
GITEA_REGISTRATION_TOKEN=GRxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Admin API token from Step 2 (optional)
|
||||
CI_GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `GITEA_URL` | Yes | — | Gitea instance URL (e.g., `https://git.example.com`) |
|
||||
| `GITEA_REGISTRATION_TOKEN` | Yes | — | Runner registration token from Gitea admin panel |
|
||||
| `CI_GITEA_TOKEN` | No | — | Admin API token for post-install verification |
|
||||
| `GITEA_INTEGRATION_RETRIES` | No | `3` | API check retries (default: 3) |
|
||||
| `GITEA_RUNNER_USER` | No | current login | Default SSH user (overrides `--user`) |
|
||||
| `GITEA_RUNNER_KEY` | No | — | Default SSH key path (overrides `--key`) |
|
||||
| `GITEA_RUNNER_LABELS` | No | — | Default runner labels (overrides `--labels`) |
|
||||
| `GRM_LANG` | No | `en` | UI language: `en`, `bg`, `de`, `ru`, `zh` |
|
||||
| `GRM_LOG_LEVEL` | No | `INFO` | Console log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||
|
||||
## Step 3: Install Your First Runner
|
||||
|
||||
Using the CLI (you will be prompted for the sudo password by default):
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Connects to `192.168.1.10` via SSH as user `ubuntu` using the specified key
|
||||
2. Creates a dedicated system user `grm-prod-runner` with lingering enabled
|
||||
3. Installs Docker in rootless mode under the `grm-prod-runner` user
|
||||
4. Downloads and installs the gitea_runner binary
|
||||
5. Creates the runner configuration file at `/etc/gitea-runner/prod-runner/config.yaml`
|
||||
6. Registers the runner with your Gitea instance
|
||||
7. Creates and starts a systemd user service (`gitea-runner.service`)
|
||||
8. Sets up a Docker prune timer (daily cleanup)
|
||||
9. Runs an integration test to verify the installation
|
||||
10. Saves the runner to the local registry at `~/.local/share/grm/runners.json`
|
||||
|
||||
> **Automation tip:** Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to skip the password prompt. This is recommended for CI/CD pipelines.
|
||||
|
||||
Using Make:
|
||||
|
||||
```bash
|
||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
||||
```
|
||||
|
||||
### Runner labels
|
||||
|
||||
By default, runners are registered with the label `docker,ubuntu-latest:docker://runner-images:ubuntu-22.04`. You can override this with `--labels`:
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --name prod-runner \
|
||||
--labels "docker:docker://gitea/runner-images:ubuntu-latest"
|
||||
```
|
||||
|
||||
> **Note:** Use an official Gitea runner image with Node.js, Python, and Docker CLI. Avoid bare OS images like `alpine:latest` because `actions/checkout@v4` needs Node.js.
|
||||
|
||||
## Step 4: Verify the Installation
|
||||
|
||||
The installer performs an automated integration test that verifies:
|
||||
|
||||
1. **`.runner` file exists** with valid JSON containing `id`, `uuid`, `token`, `address` — this proves successful registration with Gitea
|
||||
2. **Systemd user service is active** — this proves the daemon is polling for jobs
|
||||
|
||||
You can also check the Gitea UI under **Actions → Runners** to confirm the runner appears as **Online**.
|
||||
|
||||
Optional: If `CI_GITEA_TOKEN` is set, the installer will also query the Gitea API and report whether the runner appears in the admin or repo runners list. This is purely informational.
|
||||
|
||||
### Check runner status via CLI
|
||||
|
||||
```bash
|
||||
grm status prod-runner
|
||||
```
|
||||
|
||||
This connects to the remote host and checks the systemd user service status.
|
||||
|
||||
### List all runners
|
||||
|
||||
```bash
|
||||
grm list
|
||||
```
|
||||
|
||||
This displays a table with columns: NAME, HOST, USER, LABELS, STATUS for all runners in the local registry. The status is checked live via an Ansible ad-hoc command.
|
||||
|
||||
## Step 5: Manage the Runner Lifecycle
|
||||
|
||||
Once installed, you can manage the runner by name (connection details are stored in the local registry):
|
||||
|
||||
```bash
|
||||
grm stop prod-runner # Stop the runner service
|
||||
grm start prod-runner # Start the runner service
|
||||
grm enable prod-runner # Enable the runner to start on boot
|
||||
grm status prod-runner # Check the runner status
|
||||
grm update 192.168.1.10 --user ubuntu # Update the runner binary
|
||||
grm disable prod-runner # Disable and deregister the runner
|
||||
grm remove prod-runner # Remove the runner completely
|
||||
```
|
||||
|
||||
See [CLI Commands](CLI-Commands.-) for the full command reference.
|
||||
|
||||
## View Logs
|
||||
|
||||
### GRM application logs (on your local machine)
|
||||
|
||||
```bash
|
||||
# Application log file (all messages including DEBUG)
|
||||
cat ~/.local/state/grm/logs/grm.log
|
||||
|
||||
# Enable debug logging in the current session
|
||||
GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
|
||||
```
|
||||
|
||||
### Ansible execution logs
|
||||
|
||||
Each `grm` command that invokes Ansible creates a timestamped log file:
|
||||
|
||||
```bash
|
||||
ls ~/.local/state/grm/logs/ansible-*.log
|
||||
cat ~/.local/state/grm/logs/ansible-20260622-143012.log
|
||||
```
|
||||
|
||||
### Runner logs (on the remote host)
|
||||
|
||||
```bash
|
||||
# Runner logs (via systemd user service)
|
||||
sudo -u grm-prod-runner journalctl --user -u gitea-runner -f
|
||||
|
||||
# Rootless Docker daemon logs
|
||||
sudo -u grm-prod-runner journalctl --user -u docker -f
|
||||
```
|
||||
|
||||
### Logging destinations
|
||||
|
||||
The GRM application writes to three destinations:
|
||||
|
||||
| Destination | Level | Content |
|
||||
|-------------|-------|---------|
|
||||
| Console (stdout) | `GRM_LOG_LEVEL` (default: INFO) | Colorised user-facing messages and operation reports |
|
||||
| `~/.local/state/grm/logs/grm.log` | DEBUG | All messages with timestamps and severity |
|
||||
| `~/.local/state/grm/logs/ansible-<timestamp>.log` | — | Full Ansible playbook output per execution |
|
||||
|
||||
Set `GRM_LOG_LEVEL` to one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` to control console verbosity. The log file always captures everything at DEBUG level regardless of the console setting.
|
||||
|
||||
Console output is automatically colorised via `click.style`: operation headers in bright cyan, completed steps in green, failures in red, and status updates in yellow.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Install more runners** on the same or different hosts — see [Installation](Installation)
|
||||
- **Learn all CLI commands** — see [CLI Commands](CLI-Commands.-)
|
||||
- **Troubleshoot issues** — see [Troubleshooting](Troubleshooting)
|
||||
- **Understand the architecture** — see [Architecture](Architecture)
|
||||
@@ -0,0 +1,242 @@
|
||||
# Installation
|
||||
|
||||
> **Before you start:** Make sure you have cloned the repo and checked out the latest stable release tag. See [Getting Started](Getting-Started.-) for setup instructions. Do not run from `master` — it may contain unreleased changes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### On your local machine (where you run `grm`)
|
||||
|
||||
- **Python 3.12+** — GRM targets Python 3.12 and requires it for development setup. Use `pyenv` if you need to manage multiple Python versions.
|
||||
- **Ansible** — Installed automatically by `make setup` (via pip). GRM delegates all remote operations to `ansible-playbook`.
|
||||
- **SSH key** — A private key that grants access to the target host(s) as a user with sudo privileges.
|
||||
|
||||
### On the target host(s) (where runners will be installed)
|
||||
|
||||
- **SSH server** — The remote host must be reachable via SSH using the user specified with `--user` and the private key specified with `--key`. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail.
|
||||
- **Sudo access** — GRM requires root privileges on the remote host to create system users, install packages, and configure rootless Docker. By default, you will be prompted interactively for the sudo password. For automation or uninterrupted workflows, configure passwordless sudo on the remote host and pass `--no-ask-become-pass`.
|
||||
- **Gitea registration token** — You need a runner registration token from your Gitea instance. See [Getting Started](Getting-Started.-) for detailed instructions on obtaining tokens.
|
||||
- **systemd** — Required for user services and lingering. All supported OSes ship with systemd.
|
||||
- **Docker** — Installed automatically by the Ansible role (rootless mode). No pre-existing Docker installation is required.
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
| OS | Versions | Package manager |
|
||||
|----|----------|-----------------|
|
||||
| Arch Linux | rolling | pacman |
|
||||
| Ubuntu | 22.04, 24.04 | apt |
|
||||
| Debian | 12 | apt |
|
||||
|
||||
All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files. The platform matrix is defined in `devx.molecule.platforms` as the single source of truth.
|
||||
|
||||
## Installation Methods
|
||||
|
||||
### Method 1: From source (recommended for full control)
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/grm.git
|
||||
cd grm
|
||||
git checkout $(git describe --tags --abbrev=0) # Latest stable release
|
||||
make setup
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
`make setup` performs the following:
|
||||
|
||||
1. Verifies Python 3.12+ is installed
|
||||
2. Creates a virtualenv in `.venv`
|
||||
3. Installs all Python dependencies (including Ansible, Click, python-dotenv)
|
||||
4. Creates `.env` from `.env.example` if not present
|
||||
5. Installs development tools (actionlint, git-cliff, act_runner, checkmake)
|
||||
6. Sets up pre-commit hooks
|
||||
|
||||
### Method 2: Via pip
|
||||
|
||||
GRM is published to the Gitea PyPI registry at
|
||||
`https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple`.
|
||||
The registry is publicly readable — no authentication required to install.
|
||||
|
||||
**Quick install (one-off):**
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
**Persistent configuration (recommended):**
|
||||
|
||||
Add the registry to `~/.pip/pip.conf`:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
Then install normally:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks
|
||||
and role are bundled with the package, so `grm install` works out of the box.
|
||||
For development or access to Make targets, clone the repository (Method 1).
|
||||
|
||||
### Post-install configuration
|
||||
|
||||
After installation, create your `.env` file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your Gitea URL and registration token
|
||||
```
|
||||
|
||||
Required variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `GITEA_URL` | Your Gitea instance URL (e.g., `https://git.example.com`) |
|
||||
| `GITEA_REGISTRATION_TOKEN` | Runner registration token from Gitea (starts with `GR`) |
|
||||
|
||||
See [Getting Started](Getting-Started.-) for detailed token setup instructions.
|
||||
|
||||
## Quick Start Install
|
||||
|
||||
Using the CLI (you will be prompted for the sudo password by default):
|
||||
|
||||
```bash
|
||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
```
|
||||
|
||||
> **Automation tip:** Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to skip the password prompt. This is recommended for CI/CD pipelines.
|
||||
|
||||
## Make Install
|
||||
|
||||
Using Make:
|
||||
|
||||
```bash
|
||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
||||
```
|
||||
|
||||
The Make target wraps the `grm install` CLI command. All Make install variables are optional except `HOST`:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `HOST` | Remote host (IP address or hostname) — **required** |
|
||||
| `USER` | SSH user |
|
||||
| `KEY` | Path to SSH private key |
|
||||
| `NAME` | Gitea Runner name |
|
||||
| `TOKEN` | Registration token |
|
||||
| `ASK_BECOME_PASS` | Set to `1` to prompt for sudo password |
|
||||
|
||||
## What Gets Installed on the Target Host
|
||||
|
||||
When you run `grm install`, the Ansible role creates the following on the remote host:
|
||||
|
||||
| Resource | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| System user | `grm-<name>` | Dedicated system user with `/bin/bash` shell |
|
||||
| Home directory | `/home/grm-<name>/` | User home with `.config/systemd/user/` |
|
||||
| Data directory | `/var/lib/gitea-runner/<name>/` | Runner data including `.runner` registration file |
|
||||
| Config directory | `/etc/gitea-runner/<name>/` | Runner configuration file (`config.yaml`) |
|
||||
| Runner binary | `/usr/local/bin/gitea_runner` | The gitea_runner executable |
|
||||
| Docker socket | `/run/user/<UID>/docker.sock` | Rootless Docker socket |
|
||||
| Systemd service | `gitea-runner.service` | User service for the runner daemon |
|
||||
| Docker prune timer | `docker-prune.timer` | Daily Docker cleanup timer |
|
||||
| subuid/subgid | `/etc/subuid`, `/etc/subgid` | User namespace mapping (100000-165535) |
|
||||
| Lingering | `loginctl enable-linger` | Ensures services run without active login |
|
||||
|
||||
## Runner Registry
|
||||
|
||||
After installation, 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`. This means you rarely need to repeat connection arguments:
|
||||
|
||||
```bash
|
||||
# List all registered runners with live systemd status
|
||||
grm list
|
||||
|
||||
# Manage runners by name — connection details come from the registry
|
||||
grm status prod-runner
|
||||
grm stop prod-runner
|
||||
grm start prod-runner
|
||||
```
|
||||
|
||||
You can override any stored value by passing the corresponding flag (`--host`, `--user`, `--key`).
|
||||
|
||||
### Registry file format
|
||||
|
||||
```json
|
||||
{
|
||||
"prod-runner": {
|
||||
"host": "192.168.1.10",
|
||||
"user": "ubuntu",
|
||||
"key": "/home/user/.ssh/id_ed25519",
|
||||
"gitea_url": "https://git.example.com",
|
||||
"labels": "docker:docker://gitea/runner-images:ubuntu-latest",
|
||||
"created_at": "2026-06-22T14:30:12.000000+00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Instances on the Same Host
|
||||
|
||||
Each runner instance is fully isolated with its own system user, rootless Docker daemon, data directory, and systemd user service:
|
||||
|
||||
```bash
|
||||
# Install two runners on the same host
|
||||
grm install 192.168.1.10 --user ubuntu --name workflow-runner
|
||||
grm install 192.168.1.10 --user ubuntu --name build-runner
|
||||
|
||||
# Manage them independently by name
|
||||
grm stop workflow-runner
|
||||
grm status build-runner
|
||||
grm list
|
||||
```
|
||||
|
||||
Each instance gets:
|
||||
|
||||
- **Dedicated system user**: `grm-<name>` with its own home directory
|
||||
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
|
||||
- **Data directory**: `/var/lib/gitea-runner/<name>/`
|
||||
- **Config directory**: `/etc/gitea-runner/<name>/`
|
||||
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
||||
- **Docker prune timer**: Per-instance daily cleanup
|
||||
|
||||
Runners on the same host never interfere with each other or with the host's Docker installation.
|
||||
|
||||
## Updating Runners
|
||||
|
||||
To update the gitea_runner binary on a remote host:
|
||||
|
||||
```bash
|
||||
grm update 192.168.1.10 --user ubuntu
|
||||
```
|
||||
|
||||
To update to a specific version:
|
||||
|
||||
```bash
|
||||
grm update 192.168.1.10 --user ubuntu --version 1.0.8
|
||||
```
|
||||
|
||||
Using Make:
|
||||
|
||||
```bash
|
||||
make update HOST=192.168.1.10 USER=ubuntu VERSION=1.0.8
|
||||
```
|
||||
|
||||
## Removing Runners
|
||||
|
||||
To remove a runner completely (deregisters from Gitea, removes user, directories, and service files):
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --token <registration-token>
|
||||
```
|
||||
|
||||
To skip remote cleanup and only remove the local registry entry (useful when the remote host is already gone):
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --force
|
||||
```
|
||||
|
||||
Using Make:
|
||||
|
||||
```bash
|
||||
make remove NAME=prod-runner TOKEN=<registration-token>
|
||||
```
|
||||
@@ -0,0 +1,199 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Installation Issues
|
||||
|
||||
### Ansible connection fails (UNREACHABLE)
|
||||
|
||||
**Symptom:** Ansible reports `UNREACHABLE` when trying to connect to the target host.
|
||||
|
||||
**Causes and solutions:**
|
||||
|
||||
- **SSH key not found or wrong path** — Verify the key path with `--key`. The key must be readable by the user running `grm`.
|
||||
- **SSH user does not exist on the target** — Verify the `--user` argument. The user must exist on the remote host and have sudo privileges.
|
||||
- **Host is not reachable** — Verify the host IP/hostname with `ping` and `ssh -u <user> <host>`.
|
||||
- **SSH port is not 22** — GRM uses the default SSH port. If your host uses a different port, you may need to configure SSH config (`~/.ssh/config`) with the appropriate port.
|
||||
|
||||
### Sudo password prompt fails or is not displayed
|
||||
|
||||
**Symptom:** The sudo password prompt does not appear or the command hangs.
|
||||
|
||||
**Causes and solutions:**
|
||||
|
||||
- **Non-interactive session** — If running in a CI/CD pipeline or script without a TTY, the password prompt cannot be displayed. Configure passwordless sudo on the remote host and pass `--no-ask-become-pass`.
|
||||
- **Wrong sudo password** — Ensure you are entering the correct sudo password for the remote user.
|
||||
|
||||
### GITEA_URL must be set
|
||||
|
||||
**Symptom:** Error message `GITEA_URL must be set (or pass --url)`.
|
||||
|
||||
**Solution:** Set `GITEA_URL` in your `.env` file or pass it via `--url`:
|
||||
|
||||
```bash
|
||||
# In .env
|
||||
GITEA_URL=https://git.example.com
|
||||
|
||||
# Or on the command line
|
||||
grm install 192.168.1.10 --user ubuntu --url https://git.example.com
|
||||
```
|
||||
|
||||
### GITEA_REGISTRATION_TOKEN must be set
|
||||
|
||||
**Symptom:** Error message `GITEA_REGISTRATION_TOKEN must be set (or pass --token)`.
|
||||
|
||||
**Solution:** Set `GITEA_REGISTRATION_TOKEN` in your `.env` file or pass it via `--token`. The token must be a valid registration token from your Gitea instance (it starts with `GR`).
|
||||
|
||||
## Runner Issues
|
||||
|
||||
### Runner appears offline after installation
|
||||
|
||||
- Check that the `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` environment variables are correct.
|
||||
- Verify the registration token has not expired — generate a new one from Gitea if needed (Site Administration → Actions → Runners → Create Registration Token).
|
||||
- Verify the runner service is running: `sudo -u grm-<name> systemctl --user status gitea-runner`.
|
||||
- Check logs for registration errors: `sudo -u grm-<name> journalctl --user -u gitea-runner -f`.
|
||||
- Confirm the runner appears in the Gitea UI under **Actions → Runners**. If it shows as offline, the runner daemon may not be polling — check network connectivity between the runner host and Gitea.
|
||||
|
||||
### Runner service fails to start
|
||||
|
||||
- Check the service status: `sudo -u grm-<name> systemctl --user status gitea-runner`
|
||||
- Check logs: `sudo -u grm-<name> journalctl --user -u gitea-runner -f`
|
||||
- Verify the runner binary exists: `ls -la /usr/local/bin/gitea_runner`
|
||||
- Verify the config file exists: `ls -la /etc/gitea-runner/<name>/config.yaml`
|
||||
- Verify the `.runner` registration file exists: `ls -la /var/lib/gitea-runner/<name>/.runner`
|
||||
|
||||
### Rootless Docker: service fails to start
|
||||
|
||||
- Check the service status: `sudo -u grm-<name> systemctl --user status gitea-runner`.
|
||||
- Verify the rootless Docker daemon is running: `sudo -u grm-<name> systemctl --user status docker`.
|
||||
- Verify the Docker socket exists: `ls /run/user/$(id -u grm-<name>)/docker.sock`.
|
||||
- Check logs: `sudo -u grm-<name> journalctl --user -u gitea-runner -f`.
|
||||
- Ensure lingering is enabled for the runner user: `loginctl show-user grm-<name> | grep Linger`. If not enabled, run `sudo loginctl enable-linger grm-<name>`.
|
||||
- Verify subuid/subgid entries exist: `grep grm-<name> /etc/subuid /etc/subgid`. If missing, the rootless Docker setup will fail.
|
||||
|
||||
### Runner not found in registry
|
||||
|
||||
**Symptom:** Error message `Runner '<name>' not found in registry. Use 'grm install' first or provide --host and --user.`
|
||||
|
||||
**Solution:** The runner was not installed via `grm install`, or the registry file was deleted. Either:
|
||||
|
||||
1. Install the runner first: `grm install <host> --user <user> --name <name>`
|
||||
2. Or provide explicit connection details: `grm status <name> --host <host> --user <user>`
|
||||
|
||||
## Integration Test Issues
|
||||
|
||||
### Integration test fails
|
||||
|
||||
The test checks two things:
|
||||
|
||||
1. **`.runner` file missing or invalid** — Registration failed. Check:
|
||||
- `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` are correct
|
||||
- The registration token is valid and has not expired
|
||||
- Runner logs for registration errors: `sudo -u grm-<name> journalctl --user -u gitea-runner`
|
||||
- The `.runner` file should exist at `/var/lib/gitea-runner/<name>/.runner`
|
||||
- The `.runner` file should contain valid JSON with `id`, `uuid`, `token`, `address` fields
|
||||
|
||||
2. **Service not running** — Daemon failed to start. Check:
|
||||
- `sudo -u grm-<name> systemctl --user status gitea-runner`
|
||||
- Logs for connection errors: `sudo -u grm-<name> journalctl --user -u gitea-runner`
|
||||
- Verify the rootless Docker daemon is running (see above)
|
||||
|
||||
### API verification shows error status
|
||||
|
||||
If `CI_GITEA_TOKEN` is set, the integration test queries the Gitea API. If the API returns `401` or `403`, the token does not have sufficient permissions. This is **informational only** and does not affect pass/fail. The test passes as long as the `.runner` file exists and the systemd service is active.
|
||||
|
||||
## Logging and Diagnostics
|
||||
|
||||
### Enable debug logging
|
||||
|
||||
```bash
|
||||
GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
|
||||
```
|
||||
|
||||
This prints all debug messages to the console. The log file at `~/.local/state/grm/logs/grm.log` always captures DEBUG level regardless of this setting.
|
||||
|
||||
### View Ansible execution logs
|
||||
|
||||
Each `grm` command that invokes Ansible creates a timestamped log file:
|
||||
|
||||
```bash
|
||||
ls ~/.local/state/grm/logs/ansible-*.log
|
||||
cat ~/.local/state/grm/logs/ansible-<timestamp>.log
|
||||
```
|
||||
|
||||
These logs contain the full Ansible output, including task results, changed/failed counts, and any error messages.
|
||||
|
||||
### View runner logs on the remote host
|
||||
|
||||
```bash
|
||||
# Runner daemon logs
|
||||
sudo -u grm-<name> journalctl --user -u gitea-runner -f
|
||||
|
||||
# Rootless Docker daemon logs
|
||||
sudo -u grm-<name> journalctl --user -u docker -f
|
||||
|
||||
# Docker prune timer logs
|
||||
sudo -u grm-<name> journalctl --user -u docker-prune.service
|
||||
```
|
||||
|
||||
## Development Issues
|
||||
|
||||
### "Event loop is closed" warning
|
||||
|
||||
This is a harmless cleanup traceback from Molecule's Docker driver when the test process is interrupted. It does not indicate a test failure.
|
||||
|
||||
### Pre-commit rejects commit message
|
||||
|
||||
The pre-commit hook validates that commit messages follow conventional commit format (`feat:`, `fix:`, `docs:`, etc.). The `GRM-N:` prefix is not allowed on branch commits — use it only in PR titles.
|
||||
|
||||
**Correct:**
|
||||
```
|
||||
feat: add new runner label option
|
||||
```
|
||||
|
||||
**Incorrect:**
|
||||
```
|
||||
GRM-33: add new runner label option
|
||||
update README
|
||||
```
|
||||
|
||||
### `make pytest-cov` fails with coverage below 100%
|
||||
|
||||
Add tests for any new code paths. The coverage requirement is strict (`--cov-fail-under=100`). Run `make pytest-cov` locally to see which lines are not covered:
|
||||
|
||||
```bash
|
||||
make pytest-cov
|
||||
# The output shows "Missing" lines for each file
|
||||
```
|
||||
|
||||
### `make molecule` fails with Docker not available
|
||||
|
||||
Molecule requires Docker to be installed and running on your machine. Verify:
|
||||
|
||||
```bash
|
||||
docker info # Should print Docker server info
|
||||
```
|
||||
|
||||
If Docker is not installed, install it via your package manager or [Docker's official installation guide](https://docs.docker.com/get-docker/).
|
||||
|
||||
## Common Issues Reference Table
|
||||
|
||||
| Symptom | Likely Cause | Solution |
|
||||
|---------|-------------|----------|
|
||||
| Ansible UNREACHABLE | SSH connection failed | Verify `--user`, `--key`, and host reachability |
|
||||
| `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` |
|
||||
| Runner appears offline | Registration failed or service not running | Check GITEA_URL, token validity, and service status |
|
||||
| Rootless Docker fails to start | subuid/subgid missing or lingering disabled | Verify `/etc/subuid`, `/etc/subgid`, and `loginctl show-user` |
|
||||
| Runner not found in registry | Runner not installed or registry deleted | Run `grm install` or provide `--host` and `--user` |
|
||||
| Pre-commit rejects commit message | Missing conventional format or GRM-N prefix present | Use `feat: description` format without `GRM-N:` |
|
||||
| `make molecule` fails with `runner_name is undefined` | Verify playbook missing variable | Fixed in Phase 1.1; ensure you're on latest master |
|
||||
| CI molecule job fails | Docker not available on runner host | Ensure Gitea runner host has Docker installed and running |
|
||||
| Auto-merge doesn't trigger | Label not exactly `ready-to-merge` or CI checks not all green | Verify label spelling; check CI status |
|
||||
| Vikunja task not updated after merge | VIKUNJA_TOKEN expired or task ID missing from commit | Regenerate token; verify merge commit has `GRM-N:` prefix |
|
||||
| Post-merge can't find Vikunja task | Task not in project 6 or identifier mismatch | Verify task exists in Vikunja project 6 with correct identifier |
|
||||
| `make pytest-cov` fails | Coverage below 100% | Add tests for new code paths |
|
||||
| `devx.tools.configure_repo` fails | CI_GITEA_TOKEN missing or invalid | Set token with repo admin scope and re-run |
|
||||
| `configure_repo` sets wrong status checks | Stale `BRANCH_PROTECTION_CONFIG` | Updated to include `(pull_request)` suffix; re-run `configure_repo` |
|
||||
| Token visible in `ps aux` during install | Old version passed tokens via command line | Fixed: tokens now passed via temp file with `0600` permissions |
|
||||
| `remove-runner.yml` leaves lingering enabled | Old version didn't disable lingering | Fixed: now runs `loginctl disable-linger` and removes subuid/subgid |
|
||||
| apt cache update always reports `changed` | `cache_valid_time: 0` forced update every run | Fixed: changed to `cache_valid_time: 3600` |
|
||||
| Prune/service templates created even when `docker_rootless_setup: false` | Template tasks not guarded | Fixed: template creation now guarded by `docker_rootless_setup` |
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entrypoint for Gitea Runner Manager CLI."""
|
||||
|
||||
from gitea_runner_manager.cli import cli
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre-commit hook: fail if unit tests are too slow.
|
||||
# Checks both total suite time (10s) and per-test time (0.5s).
|
||||
# Aligned with CI (ci.yml uses same thresholds).
|
||||
set -e
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre-push hook: validate Vikunja task exists and tests are fast.
|
||||
#
|
||||
# This catches issues that would otherwise only surface in CI:
|
||||
# - Branch name missing task ID (e.g., GRM-N)
|
||||
# - Vikunja task does not exist for the task ID in the branch name
|
||||
# - Unit tests too slow (total > 4s, per-test > 0.5s)
|
||||
#
|
||||
# Uses devx.tools.pre_push_check for reusable validation logic.
|
||||
# Project config (task prefix, Vikunja project ID) is read from
|
||||
# [tool.devx] in pyproject.toml by devx.config — no hardcoded values here.
|
||||
#
|
||||
# Bootstrap resilience: if devx is not importable (e.g., during devx
|
||||
# upgrades), the hook prints a warning and allows the push.
|
||||
|
||||
# Determine the branch being pushed
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$BRANCH" ] || [ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Load .env if present (for VIKUNJA_TOKEN)
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
. .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
# Find the Python interpreter with devx installed
|
||||
if [ -f .venv/bin/python ]; then
|
||||
PY=.venv/bin/python
|
||||
elif [ -x "${HOME}/.pyenv/bin/pyenv" ]; then
|
||||
export PYENV_ROOT="${HOME}/.pyenv"
|
||||
export PATH="${PYENV_ROOT}/bin:${PYENV_ROOT}/shims:${PATH}"
|
||||
eval "$("${PYENV_ROOT}/bin/pyenv" init -)" 2>/dev/null || true
|
||||
eval "$("${PYENV_ROOT}/bin/pyenv" virtualenv-init -)" 2>/dev/null || true
|
||||
pyenv activate gitea-runner-manager 2>/dev/null || true
|
||||
PY=python3
|
||||
else
|
||||
PY=python3
|
||||
fi
|
||||
|
||||
# Bootstrap resilience: if devx is not importable, warn but allow the push
|
||||
if ! $PY -c "import devx.tools.pre_push_check" 2>/dev/null; then
|
||||
echo "WARNING: devx not installed — pre-push check skipped."
|
||||
echo "Run 'make setup' to install devx."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run pre-push validation via devx
|
||||
$PY -m devx.tools.pre_push_check --branch "$BRANCH" || {
|
||||
echo ""
|
||||
echo "Pre-push validation failed. Fix the issues above before pushing."
|
||||
echo "To bypass (NOT recommended): git push --no-verify"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check test speed (aligned with CI thresholds)
|
||||
$PY -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
-454
@@ -1,454 +0,0 @@
|
||||
# Gitea Runner Manager (GRM) – Complete Project Plan
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
**Gitea Runner Manager (GRM)** is a lean command‑line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on **Arch Linux, Ubuntu (22.04, 24.04, 26.04), and Debian (12, 13)** hosts. It is designed to:
|
||||
|
||||
- Be **simple and focused** – no unnecessary features.
|
||||
- Be **secure** – no hardcoded secrets, uses scoped tokens.
|
||||
- Be **idempotent** – can be run multiple times safely.
|
||||
- Be **flexible** – accepts a plain IP address or hostname, and allows specifying the SSH user and private key.
|
||||
|
||||
GRM provides a unified CLI (`grm.py`) and a `make install` target to:
|
||||
|
||||
- List registered runners in a Gitea instance.
|
||||
- Generate registration tokens.
|
||||
- Install and configure a runner on a remote host (Docker, `act_runner`, systemd service, safe Docker pruning).
|
||||
- Update the `act_runner` binary without losing registration.
|
||||
- (Future) Uninstall a runner.
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Design Decisions
|
||||
|
||||
| Area | Decision | Rationale |
|
||||
|------|----------|-----------|
|
||||
| Target OS | Arch Linux, Ubuntu 22.04/24.04/26.04, Debian 12/13 | Covers 99% of use cases; avoids complexity. |
|
||||
| Architecture | amd64 only | Hetzner and most cloud providers use x86_64. |
|
||||
| Backup | Lightweight config backup (optional) | Runner state is stored in Gitea; re‑registration is trivial. |
|
||||
| Monitoring | None | Gitea UI shows runner status; manual checks are enough. |
|
||||
| Logging | Systemd `journald` | Sufficient for debugging; no centralised logging needed. |
|
||||
| Pruning | Only runner‑labelled resources | Prevents accidental deletion of unrelated containers. |
|
||||
| Integration tests | Run after installation; fail if not successful | Ensures runner is functional from the start. |
|
||||
| Token storage | `.env` file or `--token` flag | No secrets in code; supports CI/CD. |
|
||||
| Host specification | Plain IP or hostname; SSH user and key overridable | Simplifies inventory management, works with any host. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
GRM consists of three layers:
|
||||
|
||||
1. **CLI (Python)**: User commands, Gitea API interactions, Ansible invocation.
|
||||
2. **Ansible Playbook**: Idempotent installation of runner on target host, adapting to OS distribution.
|
||||
3. **Integration Tests**: Run after installation; verify runner is online in Gitea.
|
||||
|
||||
```text
|
||||
+----------------+ +----------------+ +-----------------+
|
||||
| User / CI | ----> | grm.py CLI | ----> | Gitea API |
|
||||
+----------------+ +----------------+ +-----------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Ansible Playbook |
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Remote Host |
|
||||
| (Arch/Ubuntu |
|
||||
| /Debian) |
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Integration Tests|
|
||||
| (post-install) |
|
||||
+------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Project Structure
|
||||
|
||||
```
|
||||
gitea-runner-manager/
|
||||
├── .python-version # 3.11.11
|
||||
├── .env.example # Environment variables template
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── LICENSE (GPL-3.0)
|
||||
├── Makefile # Targets: setup, install, update, lint, ansible-lint, test, etc.
|
||||
├── pyproject.toml # Single source for Python dependencies
|
||||
├── setup.py # Minimal setup for editable install
|
||||
├── grm.py # CLI entrypoint
|
||||
├── src/
|
||||
│ └── gitea_runner_manager/
|
||||
│ ├── __init__.py
|
||||
│ ├── cli.py # CLI logic (click commands)
|
||||
│ ├── runner_manager.py # Core logic (API calls, Ansible invocation)
|
||||
│ ├── api_client.py # Gitea API interactions
|
||||
│ └── exceptions.py # Custom exceptions
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ ├── unit/
|
||||
│ │ ├── test_runner_manager.py
|
||||
│ │ └── test_api_client.py
|
||||
│ └── integration/
|
||||
│ └── test_provision.py # Integration tests for installation
|
||||
├── ansible/
|
||||
│ ├── requirements.yml # Ansible collections
|
||||
│ ├── install-runner.yml # Main playbook
|
||||
│ ├── update-runner.yml # Update playbook (future)
|
||||
│ ├── inventory.example # Optional static inventory (not required)
|
||||
│ ├── group_vars/
|
||||
│ │ └── all.yml
|
||||
│ └── roles/
|
||||
│ └── gitea-runner/
|
||||
│ ├── tasks/
|
||||
│ │ ├── main.yml
|
||||
│ │ ├── docker.yml # Install Docker (OS-specific)
|
||||
│ │ ├── download_act_runner.yml # Download binary
|
||||
│ │ ├── validate.yml # Validate binary
|
||||
│ │ ├── register.yml # Register with Gitea
|
||||
│ │ ├── config.yml # Create config file
|
||||
│ │ ├── service.yml # Systemd service
|
||||
│ │ ├── prune.yml # Docker prune timer
|
||||
│ │ └── integration_test.yml # Post-install validation
|
||||
│ ├── handlers/
|
||||
│ │ └── main.yml
|
||||
│ ├── templates/
|
||||
│ │ ├── act-runner.service.j2
|
||||
│ │ ├── act-runner-config.toml.j2
|
||||
│ │ ├── docker-prune.service.j2
|
||||
│ │ └── docker-prune.timer.j2
|
||||
│ ├── vars/
|
||||
│ │ └── main.yml
|
||||
│ └── molecule/
|
||||
│ └── default/
|
||||
│ ├── molecule.yml
|
||||
│ ├── converge.yml
|
||||
│ ├── verify.yml
|
||||
│ └── prepare.yml
|
||||
└── .pre-commit-config.yaml # Pre-commit and pre-push hooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Python Environment and Dependencies
|
||||
|
||||
- **Python version**: 3.11.11 (managed by pyenv).
|
||||
- **Virtual environment**: Created automatically by `make setup` (or manually with `python -m venv .venv`).
|
||||
|
||||
**Dependencies** (defined in `pyproject.toml`):
|
||||
|
||||
| Type | Packages |
|
||||
|------|----------|
|
||||
| Runtime | `requests`, `python-dotenv`, `click`, `ansible` |
|
||||
| Development | `pytest`, `pytest-cov`, `ruff`, `pyright`, `molecule`, `molecule-docker`, `ansible-lint`, `pre-commit` |
|
||||
|
||||
All dependencies are installed with `make setup` or `pip install -e .[dev]`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Makefile (Complete)
|
||||
|
||||
The `Makefile` provides the following targets:
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `setup` | Full environment setup: checks pyenv, installs Python dependencies, Ansible collections, and pre‑commit hooks. |
|
||||
| `install` | Installs a runner on a host. **Requires `HOST`**, optional `USER`, `KEY`, `NAME`, `TOKEN`. Example: `make install HOST=192.168.1.10 USER=arch NAME=my-runner` |
|
||||
| `update` | Updates the `act_runner` binary on the specified host (future). |
|
||||
| `lint` | Runs Python linters (`ruff`, `pyright`). |
|
||||
| `ansible-lint` | Runs `ansible-lint` on all playbooks and roles. |
|
||||
| `lint-all` | Runs `lint` and `ansible-lint`. |
|
||||
| `test-unit` | Runs unit tests with coverage. |
|
||||
| `pytest-cov` | Runs unit tests with **100% coverage requirement**. |
|
||||
| `molecule` | Runs Ansible Molecule tests. |
|
||||
| `test-all` | Runs `pytest-cov` and `molecule`. |
|
||||
| `clean` | Removes temporary files and caches. |
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```bash
|
||||
make setup # Initialize development environment
|
||||
|
||||
# Install runner on a host (plain IP) with default user (ansible_user in inventory)
|
||||
make install HOST=192.168.1.10
|
||||
|
||||
# With custom user and SSH private key
|
||||
make install HOST=192.168.1.10 USER=arch KEY=~/.ssh/id_ed25519
|
||||
|
||||
# With custom runner name and token (token auto-generated if omitted)
|
||||
make install HOST=runner.example.com USER=ubuntu NAME=prod-runner
|
||||
|
||||
make ansible-lint # Lint Ansible code
|
||||
make test-all # Run all tests (unit + molecule)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Pre-commit and Pre-push Hooks
|
||||
|
||||
Defined in `.pre-commit-config.yaml`. Hooks run automatically on `git commit` and `git push`.
|
||||
|
||||
| Hook | Stage | Purpose |
|
||||
|------|-------|---------|
|
||||
| `ruff-lint` | commit | Lint Python code |
|
||||
| `ruff-format` | commit | Format Python code |
|
||||
| `pyright` | commit | Type‑check Python code |
|
||||
| `ansible-lint` | commit | Lint Ansible playbooks/roles |
|
||||
| `detect-secrets` | commit | Prevent committing secrets |
|
||||
| `pytest-cov` | push | **100% unit test coverage** |
|
||||
| `test-all` | push | Run all tests (unit + molecule) |
|
||||
|
||||
If any hook fails, the commit or push is blocked.
|
||||
|
||||
---
|
||||
|
||||
## 8. CLI – `grm.py`
|
||||
|
||||
The CLI is built with `click` and provides the following commands:
|
||||
|
||||
```bash
|
||||
# List all registered runners
|
||||
./grm.py list
|
||||
|
||||
# Generate a new registration token
|
||||
./grm.py token
|
||||
|
||||
# Install and configure a runner on a remote host
|
||||
./grm.py install <host> --user <user> [--key <private_key_path>] [--name <runner_name>] [--token <token>]
|
||||
|
||||
# Update runner binary
|
||||
./grm.py update <host> --user <user> [--key <private_key_path>] [--version <specific_version>]
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `--user`: SSH user (default: from environment or `ansible_user` in inventory, fallback to `root`).
|
||||
- `--key`: Path to private SSH key (optional, uses default key if not provided).
|
||||
- `--name`: Runner name (default: hostname).
|
||||
- `--token`: Registration token (auto-generated if not provided).
|
||||
|
||||
**Environment**:
|
||||
- Reads `.env` file if present.
|
||||
- Uses `GITEA_URL`, `GITEA_TOKEN`, and optionally `GITEA_RUNNER_USER`, `GITEA_RUNNER_KEY` from environment.
|
||||
|
||||
**Implementation** (`src/gitea_runner_manager/cli.py`):
|
||||
- `list` → calls `api_client.get_runners()`.
|
||||
- `token` → calls `api_client.create_registration_token()`.
|
||||
- `install` → generates token (if not provided), builds an Ansible command with `-i <host>,` and `--user <user>` and `--private-key <key>`.
|
||||
- `update` → similar to install but with the update playbook.
|
||||
|
||||
**Ansible invocation**:
|
||||
```bash
|
||||
ansible-playbook install-runner.yml \
|
||||
-i "<host>," \
|
||||
-u <user> \
|
||||
--private-key <key> \
|
||||
--extra-vars "registration_token=<token> runner_name=<name>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Ansible Role – `gitea-runner`
|
||||
|
||||
The role performs the following tasks in order, adapting to the OS distribution using `ansible_facts['os_family']` and `ansible_distribution`.
|
||||
|
||||
### 9.1. `docker.yml` – OS‑specific Docker installation
|
||||
|
||||
- **For Debian/Ubuntu**:
|
||||
- Install `apt-transport-https`, `ca-certificates`, `curl`.
|
||||
- Add Docker GPG key and repository.
|
||||
- Install `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-compose-plugin`.
|
||||
|
||||
- **For Arch Linux**:
|
||||
- Install `docker`, `docker-compose` using `pacman`.
|
||||
- Ensure the `docker` systemd service is enabled and started.
|
||||
- Add the current user to the `docker` group.
|
||||
|
||||
The playbook detects the OS family and executes the appropriate block.
|
||||
|
||||
### 9.2. `download_act_runner.yml`
|
||||
- Fetches the latest (or specified) `act_runner` binary from Gitea releases.
|
||||
- Extracts it to `/usr/local/bin/act_runner` and sets executable permissions.
|
||||
- Uses `ansible_architecture` to choose the correct binary (`linux_amd64`).
|
||||
|
||||
### 9.3. `validate.yml`
|
||||
- Checks that `/usr/local/bin/act_runner` exists and is executable.
|
||||
- Runs `act_runner --version` to ensure it works.
|
||||
- Checks Docker connectivity (`docker version`).
|
||||
- Sets `runner_validated: true` if all checks pass.
|
||||
|
||||
### 9.4. `config.yml`
|
||||
- Creates `/etc/act-runner/config.toml` with the following content:
|
||||
```toml
|
||||
log.level = "info"
|
||||
runner.file = ".runner"
|
||||
container.label = "gitea-runner=true"
|
||||
```
|
||||
- This ensures all spawned containers are labelled, enabling safe pruning.
|
||||
|
||||
### 9.5. `register.yml`
|
||||
- Ensures work directory (`/var/lib/gitea-runner`) exists.
|
||||
- Runs `act_runner register` with the provided `registration_token`, `runner_name`, `labels`, and `gitea_url`.
|
||||
- Skips registration if `.act_runner` already exists (idempotent).
|
||||
|
||||
### 9.6. `service.yml`
|
||||
- Creates systemd service file `/etc/systemd/system/act-runner-{{ runner_name }}.service`.
|
||||
- Points to the config file with `--config /etc/act-runner/config.toml`.
|
||||
- Enables and starts the service.
|
||||
|
||||
### 9.7. `prune.yml`
|
||||
- Creates systemd service and timer for daily Docker prune:
|
||||
- `docker-prune.service`: runs `docker system prune` and `docker volume prune` with filters for `label=gitea-runner=true` and `until=24h`.
|
||||
- `docker-prune.timer`: triggers daily.
|
||||
- Enables and starts the timer.
|
||||
|
||||
### 9.8. `integration_test.yml`
|
||||
- Waits up to 2 minutes for the runner to appear in the Gitea API.
|
||||
- Checks that the runner status is `"online"`.
|
||||
- Fails the playbook if the runner is not found or not online.
|
||||
- This ensures that the runner is fully functional after installation.
|
||||
|
||||
---
|
||||
|
||||
## 10. Integration Tests (Detailed)
|
||||
|
||||
After registration and service start, the playbook runs `integration_test.yml`. It uses the Gitea API to verify the runner is online. The test is written in Ansible and uses the `uri` module.
|
||||
|
||||
**Conditions**:
|
||||
- Retry every 10 seconds for up to 12 attempts (2 minutes total).
|
||||
- If the runner is not found or not online, the playbook fails with a clear error message.
|
||||
|
||||
**Why this matters**:
|
||||
- Catches registration failures early.
|
||||
- Ensures the runner can communicate with Gitea.
|
||||
- Prevents deploying a broken runner.
|
||||
|
||||
---
|
||||
|
||||
## 11. Safe Docker Pruning
|
||||
|
||||
The runner labels all its containers with `gitea-runner=true` (via `container.label` in the config file). The prune service uses `--filter "label=gitea-runner=true"` to ensure it only removes resources created by the runner. This guarantees that other services on the same host are not affected.
|
||||
|
||||
---
|
||||
|
||||
## 12. Quality Gates
|
||||
|
||||
- **100% unit test coverage** (`make pytest-cov`).
|
||||
- **All linters pass** (ruff, pyright, ansible-lint).
|
||||
- **Molecule tests pass** (role validation in Docker container).
|
||||
- **Integration tests pass** (post‑installation validation).
|
||||
|
||||
These gates are enforced by pre‑push hooks.
|
||||
|
||||
---
|
||||
|
||||
## 13. Installation & Usage
|
||||
|
||||
### 13.1. Developer Setup
|
||||
|
||||
```bash
|
||||
git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git
|
||||
cd gitea-runner-manager
|
||||
pyenv install 3.11.11
|
||||
pyenv local 3.11.11
|
||||
make setup
|
||||
```
|
||||
|
||||
### 13.2. Configure Gitea Credentials
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# GITEA_URL=https://git.oblachno.oblachno.com
|
||||
# GITEA_TOKEN=your-personal-access-token
|
||||
# Optional: GITEA_RUNNER_USER=ubuntu # default SSH user
|
||||
# Optional: GITEA_RUNNER_KEY=~/.ssh/id_rsa
|
||||
```
|
||||
|
||||
The token needs `admin:runner` scope (or `admin` for full management).
|
||||
|
||||
### 13.3. Install a Runner
|
||||
|
||||
Using the CLI (recommended for flexibility):
|
||||
|
||||
```bash
|
||||
./grm.py install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||
```
|
||||
|
||||
Using Make:
|
||||
|
||||
```bash
|
||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
||||
```
|
||||
|
||||
If `USER` is not provided, the CLI uses the environment variable `GITEA_RUNNER_USER` or falls back to the current local user's username (which may not exist on the remote host – it's better to always specify).
|
||||
|
||||
### 13.4. Verify Runner
|
||||
|
||||
Check Gitea admin UI under **Actions → Runners**. The runner should appear as **Online**.
|
||||
|
||||
### 13.5. Update Runner Binary (Future)
|
||||
|
||||
```bash
|
||||
./grm.py update 192.168.1.10 --user ubuntu
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Molecule Tests
|
||||
|
||||
The Ansible role is tested with Molecule using a systemd‑enabled Docker container. For Arch Linux, we may use a different Docker image (e.g., `archlinux/archlinux`). The test suite will include scenarios for Ubuntu, Debian, and Arch Linux.
|
||||
|
||||
The `default` scenario:
|
||||
- Verifies Docker installation.
|
||||
- Checks that `act_runner` binary is present and executable.
|
||||
- Asserts that the systemd service is enabled and running.
|
||||
- Ensures the prune timer is active.
|
||||
- Runs the `integration_test` task with a mock Gitea API (or skips it if `runner_register=false`).
|
||||
|
||||
Molecule tests run as part of `make test-all`.
|
||||
|
||||
---
|
||||
|
||||
## 15. Logging
|
||||
|
||||
- All Ansible output goes to stdout (visible in CLI).
|
||||
- `act_runner` logs go to `journald` via the systemd service.
|
||||
- To view runner logs: `sudo journalctl -u act-runner-<name> -f`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Future Extensions (Optional)
|
||||
|
||||
- **Uninstall**: A playbook to stop the service, remove the binary, and delete the work directory.
|
||||
- **Version pinning**: Allow specifying a particular `act_runner` version via CLI.
|
||||
- **Additional distributions**: Extend the role to support more distros if needed.
|
||||
|
||||
---
|
||||
|
||||
## 17. Success Criteria
|
||||
|
||||
- [ ] `make setup` configures the development environment.
|
||||
- [ ] `make install HOST=... USER=...` provisions a runner on Ubuntu, Debian, and Arch Linux.
|
||||
- [ ] Integration tests pass after installation; installation fails if they do not.
|
||||
- [ ] Pre‑commit and pre‑push hooks enforce quality gates (100% coverage, linting).
|
||||
- [ ] Molecule tests pass for all supported OS.
|
||||
- [ ] Docker prune only affects runner‑labelled resources.
|
||||
- [ ] `make ansible-lint` runs successfully.
|
||||
- [ ] Documentation is complete and accurate.
|
||||
|
||||
---
|
||||
|
||||
## 18. License
|
||||
|
||||
- **GPL‑3.0** – open source, free to use and modify.
|
||||
|
||||
---
|
||||
|
||||
**This GRM plan is production‑ready, lean, cross‑distribution, and flexible.** It supports Arch, Ubuntu, and Debian, and accepts plain IP addresses with configurable SSH user and key. All components are specified, and the `make setup` command gets a developer from zero to a fully configured environment in minutes.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user