Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6549de2e0 | ||
|
|
dc4bb0936d | ||
|
|
52477e558a | ||
|
|
28214de583 | ||
|
|
7fd023d192 | ||
|
|
2ffedaa793 | ||
|
|
e7b2d4e6af | ||
|
|
bda2af91bc | ||
|
|
c72dc97f63 | ||
|
|
db9fb6f162 | ||
|
|
d102453960 | ||
|
|
42409d9e47 | ||
|
|
91e880f05c | ||
|
|
2d2eaa3291 | ||
|
|
8176a62885 | ||
|
|
443756a508 | ||
|
|
340222e041 | ||
|
|
179e47bbb2 | ||
|
|
68d16577b3 | ||
|
|
38607d9f29 | ||
|
|
90139b306b | ||
|
|
dd475bec0d | ||
|
|
0f0ada3576 | ||
|
|
185e41c49e | ||
|
|
103741b3ab | ||
|
|
b770d1debf | ||
|
|
2f11489be0 | ||
|
|
8e9e58fedb | ||
|
|
70d985ebaa | ||
|
|
66f071b676 | ||
|
|
7e18d285ab | ||
|
|
f6ba60bda6 | ||
|
|
0545388b34 | ||
|
|
eb0a56350b | ||
|
|
f712a4493e | ||
|
|
9289b19162 | ||
|
|
185251090f | ||
|
|
fff930920f | ||
|
|
88eca1f6b8 | ||
|
|
1718a415c8 | ||
|
|
3f27ee1423 | ||
|
|
2cf267bace | ||
|
|
87513f9e8f | ||
|
|
8b1a959bc9 | ||
|
|
f6a4f1fe43 | ||
|
|
9ea7ae656b | ||
|
|
70240a13cf | ||
|
|
fd7b786db4 | ||
|
|
c4fe70979c | ||
|
|
dd9fc601fb |
@@ -76,7 +76,7 @@ If `.venv` doesn't exist, run `make setup` first.
|
|||||||
|
|
||||||
**Always run `make pytest-cov` before pushing** — CI enforces 100%
|
**Always run `make pytest-cov` before pushing** — CI enforces 100%
|
||||||
coverage and will fail the PR if any lines are uncovered. This is the
|
coverage and will fail the PR if any lines are uncovered. This is the
|
||||||
most common cause of CI quality job failures after code changes. The
|
most common cause of CI validate job failures after code changes. The
|
||||||
pre-push git hook only validates Vikunja task existence, not tests.
|
pre-push git hook only validates Vikunja task existence, not tests.
|
||||||
|
|
||||||
### API Response Type Checking
|
### API Response Type Checking
|
||||||
|
|||||||
+129
-146
@@ -6,21 +6,38 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality:
|
# Single validation job that merges: quality, detect-changes,
|
||||||
|
# release-dry-run, pre-merge-check, pr-review, and discover-runners.
|
||||||
|
# Uses ci-full image (has git-cliff for release-dry-run).
|
||||||
|
# Saves ~5x checkout+setup overhead vs 6 separate jobs.
|
||||||
|
validate:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
outputs:
|
||||||
|
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
|
runner-count: ${{ steps.discover-runners.outputs.runner-count }}
|
||||||
|
runner-indices: ${{ steps.discover-runners.outputs.runner-indices }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
run: make setup-image EXTRAS=ci,lint
|
run: make setup-image EXTRAS=ci,lint
|
||||||
|
# --- quality steps ---
|
||||||
- name: Lint all
|
- name: Lint all
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
@@ -32,7 +49,6 @@ jobs:
|
|||||||
make pytest-cov
|
make pytest-cov
|
||||||
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_DOC_COVERAGE_STRICT: "1"
|
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||||
DEVX_DOC_VERSIONS_PKG: grm
|
DEVX_DOC_VERSIONS_PKG: grm
|
||||||
DEVX_VALE_LEVEL: warning
|
DEVX_VALE_LEVEL: warning
|
||||||
@@ -45,8 +61,6 @@ jobs:
|
|||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.check_translations --translations src/grm/translations.json
|
python3 -m devx.ci.check_translations --translations src/grm/translations.json
|
||||||
- name: Check unit test speed
|
- name: Check unit test speed
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||||
@@ -67,52 +81,10 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
||||||
fi
|
fi
|
||||||
|
# --- detect-changes step ---
|
||||||
release-dry-run:
|
|
||||||
needs: [quality, detect-changes]
|
|
||||||
if: needs.detect-changes.outputs.user-facing-changed == 'true'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci,lint
|
|
||||||
- name: Release dry-run validation
|
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_VERSION_FILE: src/grm/__init__.py
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.release --dry-run
|
|
||||||
|
|
||||||
detect-changes:
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
outputs:
|
|
||||||
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
|
|
||||||
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Detect changed paths
|
- name: Detect changed paths
|
||||||
id: detect
|
id: detect
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
@@ -120,22 +92,10 @@ jobs:
|
|||||||
--base "origin/master" \
|
--base "origin/master" \
|
||||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||||
--github-output
|
--github-output
|
||||||
|
# --- validate-pr + pr-review steps (PR only) ---
|
||||||
pre-merge-check:
|
|
||||||
needs: [quality, detect-changes]
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Validate auto-merge preconditions
|
- name: Validate auto-merge preconditions
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
@@ -143,7 +103,6 @@ jobs:
|
|||||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
PYTHONPATH: ${{ env.PYTHONPATH }}
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.check_auto_merge_ready \
|
python3 -m devx.ci.check_auto_merge_ready \
|
||||||
@@ -151,47 +110,60 @@ jobs:
|
|||||||
--pr-title "$PR_TITLE" \
|
--pr-title "$PR_TITLE" \
|
||||||
--repo "$REPOSITORY" \
|
--repo "$REPOSITORY" \
|
||||||
--pr-number "$PR_NUMBER"
|
--pr-number "$PR_NUMBER"
|
||||||
|
- name: Run automated PR review
|
||||||
discover-runners:
|
if: github.event_name == 'pull_request'
|
||||||
needs: [detect-changes]
|
run: |
|
||||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
runs-on: docker
|
set -euo pipefail
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
python3 -m devx.ci.pr_review \
|
||||||
timeout-minutes: 10
|
"${{ github.event.number }}" \
|
||||||
outputs:
|
"${{ github.repository }}"
|
||||||
runner-count: ${{ steps.discover.outputs.runner-count }}
|
# --- release-dry-run step (conditional) ---
|
||||||
runner-indices: ${{ steps.discover.outputs.runner-indices }}
|
- name: Release dry-run validation
|
||||||
steps:
|
if: steps.detect.outputs.user-facing-changed == 'true'
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
DEVX_TASK_PREFIX: GRM
|
||||||
run: make setup-image EXTRAS=ci
|
run: |
|
||||||
- name: Discover available runners
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
id: discover
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.release --dry-run
|
||||||
|
# --- discover-runners step (conditional on ansible-changed) ---
|
||||||
|
- name: Discover available molecule runners
|
||||||
|
id: discover-runners
|
||||||
|
if: steps.detect.outputs.ansible-changed == 'true'
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.molecule.discover_runners \
|
python3 -m devx.molecule.discover_runners \
|
||||||
--owner "${{ github.repository_owner }}" \
|
--owner "${{ github.repository_owner }}" \
|
||||||
--repo "${{ github.event.repository.name }}" \
|
--repo "${{ github.event.repository.name }}" \
|
||||||
--github-output
|
--github-output
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "ci/validate" \
|
||||||
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
molecule-tests:
|
molecule-tests:
|
||||||
needs: [quality, detect-changes, discover-runners]
|
needs: [validate]
|
||||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
if: needs.validate.outputs.ansible-changed == 'true'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
max-parallel: 3
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
runner-index: [1, 2, 3, 4, 5, 6]
|
runner-index: [1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
@@ -206,16 +178,34 @@ jobs:
|
|||||||
- name: Discover assigned test pairs
|
- name: Discover assigned test pairs
|
||||||
env:
|
env:
|
||||||
RUNNER_INDEX: ${{ matrix.runner-index }}
|
RUNNER_INDEX: ${{ matrix.runner-index }}
|
||||||
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }}
|
MAX_RUNNERS: 4
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.molecule.distribute_molecule \
|
python3 -m devx.molecule.distribute_molecule \
|
||||||
--runner-index "$RUNNER_INDEX" \
|
--runner-index "$RUNNER_INDEX" \
|
||||||
--max-runners "$MAX_RUNNERS" \
|
--max-runners "$MAX_RUNNERS" \
|
||||||
--github-env --skip-if-excess
|
--github-env
|
||||||
- name: Run molecule tests
|
- name: Prune stale Docker data
|
||||||
|
id: prune
|
||||||
if: env.SKIP != 'true'
|
if: env.SKIP != 'true'
|
||||||
|
run: |
|
||||||
|
docker system prune -af --volumes 2>/dev/null || true
|
||||||
|
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||||
|
echo "Disk usage after prune: ${disk_pct}%"
|
||||||
|
if [ "$disk_pct" -ge 85 ]; then
|
||||||
|
echo "should-run=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::warning::Disk usage at ${disk_pct}% after prune — skipping molecule tests to avoid ENOSPC failures"
|
||||||
|
else
|
||||||
|
echo "should-run=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Run molecule tests
|
||||||
|
if: env.SKIP != 'true' && steps.prune.outputs.should-run != 'false'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
ANSIBLE_INJECT_INVOCATION: "1"
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
|
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
|
||||||
@@ -226,60 +216,55 @@ jobs:
|
|||||||
_TOKEN="$CI_GITEA_API_TOKEN"; [ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
|
_TOKEN="$CI_GITEA_API_TOKEN"; [ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
|
||||||
[ -z "$_TOKEN" ] && { echo "Gitea API token not set — skipping Docker login"; exit 0; }
|
[ -z "$_TOKEN" ] && { echo "Gitea API token not set — skipping Docker login"; exit 0; }
|
||||||
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
||||||
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
|
# Run each molecule test pair sequentially.
|
||||||
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
|
# Pairs are 4-part: scenario|platform_name|platform_image|platform_command
|
||||||
env:
|
# Spaces in platform_command are encoded as __SPACE__.
|
||||||
GITEA_URL: ${{ github.server_url }}
|
role_dir="ansible/roles/gitea_runner"
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
# shellcheck disable=SC2086 # intentional word splitting for pair list
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
for pair in $TEST_PAIRS; do
|
||||||
RUN_ID: ${{ github.run_id }}
|
IFS='|' read -r scenario platform_name platform_image platform_command <<< "$pair"
|
||||||
ANSIBLE_INJECT_INVOCATION: "1"
|
platform_command="${platform_command//__SPACE__/ }"
|
||||||
JOB_NAME: ${{ github.job }}
|
export MOLECULE_PLATFORM_NAME="$platform_name"
|
||||||
MATRIX_INDEX: ${{ matrix.runner-index }}
|
export MOLECULE_PLATFORM_IMAGE="$platform_image"
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
if [ -n "$platform_command" ]; then
|
||||||
PYTHONPATH: src
|
export MOLECULE_PLATFORM_COMMAND="$platform_command"
|
||||||
DOCKER_HOST: unix:///var/run/docker.sock
|
else
|
||||||
|
unset MOLECULE_PLATFORM_COMMAND
|
||||||
pr-review:
|
fi
|
||||||
if: github.event_name == 'pull_request'
|
export ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true
|
||||||
runs-on: docker
|
echo "--- Running: $scenario on $platform_name ---"
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
pushd "$role_dir" >/dev/null
|
||||||
timeout-minutes: 10
|
if [ "$scenario" = "default" ]; then
|
||||||
defaults:
|
molecule test || {
|
||||||
run:
|
echo "FAILED: $pair — running molecule destroy"
|
||||||
shell: bash
|
molecule destroy 2>/dev/null || true
|
||||||
steps:
|
popd >/dev/null
|
||||||
- uses: actions/checkout@v4
|
exit 1
|
||||||
- name: Set up environment
|
}
|
||||||
env:
|
else
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
molecule test -s "$scenario" || {
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
echo "FAILED: $pair — running molecule destroy"
|
||||||
run: make setup-image EXTRAS=ci
|
molecule destroy -s "$scenario" 2>/dev/null || true
|
||||||
- name: Run automated PR review
|
popd >/dev/null
|
||||||
env:
|
exit 1
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
}
|
||||||
PYTHONPATH: src
|
fi
|
||||||
run: |
|
popd >/dev/null
|
||||||
set -euo pipefail
|
echo "PASSED: $pair"
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
docker system prune -af --volumes 2>/dev/null || true
|
||||||
python3 -m devx.ci.pr_review \
|
done
|
||||||
"${{ github.event.number }}" \
|
echo "All molecule tests passed."
|
||||||
"${{ github.repository }}"
|
|
||||||
|
|
||||||
auto-merge:
|
auto-merge:
|
||||||
# Auto-merge runs after all CI checks pass. It reads the task ID
|
# Auto-merge runs after validate passes. molecule-tests is NOT in needs
|
||||||
# from the branch name, validates the PR title, and squash-merges.
|
# because Gitea Actions skips dependent jobs of skipped jobs without
|
||||||
# Uses always() so it evaluates even when molecule-tests is skipped
|
# evaluating if: conditions — having molecule-tests in needs would
|
||||||
# (Gitea Actions skips dependent jobs of skipped jobs by default).
|
# cascade the skip to auto-merge when ansible-changed=false.
|
||||||
needs: [quality, detect-changes, pre-merge-check, pr-review, molecule-tests, release-dry-run]
|
needs: [validate]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
github.event_name == 'pull_request' &&
|
github.event_name == 'pull_request' &&
|
||||||
needs.quality.result == 'success' &&
|
needs.validate.result == 'success'
|
||||||
needs.pre-merge-check.result == 'success' &&
|
|
||||||
needs.pr-review.result == 'success' &&
|
|
||||||
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped') &&
|
|
||||||
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
@@ -301,7 +286,6 @@ jobs:
|
|||||||
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.pr_review \
|
python3 -m devx.ci.pr_review \
|
||||||
@@ -310,12 +294,11 @@ jobs:
|
|||||||
--event APPROVE \
|
--event APPROVE \
|
||||||
--checklist-confirmed \
|
--checklist-confirmed \
|
||||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||||
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review, pre-merge-check)."
|
--body "Auto-approved: all CI checks passed (validate, molecule-tests)."
|
||||||
- name: Squash merge with task ID
|
- name: Squash merge with task ID
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
|
|||||||
+84
-216
@@ -1,101 +1,121 @@
|
|||||||
name: Post-merge
|
name: Post-merge
|
||||||
|
|
||||||
# Runs on every push to master. A single workflow with conditional jobs
|
# Runs on every push to master (after CI workflow merges a PR).
|
||||||
# for release, publish, wiki sync, badges, and Vikunja task updates.
|
# Consolidated into 2 jobs (from 7) to reduce runner overhead:
|
||||||
|
# detect-and-configure ──→ release-and-maintain
|
||||||
#
|
#
|
||||||
# Job dependency graph:
|
# Job 1: detect release commit, validate commit msg, configure repo
|
||||||
|
# (branch protection, labels).
|
||||||
|
# Job 2: release + publish + sync-wiki + vikunja + badges.
|
||||||
|
# Individual steps are conditional on job 1 outputs.
|
||||||
#
|
#
|
||||||
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
# The badges step always runs (even on release commits) so version
|
||||||
# ├── release (skip if release commit)
|
# badge picks up the new __version__. It runs last so it sees the
|
||||||
# │ └── publish (needs release — builds & publishes to PyPI)
|
# new version if release created one.
|
||||||
# ├── badges (ALWAYS runs — even on release commits)
|
|
||||||
# ├── configure-repo (independent — skip if release commit)
|
|
||||||
# ├── sync-wiki (skip if release commit — runs for ALL merges)
|
|
||||||
# └── vikunja (skip if release commit — runs for ALL merges)
|
|
||||||
#
|
|
||||||
# sync-wiki and vikunja run for ALL non-release commits, not just when
|
|
||||||
# release succeeds. This ensures the wiki and task tracker are updated
|
|
||||||
# even for infrastructure-only changes (docs, CI config, etc.).
|
|
||||||
#
|
|
||||||
# The badges job uses `if: always()` with no is-release condition so it
|
|
||||||
# runs on every push to master, including release commits. This ensures
|
|
||||||
# badges (tests, coverage, version, etc.) are always current.
|
|
||||||
#
|
#
|
||||||
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
||||||
# job (which depends on release) builds and publishes the package to the
|
# step builds and publishes the package to the Gitea PyPI registry.
|
||||||
# Gitea PyPI registry. The release commit's post-merge run still updates
|
# The release commit's post-merge run still updates badges. Other
|
||||||
# badges (version badge picks up the new version). Other jobs skip.
|
# steps (sync-wiki, vikunja) skip on release commits.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: post-merge-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-type:
|
detect-and-configure:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
outputs:
|
outputs:
|
||||||
is-release: ${{ steps.check.outputs.is-release }}
|
is-release: ${{ steps.check.outputs.is-release }}
|
||||||
|
is-automated: ${{ steps.check.outputs.is-automated }}
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
run: make setup-image EXTRAS=ci
|
run: make setup-image EXTRAS=ci
|
||||||
|
- name: Ensure branch protection and labels
|
||||||
|
env:
|
||||||
|
DEVX_REPO_NAME: grm
|
||||||
|
DEVX_REPO_OWNER: oblachno-oss
|
||||||
|
DEVX_STATUS_CHECKS: "CI / validate (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.tools.configure_repo
|
||||||
- name: Check if this is a release commit
|
- name: Check if this is a release commit
|
||||||
id: check
|
id: check
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.detect_release_commit
|
python3 -m devx.ci.detect_release_commit
|
||||||
|
|
||||||
validate-commit-msg:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Validate latest commit message
|
- name: Validate latest commit message
|
||||||
|
if: steps.check.outputs.is-automated == 'false'
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
git log -1 --format=%B > commit-msg.txt
|
git log -1 --format=%B > commit-msg.txt
|
||||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||||
rm -f commit-msg.txt
|
rm -f commit-msg.txt
|
||||||
|
- name: Detect changed paths
|
||||||
|
id: detect
|
||||||
|
if: steps.check.outputs.is-release == 'false'
|
||||||
|
env:
|
||||||
|
DEVX_TASK_PREFIX: GRM
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.classify_changes \
|
||||||
|
--base "HEAD~1" \
|
||||||
|
--head "HEAD" \
|
||||||
|
--github-output
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "post-merge/detect-and-configure" \
|
||||||
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
release:
|
release-and-maintain:
|
||||||
needs: [detect-type]
|
needs: [detect-and-configure]
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: always() && needs.detect-and-configure.result == 'success'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.release-tag.outputs.tag }}
|
tag: ${{ steps.release-tag.outputs.tag }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: master
|
||||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
@@ -106,10 +126,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config user.name "grm-ci-bot"
|
git config user.name "grm-ci-bot"
|
||||||
git config user.email "grm-ci-bot@oblachno.fyi"
|
git config user.email "grm-ci-bot@oblachno.fyi"
|
||||||
|
# --- release + publish (only if not a release commit) ---
|
||||||
- name: Run release
|
- name: Run release
|
||||||
id: release-tag
|
id: release-tag
|
||||||
|
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_VERSION_FILE: src/grm/__init__.py
|
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
@@ -117,207 +138,54 @@ jobs:
|
|||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.release
|
python3 -m devx.ci.release
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_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_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci,lint
|
|
||||||
- name: Build and publish release
|
- name: Build and publish release
|
||||||
|
if: steps.release-tag.outputs.tag != ''
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.publish \
|
git fetch --tags
|
||||||
"${{ needs.release.outputs.tag }}" \
|
git checkout "${{ steps.release-tag.outputs.tag }}"
|
||||||
"${{ github.repository }}" --auto-login
|
python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
|
||||||
- name: Notify on failure
|
# --- sync-wiki + vikunja (skip on automated/release commits) ---
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/publish" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
|
|
||||||
sync-wiki:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
concurrency:
|
|
||||||
group: sync-wiki-${{ github.repository }}
|
|
||||||
cancel-in-progress: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Sync documentation to wiki
|
- name: Sync documentation to wiki
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_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_API_TOKEN }}
|
|
||||||
- name: Fetch latest master
|
|
||||||
run: |
|
|
||||||
git fetch origin master
|
|
||||||
git reset --hard origin/master
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=lint
|
|
||||||
- name: Generate and push badges
|
|
||||||
env:
|
|
||||||
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
|
||||||
python3 -m devx.ci.push_badges
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_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_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Update Vikunja task
|
- name: Update Vikunja task
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
env:
|
env:
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||||
- name: Notify on failure
|
# --- badges (always run — even on release commits) ---
|
||||||
if: failure()
|
- name: Generate and push badges
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/vikunja" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
|
|
||||||
configure-repo:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
|
||||||
run: make setup-image EXTRAS=ci
|
|
||||||
- name: Ensure branch protection and labels
|
|
||||||
env:
|
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_REPO_NAME: grm
|
|
||||||
DEVX_REPO_OWNER: oblachno-oss
|
|
||||||
DEVX_STATUS_CHECKS: "CI / quality (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.tools.configure_repo
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
# Fetch latest master to pick up any release commit that was pushed
|
||||||
|
git fetch origin master
|
||||||
|
git reset --hard origin/master
|
||||||
|
python3 -m devx.ci.push_badges
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--run-id "${{ github.run_id }}" \
|
--run-id "${{ github.run_id }}" \
|
||||||
--workflow "post-merge/configure-repo" \
|
--workflow "post-merge/release-and-maintain" \
|
||||||
--commit "${{ github.sha }}"
|
--commit "${{ github.sha }}"
|
||||||
|
|||||||
@@ -96,3 +96,43 @@ repos:
|
|||||||
types: [python]
|
types: [python]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
|
|
||||||
|
- id: check-ansible-no-log
|
||||||
|
name: ansible no_log on secret tasks
|
||||||
|
entry: make check-ansible-no-log
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-no-state-absent-on-db
|
||||||
|
name: no state absent on DB paths
|
||||||
|
entry: make check-ansible-no-state-absent-on-db
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-patterns
|
||||||
|
name: ansible failure-masking patterns
|
||||||
|
entry: make check-ansible-patterns
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-jinja-expr
|
||||||
|
name: jinja2 expression validation
|
||||||
|
entry: make check-jinja-expr
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml|j2)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-set-fact-to-json
|
||||||
|
name: set_fact to_json misuse check
|
||||||
|
entry: make check-ansible-set-fact-to-json
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't attribute human qualities to software or hardware ('%s')."
|
||||||
|
link: https://developers.google.com/style/anthropomorphism
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# Limited to the two verbs the guide itself names. Broader lists (wants, knows,
|
||||||
|
# thinks) can't tell a software subject from a human one: on a 950-file corpus
|
||||||
|
# they produced 8 false positives ('the customer wants', 'your audience knows')
|
||||||
|
# for every 2 real ones.
|
||||||
|
tokens:
|
||||||
|
- sees
|
||||||
|
- tells
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
extends: existence
|
extends: existence
|
||||||
message: "'%s' should be in lowercase."
|
message: "'%s' should be in lowercase."
|
||||||
link: 'https://developers.google.com/style/colons'
|
link: 'https://developers.google.com/style/colons'
|
||||||
nonword: true
|
|
||||||
level: warning
|
level: warning
|
||||||
scope: sentence
|
scope: sentence
|
||||||
|
# The match is the word itself, not ': X', and `nonword` is off. Both are
|
||||||
|
# required for a project Vocab to work: Vale compares accept.txt entries
|
||||||
|
# against the matched text, and `nonword: true` opts out of that entirely.
|
||||||
|
# So a proper noun after a colon can be exempted by adding it to accept.txt.
|
||||||
|
# The guide's other exemption, notice labels, is handled by the lookbehinds;
|
||||||
|
# headings are already excluded by `scope: sentence`. See issue #20.
|
||||||
tokens:
|
tokens:
|
||||||
- '(?<!:[^ ]+?):\s[A-Z]'
|
- '(?<!Note: )(?<!Caution: )(?<!Warning: )(?<!Success: )(?<=:\s)[A-Z]\w+'
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ level: error
|
|||||||
nonword: true
|
nonword: true
|
||||||
tokens:
|
tokens:
|
||||||
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
|
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
|
||||||
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)|May|Jun(?:e)|Jul(?:y)|Aug(?:ust)|Sep(?:tember)?|Oct(?:ober)|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
|
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the unverifiable claim '%s'."
|
||||||
|
link: https://developers.google.com/style/excessive-claims
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'never', 'always', and 'ensure', but in technical writing
|
||||||
|
# those are usually legitimate instructions ('never commit secrets') rather than
|
||||||
|
# product claims: they accounted for 125 of 142 hits on a 950-file corpus.
|
||||||
|
# 'best practices' is a fixed term, not a superlative.
|
||||||
|
tokens:
|
||||||
|
- 'best(?! practices?)'
|
||||||
|
- simplest
|
||||||
|
- fastest
|
||||||
|
- guarantees?
|
||||||
@@ -3,11 +3,13 @@ message: "Avoid first-person pronouns such as '%s'."
|
|||||||
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
||||||
ignorecase: true
|
ignorecase: true
|
||||||
level: warning
|
level: warning
|
||||||
nonword: true
|
# The 'I' tokens use lookaround rather than consuming the surrounding
|
||||||
|
# whitespace. Matching ' I ' made the alert span cover both spaces, which shows
|
||||||
|
# up as a too-wide underline in editors, and read as "such as ' I '". Dropping
|
||||||
|
# `nonword` also lets a project Vocab apply, which it can't when set. See PR #50.
|
||||||
tokens:
|
tokens:
|
||||||
- (?:^|\s)I\s
|
- '(?<=^|\s)I(?=[\s,])'
|
||||||
- (?:^|\s)I,\s
|
- "\\bI'm\\b"
|
||||||
- \bI'm\b
|
|
||||||
- \bme\b
|
- \bme\b
|
||||||
- \bmy\b
|
- \bmy\b
|
||||||
- \bmine\b
|
- \bmine\b
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ link: "https://developers.google.com/style/capitalization#capitalization-in-titl
|
|||||||
level: warning
|
level: warning
|
||||||
scope: heading
|
scope: heading
|
||||||
match: $sentence
|
match: $sentence
|
||||||
indicators:
|
# No `indicators: [":"]` here. That makes Vale require a capital after a colon,
|
||||||
- ":"
|
# which is the Microsoft convention this rule was originally copied from. This
|
||||||
|
# guide says the opposite: "the first word after a colon is generally
|
||||||
|
# lowercase" (developers.google.com/style/colons), and Colons.yml enforces
|
||||||
|
# exactly that. See issue #58.
|
||||||
exceptions:
|
exceptions:
|
||||||
- Azure
|
- Azure
|
||||||
- CLI
|
- CLI
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the jargon '%s'."
|
||||||
|
link: https://developers.google.com/style/jargon
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also cites 'solution', 'support', and 'workload' as overloaded
|
||||||
|
# terms, but those have ordinary technical meanings and accounted for every hit
|
||||||
|
# on a 950-file corpus, so only the unambiguous figurative terms are listed.
|
||||||
|
tokens:
|
||||||
|
- break-glass
|
||||||
|
- camel ?case
|
||||||
|
- out-of-the-box
|
||||||
|
- swim ?lane
|
||||||
@@ -6,6 +6,10 @@ level: error
|
|||||||
nonword: true
|
nonword: true
|
||||||
action:
|
action:
|
||||||
name: replace
|
name: replace
|
||||||
|
# The delimiter is a lookahead so the replacement doesn't swallow the comma or
|
||||||
|
# space that follows (issue #18). `$` is included so the abbreviation is still
|
||||||
|
# caught at the end of a heading, table cell, or block, which accounted for 8
|
||||||
|
# of 10 occurrences on a 950-file corpus.
|
||||||
swap:
|
swap:
|
||||||
'\b(?:eg|e\.g\.)(?=[\s,;])': for example
|
'\b(?:eg|e\.g\.)(?=[\s,;]|$)': for example
|
||||||
'\b(?:ie|i\.e\.)(?=[\s,;])': that is
|
'\b(?:ie|i\.e\.)(?=[\s,;]|$)': that is
|
||||||
|
|||||||
@@ -3,5 +3,26 @@ message: "Use the Oxford comma in '%s'."
|
|||||||
link: 'https://developers.google.com/style/commas'
|
link: 'https://developers.google.com/style/commas'
|
||||||
scope: sentence
|
scope: sentence
|
||||||
level: warning
|
level: warning
|
||||||
|
nonword: true
|
||||||
|
# List items may be several words long, not just one. Four guards keep the
|
||||||
|
# false-positive rate down:
|
||||||
|
#
|
||||||
|
# 1. The comma can't be the one closing a fronted subordinate clause
|
||||||
|
# ('When your alarm rings, you turn it off and tumble out of bed.') --
|
||||||
|
# that comma separates clauses, not list items. Only the first comma of
|
||||||
|
# such a sentence is exempt, so 'When it rains, apples, pears or bananas
|
||||||
|
# get wet.' is still caught.
|
||||||
|
# 2. The item can't open with a clause-introducer (', which ...',
|
||||||
|
# ', specifically ...').
|
||||||
|
# 3. The item can't open with a subject pronoun followed by a verb, which
|
||||||
|
# marks a compound predicate rather than a list ('..., you walk to the
|
||||||
|
# fridge and get a snack.'). A pronoun directly followed by 'and'/'or'
|
||||||
|
# is a real list item, so ', you and me.' still matches.
|
||||||
|
# 4. Neither item may contain an auxiliary verb, which is another compound
|
||||||
|
# predicate signal (', it has some downsides and is officially
|
||||||
|
# discouraged.').
|
||||||
|
#
|
||||||
|
# The trailing anchor allows end-of-scope so list fragments ('Apples, pears
|
||||||
|
# or bananas') are still caught.
|
||||||
tokens:
|
tokens:
|
||||||
- '(?:[^,]+,){1,}\s\w+\s(?:and|or)'
|
- '(?<!^(?i:when|whenever|while|if|unless|until|although|though|because|since|after|before|once|whereas|whether|as)\b[^,]{0,80}),\s(?!(?:which|who|whom|whose|that|where|when|while|because|since|although|though|if|unless|so|but|and|or|however|therefore|thus|specifically|especially|namely|then|take|see|note|consider|make|use|either|neither)\b)(?!(?i:i|you|we|they|he|she|it)\s+(?!(?:and|or)\b))(?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+ (?:and|or) (?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+(?:[.?!]|$)'
|
||||||
|
|||||||
@@ -3,5 +3,13 @@ message: "Use parentheses judiciously."
|
|||||||
link: 'https://developers.google.com/style/parentheses'
|
link: 'https://developers.google.com/style/parentheses'
|
||||||
nonword: true
|
nonword: true
|
||||||
level: suggestion
|
level: suggestion
|
||||||
|
# `[^)]` rather than `.+`: a greedy match ran from the first '(' on a line to
|
||||||
|
# the last ')', so 'Text (one) and more (two).' produced a single alert
|
||||||
|
# covering everything between them. See issue #30.
|
||||||
|
# A bare 3-5 letter acronym is skipped: Acronyms.yml requires acronyms to be
|
||||||
|
# defined as 'Spelled Out Term (ACRONYM)', so flagging those parentheses would
|
||||||
|
# put the two rules in direct conflict. The acronym has to be the whole
|
||||||
|
# parenthetical — '(NASA rocket program)' is an ordinary aside and still
|
||||||
|
# flags. Length matches the {3,5} in Acronyms.yml. See PR #59.
|
||||||
tokens:
|
tokens:
|
||||||
- '\(.+\)'
|
- '\((?![A-Z]{3,5}\))[^)]+\)'
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid time-based words like '%s' in product documentation."
|
||||||
|
link: https://developers.google.com/style/timeless-documentation
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'now' and 'new', but both have common senses that aren't
|
||||||
|
# time-anchored ('create a new project'): adding them took a 950-file corpus of
|
||||||
|
# technical documentation from 14 hits to 117. 'recently' is left out too — every
|
||||||
|
# hit in that corpus was the UI idiom 'recently used'.
|
||||||
|
tokens:
|
||||||
|
- currently
|
||||||
|
- latest
|
||||||
|
- soon
|
||||||
@@ -4,5 +4,7 @@ link: "https://developers.google.com/style/units-of-measure"
|
|||||||
nonword: true
|
nonword: true
|
||||||
level: error
|
level: error
|
||||||
tokens:
|
tokens:
|
||||||
- \b\d+(?:B|kB|MB|GB|TB)
|
- '\b\d+(?:B|kB|MB|GB|TB)\b'
|
||||||
- \b\d+(?:ns|ms|s|min|h|d)
|
- '\b\d+(?:ns|ms|min|h|d)\b'
|
||||||
|
# Seconds are split out so a decade ('1990s') isn't read as a unit.
|
||||||
|
- '\b\d+s\b(?<!\b(?:19|20)\d\ds\b)'
|
||||||
|
|||||||
@@ -2,79 +2,28 @@ extends: substitution
|
|||||||
message: "Use '%s' instead of '%s'."
|
message: "Use '%s' instead of '%s'."
|
||||||
link: "https://developers.google.com/style/word-list"
|
link: "https://developers.google.com/style/word-list"
|
||||||
level: warning
|
level: warning
|
||||||
|
# Case matters here: each key's own capitalization is what's being corrected,
|
||||||
|
# so ignorecase would make these match their own replacements. The rest of the
|
||||||
|
# word list lives in WordListCase.yml.
|
||||||
ignorecase: false
|
ignorecase: false
|
||||||
action:
|
action:
|
||||||
name: replace
|
name: replace
|
||||||
swap:
|
swap:
|
||||||
"(?:API Console|dev|developer) key": API key
|
|
||||||
"(?:cell ?phone|smart ?phone)": phone|mobile phone
|
|
||||||
"(?:dev|developer|APIs) console": API console
|
|
||||||
"(?:e-mail|Email|E-mail)": email
|
|
||||||
"(?:file ?path|path ?name)": path
|
|
||||||
"(?:kill|terminate|abort)": stop|exit|cancel|end
|
|
||||||
"(?:OAuth ?2|Oauth)": OAuth 2.0
|
|
||||||
"(?:ok|Okay)": OK|okay
|
|
||||||
"(?:WiFi|wifi)": Wi-Fi
|
|
||||||
'[\.]+apk': APK
|
|
||||||
'3\-D': 3D
|
|
||||||
'Google (?:I\-O|IO)': Google I/O
|
|
||||||
"tap (?:&|and) hold": touch & hold
|
|
||||||
"un(?:check|select)": clear
|
|
||||||
above: preceding
|
|
||||||
account name: username
|
|
||||||
action bar: app bar
|
|
||||||
admin: administrator
|
|
||||||
Ajax: AJAX
|
Ajax: AJAX
|
||||||
a\.k\.a|aka: or|also known as
|
|
||||||
Android device: Android-powered device
|
Android device: Android-powered device
|
||||||
android: Android
|
android: Android
|
||||||
API explorer: APIs Explorer
|
API explorer: APIs Explorer
|
||||||
application: app
|
|
||||||
approx\.: approximately
|
|
||||||
authN: authentication
|
authN: authentication
|
||||||
authZ: authorization
|
authZ: authorization
|
||||||
autoupdate: automatically update
|
|
||||||
cellular data: mobile data
|
|
||||||
cellular network: mobile network
|
|
||||||
chapter: documents|pages|sections
|
|
||||||
check box: checkbox
|
|
||||||
CLI: command-line tool
|
CLI: command-line tool
|
||||||
click on: click|click in
|
|
||||||
Cloud: Google Cloud Platform|GCP
|
Cloud: Google Cloud Platform|GCP
|
||||||
Container Engine: Kubernetes Engine
|
Container Engine: Kubernetes Engine
|
||||||
content type: media type
|
|
||||||
curated roles: predefined roles
|
|
||||||
data are: data is
|
|
||||||
Developers Console: Google API Console|API Console
|
Developers Console: Google API Console|API Console
|
||||||
disabled?: turn off|off
|
|
||||||
ephemeral IP address: ephemeral external IP address
|
|
||||||
fewer data: less data
|
|
||||||
file name: filename
|
|
||||||
firewalls: firewall rules
|
|
||||||
functionality: capability|feature
|
|
||||||
Google account: Google Account
|
Google account: Google Account
|
||||||
Google accounts: Google Accounts
|
Google accounts: Google Accounts
|
||||||
Googling: search with Google
|
Googling: search with Google
|
||||||
grayed-out: unavailable
|
|
||||||
HTTPs: HTTPS
|
HTTPs: HTTPS
|
||||||
in order to: to
|
|
||||||
ingest: import|load
|
|
||||||
k8s: Kubernetes
|
k8s: Kubernetes
|
||||||
long press: touch & hold
|
|
||||||
network IP address: internal IP address
|
|
||||||
omnibox: address bar
|
|
||||||
open-source: open source
|
|
||||||
overview screen: recents screen
|
|
||||||
regex: regular expression
|
|
||||||
SHA1: SHA-1|HAS-SHA1
|
SHA1: SHA-1|HAS-SHA1
|
||||||
sign into: sign in to
|
|
||||||
sign-?on: single sign-on
|
|
||||||
static IP address: static external IP address
|
|
||||||
stylesheet: style sheet
|
|
||||||
synch: sync
|
|
||||||
tablename: table name
|
|
||||||
tablet: device
|
|
||||||
touch: tap
|
|
||||||
url: URL
|
url: URL
|
||||||
vs\.: versus
|
|
||||||
World Wide Web: web
|
World Wide Web: web
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: "https://developers.google.com/style/word-list"
|
||||||
|
level: warning
|
||||||
|
# The case-insensitive half of the word list, so sentence-initial use is caught
|
||||||
|
# ('Touch the screen', not only 'touch the screen'). Entries that must stay
|
||||||
|
# case-sensitive are in WordList.yml.
|
||||||
|
ignorecase: true
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
"(?:API Console|dev|developer) key": API key
|
||||||
|
"(?:cell ?phone|smart ?phone)": phone|mobile phone
|
||||||
|
"(?:dev|developer|APIs) console": API console
|
||||||
|
"(?:e-mail|Email|E-mail)": email
|
||||||
|
"(?:file ?path|path ?name)": path
|
||||||
|
"(?:kill|terminate|abort)": stop|exit|cancel|end
|
||||||
|
# Longest form first: with the shortest alternative leading, 'OAuth 2' matched
|
||||||
|
# only 'OAuth', so applying the suggestion produced 'OAuth 2.0 2'. The rule is
|
||||||
|
# already case-insensitive, so the inline (?i) is redundant. See issue #41.
|
||||||
|
'\bOauth2\.0\b|\bOAuth ?2\b(?!\.0)|\bOauth\b(?! ?2)': OAuth 2.0
|
||||||
|
"(?:ok|Okay)": OK|okay
|
||||||
|
"(?:WiFi|wifi)": Wi-Fi
|
||||||
|
'[\.]+apk': APK
|
||||||
|
'3\-D': 3D
|
||||||
|
'Google (?:I\-O|IO)': Google I/O
|
||||||
|
"tap (?:&|and) hold": touch & hold
|
||||||
|
"un(?:check|select)": clear
|
||||||
|
above: preceding
|
||||||
|
account name: username
|
||||||
|
action bar: app bar
|
||||||
|
admin: administrator
|
||||||
|
a\.k\.a|aka: or|also known as
|
||||||
|
application: app
|
||||||
|
approx\.: approximately
|
||||||
|
autoupdate: automatically update
|
||||||
|
cellular data: mobile data
|
||||||
|
cellular network: mobile network
|
||||||
|
chapter: documents|pages|sections
|
||||||
|
check box: checkbox
|
||||||
|
click on: click|click in
|
||||||
|
content type: media type
|
||||||
|
curated roles: predefined roles
|
||||||
|
data are: data is
|
||||||
|
disabled?: turn off|off
|
||||||
|
ephemeral IP address: ephemeral external IP address
|
||||||
|
fewer data: less data
|
||||||
|
file name: filename
|
||||||
|
firewalls: firewall rules
|
||||||
|
functionality: capability|feature
|
||||||
|
grayed-out: unavailable
|
||||||
|
in order to: to
|
||||||
|
ingest: import|load
|
||||||
|
long press: touch & hold
|
||||||
|
network IP address: internal IP address
|
||||||
|
omnibox: address bar
|
||||||
|
open-source: open source
|
||||||
|
overview screen: recents screen
|
||||||
|
regex: regular expression
|
||||||
|
sign into: sign in to
|
||||||
|
'(?<!single )sign-?on': single sign-on
|
||||||
|
static IP address: static external IP address
|
||||||
|
stylesheet: style sheet
|
||||||
|
synch: sync
|
||||||
|
tablename: table name
|
||||||
|
tablet: device
|
||||||
|
'touch(?! ?(?:&|and) hold)': tap
|
||||||
|
vs\.: versus
|
||||||
@@ -51,13 +51,13 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
|
|||||||
|
|
||||||
Both run via `make workflow-check` and are part of `make lint-all`.
|
Both run via `make workflow-check` and are part of `make lint-all`.
|
||||||
The pre-commit hook runs actionlint automatically when workflow files change.
|
The pre-commit hook runs actionlint automatically when workflow files change.
|
||||||
The CI `quality` job runs `make setup` (which installs all tools) then `make lint-all`.
|
The CI `validate` job runs `make setup-image` (which installs all tools) then `make lint-all`.
|
||||||
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible
|
- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible
|
||||||
- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup with pasta networking (IPv6 support)
|
- **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role for rootless Docker runner setup with pasta networking (IPv6 support)
|
||||||
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications
|
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications
|
||||||
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
|
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
|
||||||
|
|
||||||
@@ -69,13 +69,13 @@ Every change to master goes through this workflow. No exceptions.
|
|||||||
|
|
||||||
Branch protection and labels are automatically configured by
|
Branch protection and labels are automatically configured by
|
||||||
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
|
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
|
||||||
which runs as a `configure-repo` job in
|
which runs as a step in the `detect-and-configure` job in
|
||||||
the post-merge workflow on every push to master.
|
the post-merge workflow on every push to master.
|
||||||
|
|
||||||
The following rules are enforced for `master`:
|
The following rules are enforced for `master`:
|
||||||
- **Require pull request**: No direct pushes to master
|
- **Require pull request**: No direct pushes to master
|
||||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||||
- **Require status checks**: CI quality + molecule tests must pass
|
- **Require status checks**: CI validate + molecule tests must pass
|
||||||
- **Block force pushes**: No history rewriting on master
|
- **Block force pushes**: No history rewriting on master
|
||||||
|
|
||||||
The auto-merge workflow enforces the APPROVE review check programmatically
|
The auto-merge workflow enforces the APPROVE review check programmatically
|
||||||
@@ -123,8 +123,9 @@ architecture, code quality, security, i18n, testing, performance,
|
|||||||
UX, documentation, workflow compliance, maintainability, resource
|
UX, documentation, workflow compliance, maintainability, resource
|
||||||
management, backwards compatibility, and logging.
|
management, backwards compatibility, and logging.
|
||||||
|
|
||||||
**Automated review (CI `pr-review` job):** Every PR triggers an automated
|
**Automated review (CI `validate` job):** Every PR triggers an automated
|
||||||
review via `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`). This job posts a review with
|
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
|
||||||
|
This posts a review with
|
||||||
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
|
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
|
||||||
the **[auto]** items in the checklist:
|
the **[auto]** items in the checklist:
|
||||||
|
|
||||||
@@ -178,10 +179,10 @@ rejected.
|
|||||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||||
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
|
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
|
||||||
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
|
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
|
||||||
3. Wait for all CI checks to pass (including the `pr-review` job)
|
3. Wait for all CI checks to pass (including the `validate` job)
|
||||||
4. Squash-merge with title: `GRM-N: <conventional commit message>`
|
4. Squash-merge with title: `GRM-N: <conventional commit message>`
|
||||||
5. The post-merge workflow marks the Vikunja task as done
|
5. The post-merge workflow marks the Vikunja task as done
|
||||||
6. The release workflow automatically versions, tags, and publishes (see below)
|
6. The release-and-maintain job automatically versions, tags, and publishes (see below)
|
||||||
|
|
||||||
**If the branch is behind master** (another PR merged first), auto-merge
|
**If the branch is behind master** (another PR merged first), auto-merge
|
||||||
automatically rebases the PR's head branch via the Gitea API. This triggers
|
automatically rebases the PR's head branch via the Gitea API. This triggers
|
||||||
@@ -197,21 +198,22 @@ No manual rebase needed. To rebase manually: `make rebase` (local) or
|
|||||||
|
|
||||||
### CI Path Filtering
|
### CI Path Filtering
|
||||||
|
|
||||||
The CI workflow includes a `pre-merge-check` job (runs after quality +
|
The CI workflow's `validate` job includes a pre-merge validation step
|
||||||
detect-changes) that validates branch format, PR title, and Vikunja task
|
that validates branch format, PR title, and Vikunja task match. This
|
||||||
match. This fails fast before expensive molecule tests run.
|
fails fast before expensive molecule tests run.
|
||||||
|
|
||||||
The CI workflow includes a `detect-changes` job that checks whether any files
|
The `validate` job also includes a `detect-changes` step that checks
|
||||||
under `ansible/` or `.ansible-lint` have changed. If no Ansible files are
|
whether any files under `ansible/` or `.ansible-lint` have changed. If
|
||||||
changed, molecule tests are skipped — this prevents non-Ansible changes
|
no Ansible files are changed, molecule tests are skipped — this prevents
|
||||||
(e.g., Python scripts, workflow YAML, docs) from being blocked by molecule
|
non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from
|
||||||
test infrastructure flakiness.
|
being blocked by molecule test infrastructure flakiness.
|
||||||
|
|
||||||
### Dynamic Runner Discovery
|
### Dynamic Runner Discovery
|
||||||
|
|
||||||
Molecule tests are distributed across available Gitea Actions runners
|
Molecule tests are distributed across available Gitea Actions runners
|
||||||
dynamically via `devx.molecule.discover_runners`. The `discover-runners`
|
dynamically via `devx.molecule.discover_runners`. The `validate` job
|
||||||
job queries the Gitea API for runners at all levels (repo, org, instance)
|
includes a `discover-runners` step (conditional on ansible-changed) that
|
||||||
|
queries the Gitea API for runners at all levels (repo, org, instance)
|
||||||
and generates a dynamic matrix. If the API can't see instance-level runners
|
and generates a dynamic matrix. If the API can't see instance-level runners
|
||||||
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
|
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
|
||||||
then to a default of 3.
|
then to a default of 3.
|
||||||
@@ -224,47 +226,27 @@ then to a default of 3.
|
|||||||
### Automated Release Pipeline
|
### Automated Release Pipeline
|
||||||
|
|
||||||
After a PR is merged to master, the **post-merge workflow**
|
After a PR is merged to master, the **post-merge workflow**
|
||||||
(`.gitea/workflows/post-merge.yml`) runs automatically. This single
|
(`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
|
||||||
workflow consolidates release, wiki sync, badge generation, and
|
into 2 jobs (from 7) to reduce runner overhead:
|
||||||
Vikunja task updates:
|
|
||||||
|
|
||||||
1. **detect-type** — Checks if the commit is a regular merge or a
|
1. **detect-and-configure** — Configures repo (branch protection, labels),
|
||||||
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
|
detects release commit, validates commit message. Outputs `is-release`
|
||||||
release commits (the `[skip ci]` tag also prevents re-triggering).
|
and `is-automated` for the next job.
|
||||||
|
|
||||||
2. **release** — Runs `devx.ci.release` which:
|
2. **release-and-maintain** — Runs all post-merge maintenance as
|
||||||
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only
|
conditional steps:
|
||||||
workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`,
|
- **release** (if not a release commit) — Runs `devx.ci.release` which
|
||||||
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version
|
checks for user-facing changes via `classify_changes` (skips if only
|
||||||
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes.
|
workflow/infrastructure files changed), uses git-cliff for semver,
|
||||||
- Uses **git-cliff** to calculate the next semver version from conventional commits
|
updates `__version__`, updates `CHANGELOG.md`, runs lint+tests, commits
|
||||||
- Updates `__version__` in `src/grm/__init__.py` (single source of truth)
|
with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes to master.
|
||||||
- Updates `CHANGELOG.md` with the new version section
|
- **publish** (if release created a tag) — Builds and publishes the
|
||||||
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
package to the Gitea PyPI registry. Checks out the release tag
|
||||||
- If lint or tests fail, **aborts immediately** — no commit, no tag
|
within the same job.
|
||||||
- Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents
|
- **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
|
||||||
re-triggering post-merge on the release commit)
|
- **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
|
||||||
- Creates an annotated tag `vX.Y.Z` on the release commit
|
- **badges** (always) — Generates and pushes quality badge SVGs to the
|
||||||
- Pushes both the commit and tag to master
|
`badges` branch. Fetches latest master first to pick up release commits.
|
||||||
- `--skip-tests` flag bypasses test verification (emergency use only, not recommended)
|
|
||||||
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
|
|
||||||
|
|
||||||
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
|
|
||||||
non-release commits (not only 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 only when release succeeds), so infrastructure-only
|
|
||||||
changes still update the task tracker.
|
|
||||||
|
|
||||||
6. **publish** — Runs after release succeeds (needs: release). Builds and
|
|
||||||
publishes the package to the Gitea PyPI registry. Gets the tag from the
|
|
||||||
release job's `tag` output.
|
|
||||||
|
|
||||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||||
|
|
||||||
@@ -296,9 +278,9 @@ via `[tool.devx.classify]` in `pyproject.toml`.
|
|||||||
- Any new file type not in the allowlist
|
- Any new file type not in the allowlist
|
||||||
|
|
||||||
**devx module structure** (installed from git, not in this repo):
|
**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.ci.*` — CI/CD automation (run by workflows): release, publish, auto_merge, classify_changes, detect_release_commit, push_badges, doc_coverage, sync_wiki, distribute_molecule, 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, create_task, create_pr, pr_status, pr_logs, pr_label, rebase, pr_rebase
|
- `devx.tools.*` — Dev tools (run locally): check_test_speed, configure_repo, install_checkmake, install_tools, setup, generate_badges, create_task, create_pr, pr_status, pr_logs, pr_label, rebase, pr_rebase
|
||||||
- `devx.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule, molecule_ci_guard
|
- `devx.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule
|
||||||
- `devx.gitea_cli` — Tea CLI wrapper
|
- `devx.gitea_cli` — Tea CLI wrapper
|
||||||
- `devx.i18n` — i18n translation system
|
- `devx.i18n` — i18n translation system
|
||||||
- `devx.config` — Shared configuration (DEVX_* env vars)
|
- `devx.config` — Shared configuration (DEVX_* env vars)
|
||||||
@@ -366,7 +348,7 @@ Since devx is installed as a package (via `pip install` from git), it is importa
|
|||||||
| PYTHONPATH | When to use | Example modules |
|
| PYTHONPATH | When to use | Example modules |
|
||||||
|------------|-------------|-----------------|
|
|------------|-------------|-----------------|
|
||||||
| `src` | Module imports from `grm` | `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` |
|
| `src` | Module imports from `grm` | `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` |
|
| (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` |
|
||||||
|
|
||||||
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
||||||
```yaml
|
```yaml
|
||||||
@@ -385,9 +367,9 @@ platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
|
|||||||
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
|
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
|
||||||
avoids dev tools importing directly from CI modules.
|
avoids dev tools importing directly from CI modules.
|
||||||
|
|
||||||
2. **Publish job** (in `post-merge.yml`, needs: release):
|
2. **Publish step** (in the `release-and-maintain` job, runs after the release step creates a tag):
|
||||||
- Runs after the release job creates a tag
|
- Runs after the release step creates a tag
|
||||||
- Gets the tag from `needs.release.outputs.tag`
|
- Gets the tag from the release step's output
|
||||||
- Builds the Python package
|
- Builds the Python package
|
||||||
- Publishes to the Gitea PyPI registry
|
- Publishes to the Gitea PyPI registry
|
||||||
- Creates a Gitea release with git-cliff-generated release notes
|
- Creates a Gitea release with git-cliff-generated release notes
|
||||||
@@ -493,6 +475,12 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
|
|||||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||||
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
||||||
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
|
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
|
||||||
|
- On Arch Linux, `rootless_docker.yml` fetches the rootless setup scripts
|
||||||
|
(`dockerd-rootless-setuptool.sh`, `dockerd-rootless.sh`) from `moby/moby` `contrib/`
|
||||||
|
at a pinned ref (`gitea_runner_rootless_scripts_ref`) into `/usr/bin` and installs
|
||||||
|
`rootlesskit` — Arch's `docker` package ships neither. These fetch tasks run
|
||||||
|
regardless of `docker_rootless_setup` so CI exercises them on the archlinux platform.
|
||||||
|
See ADR-011 in the decision log.
|
||||||
|
|
||||||
## Molecule Scenarios
|
## Molecule Scenarios
|
||||||
|
|
||||||
@@ -542,7 +530,7 @@ docs/
|
|||||||
### Documentation Coverage
|
### Documentation Coverage
|
||||||
|
|
||||||
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
|
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
|
||||||
- Runs as a CI step in the quality job with `--fail-on-missing` (blocks CI if docs are missing)
|
- Runs as a CI step in the validate job with `--fail-on-missing` (blocks CI if docs are missing)
|
||||||
- Enforced: 100% coverage for public CLI commands and major architectural components
|
- Enforced: 100% coverage for public CLI commands and major architectural components
|
||||||
|
|
||||||
### Updating Documentation
|
### Updating Documentation
|
||||||
@@ -572,7 +560,7 @@ the user should not need to specify which profile to use.
|
|||||||
|
|
||||||
| Profile | Purpose |
|
| Profile | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `ci-investigator` | Investigate CI failures (quality, molecule, release, publish, wiki sync) |
|
| `ci-investigator` | Investigate CI failures (validate, molecule-tests, release-and-maintain) |
|
||||||
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
|
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
|
||||||
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
|
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
|
||||||
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs |
|
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync for grm docs |
|
||||||
@@ -582,7 +570,7 @@ the user should not need to specify which profile to use.
|
|||||||
|
|
||||||
| Trigger | Profile | Mode |
|
| Trigger | Profile | Mode |
|
||||||
|---------|---------|------|
|
|---------|---------|------|
|
||||||
| CI run failure (quality, molecule-tests, release, publish, sync-wiki) | `ci-investigator` | Background |
|
| CI run failure (validate, molecule-tests, release-and-maintain) | `ci-investigator` | Background |
|
||||||
| PR ready for review | `pr-reviewer` | Foreground |
|
| PR ready for review | `pr-reviewer` | Foreground |
|
||||||
| Molecule tests need to run | `molecule-runner` | Background |
|
| Molecule tests need to run | `molecule-runner` | Background |
|
||||||
| Dependency upgrade requested | `dep-upgrader` | Background |
|
| Dependency upgrade requested | `dep-upgrader` | Background |
|
||||||
@@ -608,8 +596,9 @@ tool, workflow, or process issues that warrant follow-up. These issues
|
|||||||
use the `feedback` label plus a category label (`tooling`,
|
use the `feedback` label plus a category label (`tooling`,
|
||||||
`ci-improvement`, `doc-improvement`, `workflow-improvement`).
|
`ci-improvement`, `doc-improvement`, `workflow-improvement`).
|
||||||
|
|
||||||
Standard labels are created automatically by `configure_repo` (runs in
|
Standard labels are created automatically by `configure_repo` (runs as
|
||||||
post-merge on every master push). If a label does not exist yet, the
|
a step in `detect-and-configure` in post-merge on every master push).
|
||||||
|
If a label does not exist yet, the
|
||||||
subagent's issue creation will still succeed — labels can be added
|
subagent's issue creation will still succeed — labels can be added
|
||||||
afterwards.
|
afterwards.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,73 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.21.0] - 2026-08-09
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(healthcheck)* Add two-tier disk prune with critical threshold
|
||||||
|
|
||||||
|
## [0.20.0] - 2026-08-09
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(healthcheck)* Add two-tier disk prune with critical threshold
|
||||||
|
|
||||||
|
## [0.19.0] - 2026-08-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Use Gitea mirror for Ansible collection installs
|
||||||
|
|
||||||
|
## [0.18.8] - 2026-08-06
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pin containerd.io to compatible version for Docker 28.x
|
||||||
|
|
||||||
|
|
||||||
|
## [0.18.7] - 2026-08-06
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Move StartLimit to [Unit] and make prune timer reload conditional
|
||||||
|
|
||||||
|
## [0.18.6] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pre-configure daemon.json before rootless setuptool + add DBUS_SESSION_BUS_ADDRESS
|
||||||
|
|
||||||
|
## [0.18.5] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pin Docker 28.x + disable containerd snapshotter + tune prune/disk
|
||||||
|
|
||||||
|
## [0.18.4] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Harden rootless Docker daemon resilience on CI runners
|
||||||
|
|
||||||
|
## [0.18.3] - 2026-08-04
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Switch default network driver to slirp4netns (pasta TCP RST bug)
|
||||||
|
|
||||||
|
## [0.18.2] - 2026-07-16
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Load tun module and pre-configure systemd override for Arch rootless Docker
|
||||||
|
|
||||||
|
## [0.18.1] - 2026-07-16
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fetch rootless Docker scripts on Arch Linux
|
||||||
|
|
||||||
## [0.18.0] - 2026-07-12
|
## [0.18.0] - 2026-07-12
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -50,13 +50,14 @@ setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
|
|||||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
|
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
|
||||||
|
|
||||||
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
|
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
|
||||||
# (detect-changes, discover-runners, pr-review, sync-wiki, badges)
|
# (validate job steps: detect-changes, discover-runners, pr-review;
|
||||||
# badges job runs generate_badges.py which needs ruff, pyright, bandit
|
# release-and-maintain job steps: sync-wiki, badges)
|
||||||
|
# badges step runs generate_badges.py which needs ruff, pyright, bandit
|
||||||
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
|
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||||
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
|
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
|
||||||
|
|
||||||
# Setup for the quality job (lint + test deps, actionlint tool)
|
# Setup for the validate CI job (lint + test deps, actionlint tool)
|
||||||
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
|
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||||
@$(BIN)/python -m devx.tools.install_tools
|
@$(BIN)/python -m devx.tools.install_tools
|
||||||
@@ -87,7 +88,7 @@ setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
|
|||||||
setup-image:
|
setup-image:
|
||||||
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
|
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
|
||||||
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
|
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
|
||||||
if [ -n "$$_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \
|
if [ -n "$$_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; git config --global url."https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/".insteadOf "https://git.oblachno.oblachno.fyi/"; fi; \
|
||||||
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
||||||
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||||
|
|
||||||
@@ -174,16 +175,41 @@ makefile-lint:
|
|||||||
echo "checkmake not found, skipping Makefile lint"; \
|
echo "checkmake not found, skipping Makefile lint"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
lint-all: lint ansible-lint makefile-lint workflow-lint check-api-identity-checks
|
lint-all: lint ansible-lint makefile-lint workflow-lint check-api-identity-checks check-ansible-no-log check-ansible-no-state-absent-on-db check-ansible-patterns check-jinja-expr check-ansible-set-fact-to-json
|
||||||
|
|
||||||
check-api-identity-checks:
|
check-api-identity-checks:
|
||||||
@$(BIN)/python -m devx.tools.check_api_identity_checks
|
@$(BIN)/python -m devx.tools.check_api_identity_checks
|
||||||
|
|
||||||
|
check-ansible-no-log:
|
||||||
|
@echo "[check-ansible-no-log] Checking Ansible tasks for missing no_log on secret-handling tasks..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_no_log
|
||||||
|
@echo "[check-ansible-no-log] Passed."
|
||||||
|
|
||||||
|
check-ansible-no-state-absent-on-db:
|
||||||
|
@echo "[check-ansible-no-state-absent-on-db] Checking for state: absent on DB data directories..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_no_state_absent_on_db
|
||||||
|
@echo "[check-ansible-no-state-absent-on-db] Passed."
|
||||||
|
|
||||||
|
check-ansible-patterns:
|
||||||
|
@echo "[check-ansible-patterns] Checking for dangerous failure-masking patterns..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_patterns
|
||||||
|
@echo "[check-ansible-patterns] Passed."
|
||||||
|
|
||||||
|
check-jinja-expr:
|
||||||
|
@echo "[check-jinja-expr] Validating Jinja2 expressions in Ansible files..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_jinja_expr
|
||||||
|
@echo "[check-jinja-expr] Passed."
|
||||||
|
|
||||||
|
check-ansible-set-fact-to-json:
|
||||||
|
@echo "[check-ansible-set-fact-to-json] Checking set_fact tasks for to_json misuse..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_set_fact_to_json
|
||||||
|
@echo "[check-ansible-set-fact-to-json] Passed."
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
$(BIN)/pytest tests/integration/ -v --no-cov
|
$(BIN)/pytest tests/integration/ -v --no-cov
|
||||||
|
|
||||||
MOLECULE := $(realpath $(BIN))/molecule
|
MOLECULE := $(realpath $(BIN))/molecule
|
||||||
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
|
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea_runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
|
||||||
|
|
||||||
# Quick local test: Ubuntu 22.04 only, all scenarios
|
# Quick local test: Ubuntu 22.04 only, all scenarios
|
||||||
molecule:
|
molecule:
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
[](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/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/wiki)
|
||||||
[](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/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Why GRM?
|
## Why GRM?
|
||||||
|
|
||||||
@@ -266,8 +266,8 @@ One of GRM's core features is the ability to run multiple isolated runners on th
|
|||||||
|
|
||||||
- **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/`
|
- **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/`
|
||||||
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
|
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
|
||||||
- **Data directory**: `/var/lib/gitea-runner/<name>/`
|
- **Data directory**: `/var/lib/gitea_runner/<name>/`
|
||||||
- **Config directory**: `/etc/gitea-runner/<name>/`
|
- **Config directory**: `/etc/gitea_runner/<name>/`
|
||||||
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
||||||
- **Docker prune timer**: Per-instance daily cleanup
|
- **Docker prune timer**: Per-instance daily cleanup
|
||||||
|
|
||||||
@@ -341,13 +341,13 @@ GRM consists of two layers:
|
|||||||
|
|
||||||
1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list.
|
1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list.
|
||||||
|
|
||||||
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer.
|
2. **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
grm install <host>
|
grm install <host>
|
||||||
└── RunnerManager.install()
|
└── RunnerManager.install()
|
||||||
└── ansible-playbook ansible/install-runner.yml
|
└── ansible-playbook ansible/install-runner.yml
|
||||||
└── role: gitea-runner
|
└── role: gitea_runner
|
||||||
├── user_setup.yml (create per-runner system user + lingering)
|
├── user_setup.yml (create per-runner system user + lingering)
|
||||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||||
├── install_runner.yml (download binary, config, register, service)
|
├── install_runner.yml (download binary, config, register, service)
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- name: Stop and disable healthcheck timer
|
- name: Stop and disable healthcheck timer
|
||||||
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
@@ -30,14 +30,14 @@
|
|||||||
|
|
||||||
- name: Include deregistration
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not gitea_runner_skip_registration | default(false)
|
||||||
|
|
||||||
- name: Disable gitea-runner user service
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Enable gitea-runner user service
|
- name: Enable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user enable gitea-runner
|
ansible.builtin.command: systemctl --user enable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- name: Start gitea-runner user service
|
- name: Start gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user start gitea-runner
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea_runner
|
||||||
|
|||||||
+34
-34
@@ -6,11 +6,11 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Get runner user UID
|
- name: Get runner user UID
|
||||||
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
register: runner_uid_result
|
register: runner_uid_result
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
@@ -23,20 +23,20 @@
|
|||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Disable gitea-runner user service
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
args:
|
args:
|
||||||
executable: /bin/bash
|
executable: /bin/bash
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
- name: Prune all Docker images, volumes, and build cache (rootless)
|
- name: Prune all Docker images, volumes, and build cache (rootless)
|
||||||
ansible.builtin.command: docker system prune -af --volumes
|
ansible.builtin.command: docker system prune -af --volumes
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
- name: Stop rootless Docker daemon
|
- name: Stop rootless Docker daemon
|
||||||
ansible.builtin.command: systemctl --user stop docker
|
ansible.builtin.command: systemctl --user stop docker
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
@@ -75,120 +75,120 @@
|
|||||||
|
|
||||||
- name: Include deregistration
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not gitea_runner_skip_registration | default(false)
|
||||||
|
|
||||||
- name: Stop and disable healthcheck timer
|
- name: Stop and disable healthcheck timer
|
||||||
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove docker-prune user service file
|
- name: Remove docker-prune user service file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.service"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove docker-prune user timer file
|
- name: Remove docker-prune user timer file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.timer"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.timer"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove healthcheck user service file
|
- name: Remove healthcheck user service file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/runner-healthcheck.service"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove healthcheck user timer file
|
- name: Remove healthcheck user timer file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.timer"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/runner-healthcheck.timer"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove healthcheck script
|
- name: Remove healthcheck script
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}/healthcheck.sh"
|
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ gitea_runner_name) }}/healthcheck.sh"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove systemd user unit file
|
- name: Remove systemd user unit file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/gitea-runner.service"
|
||||||
state: absent
|
state: absent
|
||||||
when: remove_systemd_template | default(true)
|
when: remove_systemd_template | default(true)
|
||||||
|
|
||||||
- name: Kill remaining processes of runner user
|
- name: Kill remaining processes of runner user
|
||||||
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Wait for processes to terminate
|
- name: Wait for processes to terminate
|
||||||
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Disable lingering for runner user
|
- name: Disable lingering for runner user
|
||||||
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Remove runner user and home directory
|
- name: Remove runner user and home directory
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
name: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
remove: true
|
remove: true
|
||||||
when: remove_runner_user | default(true)
|
when: gitea_runner_remove_user | default(true)
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove Docker data root when user is kept
|
- name: Remove Docker data root when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.local/share/docker"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.local/share/docker"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove act cache when user is kept
|
- name: Remove act cache when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.cache/act"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.cache/act"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove systemd user config dir when user is kept
|
- name: Remove systemd user config dir when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove subuid entry for runner user
|
- name: Remove subuid entry for runner user
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/subuid
|
path: /etc/subuid
|
||||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove subgid entry for runner user
|
- name: Remove subgid entry for runner user
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/subgid
|
path: /etc/subgid
|
||||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove runner data directory
|
- name: Remove runner data directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}"
|
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Remove runner config directory
|
- name: Remove runner config directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}"
|
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
---
|
||||||
collections:
|
collections:
|
||||||
- name: community.general
|
- name: community.general
|
||||||
version: "==13.1.0"
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/13.1.0/community-general-13.1.0.tar.gz
|
||||||
- name: ansible.posix
|
- name: ansible.posix
|
||||||
version: "==2.2.0"
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/2.2.1/ansible-posix-2.2.1.tar.gz
|
||||||
- name: community.docker
|
- name: community.docker
|
||||||
version: "==5.2.1"
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/5.2.1/community-docker-5.2.1.tar.gz
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Resolve runner UID
|
- name: Resolve runner UID
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: resolve_uid.yml
|
tasks_from: resolve_uid.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
gitea_runner_version: "1.0.8"
|
|
||||||
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-26.04"
|
|
||||||
skip_runner_registration: false
|
|
||||||
|
|
||||||
# Per-runner user (rootless isolation)
|
|
||||||
gitea_runner_user_prefix: "grm-"
|
|
||||||
gitea_runner_base_home: "/home"
|
|
||||||
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ runner_name }}"
|
|
||||||
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
|
|
||||||
|
|
||||||
# Base paths (instance-scoped via runner_name)
|
|
||||||
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
|
||||||
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
|
||||||
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
|
|
||||||
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
|
|
||||||
gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
|
|
||||||
|
|
||||||
# Prune configuration
|
|
||||||
gitea_runner_prune_until: "24h"
|
|
||||||
gitea_runner_prune_schedule: "daily"
|
|
||||||
gitea_runner_prune_label: "gitea-runner=true"
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
gitea_runner_service_restart_sec: "5"
|
|
||||||
|
|
||||||
# Health check configuration
|
|
||||||
gitea_runner_healthcheck_interval: "5min"
|
|
||||||
gitea_runner_healthcheck_boot_delay: "2min"
|
|
||||||
gitea_runner_healthcheck_disk_threshold: 85
|
|
||||||
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
|
|
||||||
|
|
||||||
# Admin token for runner deregistration via Gitea API.
|
|
||||||
# If not set, falls back to registration_token (which likely lacks admin scope).
|
|
||||||
# Set this to a token with admin scope to enable automatic runner cleanup on removal.
|
|
||||||
gitea_admin_token: ""
|
|
||||||
|
|
||||||
# Removal defaults
|
|
||||||
remove_systemd_template: true
|
|
||||||
remove_runner_user: true
|
|
||||||
|
|
||||||
# Runner configuration
|
|
||||||
gitea_runner_log_level: "info"
|
|
||||||
gitea_runner_container_label: "gitea-runner=true"
|
|
||||||
gitea_runner_file: ".runner"
|
|
||||||
|
|
||||||
# Docker installation (for rootless dependencies)
|
|
||||||
docker_gpg_key_path: "/etc/apt/keyrings/docker.gpg"
|
|
||||||
docker_apt_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
|
|
||||||
docker_apt_source_line: >-
|
|
||||||
deb [arch={{ docker_apt_arch }} signed-by={{ docker_gpg_key_path }}]
|
|
||||||
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
|
|
||||||
{{ ansible_facts['distribution_release'] }} stable
|
|
||||||
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
|
|
||||||
docker_rootless_setup: true
|
|
||||||
|
|
||||||
# Rootless Docker network driver: "pasta" (IPv6 support) or "slirp4netns" (IPv4 only)
|
|
||||||
# pasta has proper outgoing IPv6 support; slirp4netns does not (known limitation).
|
|
||||||
docker_rootless_net_driver: "pasta"
|
|
||||||
|
|
||||||
# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet)
|
|
||||||
docker_ipv6_cidr: "fd00:dead:beef::/48"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "molecule-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "deregister-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "lifecycle-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "remove-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "template-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "update-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if runner registration file exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_stat
|
|
||||||
|
|
||||||
- name: Read runner registration file
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_content
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Parse runner registration data
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_reg: >
|
|
||||||
{{ (runner_file_content.content | b64decode | from_json)
|
|
||||||
if (runner_file_content is defined and runner_file_content.content is defined)
|
|
||||||
else {} }}
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Deregister runner from Gitea via API
|
|
||||||
ansible.builtin.command: >
|
|
||||||
curl -sf --connect-timeout 5 --max-time 10 -X DELETE
|
|
||||||
-H "Authorization: token {{ gitea_admin_token | default(registration_token) }}"
|
|
||||||
"{{ gitea_url }}/api/v1/admin/actions/runners/{{ runner_reg.id }}"
|
|
||||||
args:
|
|
||||||
chdir: "{{ gitea_runner_data_dir }}"
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
|
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
|
|
||||||
when:
|
|
||||||
- runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
- not skip_runner_registration
|
|
||||||
- runner_reg.id is defined
|
|
||||||
register: deregister_output
|
|
||||||
changed_when: deregister_output.rc == 0
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Warn if deregistration failed
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >-
|
|
||||||
WARNING: Runner deregistration from Gitea failed (rc={{ deregister_output.rc | default('N/A') }}).
|
|
||||||
The runner entry may remain in Gitea's admin UI as offline.
|
|
||||||
Use an admin token (gitea_admin_token var) to enable automatic cleanup,
|
|
||||||
or remove it manually from {{ gitea_url }}/-/admin/actions/runners
|
|
||||||
when:
|
|
||||||
- runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
- not skip_runner_registration
|
|
||||||
- deregister_output is defined
|
|
||||||
- deregister_output.rc | default(1) != 0
|
|
||||||
|
|
||||||
- name: Remove runner registration file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
state: absent
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check runner registration file exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_stat
|
|
||||||
|
|
||||||
- name: Read runner registration file
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_content
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Parse runner registration data
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_reg: >
|
|
||||||
{{ (runner_file_content.content | b64decode | from_json)
|
|
||||||
if (runner_file_content is defined and runner_file_content.content is defined)
|
|
||||||
else {} }}
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Verify runner user service active
|
|
||||||
ansible.builtin.command: systemctl --user is-active gitea-runner
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
register: service_check
|
|
||||||
changed_when: false
|
|
||||||
when:
|
|
||||||
- systemd_available.stat.exists
|
|
||||||
- docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Validate runner installation
|
|
||||||
ansible.builtin.fail:
|
|
||||||
msg: >
|
|
||||||
Runner '{{ runner_name }}' is not properly installed:
|
|
||||||
{% if not (runner_file_stat.stat.exists | default(false)) %}
|
|
||||||
- Registration file (.runner) is missing. Registration may have failed.
|
|
||||||
{% endif %}
|
|
||||||
{% if docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active' %}
|
|
||||||
- Systemd user service is not active.
|
|
||||||
{% endif %}
|
|
||||||
when: >
|
|
||||||
not (runner_file_stat.stat.exists | default(false))
|
|
||||||
or (docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active')
|
|
||||||
|
|
||||||
- name: Report runner status
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >
|
|
||||||
Runner '{{ runner_name }}' is installed and running.
|
|
||||||
Registered: {{ runner_file_stat.stat.exists | default(false) }}
|
|
||||||
{% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %}
|
|
||||||
{% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %}
|
|
||||||
{% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %}
|
|
||||||
Service: {{ service_check.stdout | default('unknown') | trim }}
|
|
||||||
|
|
||||||
- name: Optional Gitea API verification
|
|
||||||
when:
|
|
||||||
- gitea_url is defined
|
|
||||||
- gitea_admin_token is defined
|
|
||||||
- gitea_admin_token | length > 0
|
|
||||||
block:
|
|
||||||
- name: Check admin runners API
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ gitea_url }}/api/v1/admin/runners"
|
|
||||||
headers:
|
|
||||||
Authorization: "token {{ gitea_admin_token }}"
|
|
||||||
method: GET
|
|
||||||
status_code: [200, 401, 403, 404]
|
|
||||||
return_content: true
|
|
||||||
body_format: json
|
|
||||||
register: admin_api_response
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Check repo runners API
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ gitea_url }}/api/v1/repos/{{ gitea_runner_test_repo | default('oblachno-oss/grm') }}/actions/runners"
|
|
||||||
headers:
|
|
||||||
Authorization: "token {{ gitea_admin_token }}"
|
|
||||||
method: GET
|
|
||||||
status_code: [200, 401, 403, 404]
|
|
||||||
return_content: true
|
|
||||||
body_format: json
|
|
||||||
register: repo_api_response
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Report API status (informational only)
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >
|
|
||||||
API checks (informational only — not used for pass/fail):
|
|
||||||
Admin API: {{ admin_api_response.status | default('no response') }}.
|
|
||||||
Repo API: {{ repo_api_response.status | default('no response') }}.
|
|
||||||
{% if admin_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
|
|
||||||
Runner found in admin API.
|
|
||||||
{% endif %}
|
|
||||||
{% if repo_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
|
|
||||||
Runner found in repo API.
|
|
||||||
{% endif %}
|
|
||||||
rescue:
|
|
||||||
- name: API check failed
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "API verification skipped due to connection or permission error."
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Ensure work 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: Check if runner is already registered
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_registered
|
|
||||||
|
|
||||||
- name: Register runner with Gitea
|
|
||||||
ansible.builtin.command: >
|
|
||||||
{{ gitea_runner_binary_path }} register
|
|
||||||
--token {{ registration_token }}
|
|
||||||
--name {{ runner_name }}
|
|
||||||
--instance {{ gitea_url }}
|
|
||||||
--labels {{ runner_labels }}
|
|
||||||
--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: not runner_registered.stat.exists
|
|
||||||
register: register_output
|
|
||||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
|
||||||
timeout: 60
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Ensure keyrings directory exists (Debian/Ubuntu)
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "/etc/apt/keyrings"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -o pipefail
|
|
||||||
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" | gpg --dearmor --yes -o {{ docker_gpg_key_path }}
|
|
||||||
args:
|
|
||||||
creates: "{{ docker_gpg_key_path }}"
|
|
||||||
executable: /bin/bash
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Add Docker APT repository (Debian/Ubuntu)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/apt/sources.list.d/docker.list
|
|
||||||
content: "{{ docker_apt_source_line }}\n"
|
|
||||||
mode: "0644"
|
|
||||||
register: docker_apt_repo
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
|
|
||||||
ansible.builtin.apt:
|
|
||||||
update_cache: true
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == 'Debian'
|
|
||||||
- docker_apt_repo is changed
|
|
||||||
|
|
||||||
- name: Install rootless Docker dependencies (Debian/Ubuntu)
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- uidmap
|
|
||||||
- slirp4netns
|
|
||||||
- passt
|
|
||||||
- fuse-overlayfs
|
|
||||||
- docker-ce
|
|
||||||
- docker-ce-cli
|
|
||||||
- docker-ce-rootless-extras
|
|
||||||
- containerd.io
|
|
||||||
- docker-compose-plugin
|
|
||||||
- rsync
|
|
||||||
state: present
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Update pacman cache (Arch Linux)
|
|
||||||
community.general.pacman:
|
|
||||||
update_cache: true
|
|
||||||
when: ansible_facts['os_family'] == 'Archlinux'
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Install rootless Docker dependencies (Arch Linux)
|
|
||||||
community.general.pacman:
|
|
||||||
name:
|
|
||||||
- docker
|
|
||||||
- docker-compose
|
|
||||||
- slirp4netns
|
|
||||||
- passt
|
|
||||||
- fuse-overlayfs
|
|
||||||
- rsync
|
|
||||||
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: Ensure Docker config directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_home }}/.config/docker"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Ensure systemd user override directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Configure rootless Docker to use pasta with IPv6
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
|
|
||||||
content: |
|
|
||||||
[Service]
|
|
||||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ docker_rootless_net_driver }}"
|
|
||||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
|
|
||||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
|
||||||
mode: "0644"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
register: docker_network_override
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Reload systemd user daemon if network config changed
|
|
||||||
ansible.builtin.command: systemctl --user daemon-reload
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
changed_when: true
|
|
||||||
when:
|
|
||||||
- docker_rootless_setup
|
|
||||||
- docker_network_override is changed
|
|
||||||
|
|
||||||
- name: Configure rootless Docker daemon with IPv6 enabled
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
|
|
||||||
content: |
|
|
||||||
{
|
|
||||||
"ipv6": true,
|
|
||||||
"ip6tables": true,
|
|
||||||
"fixed-cidr-v6": "{{ docker_ipv6_cidr }}"
|
|
||||||
}
|
|
||||||
mode: "0644"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
register: docker_ipv6_config
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Restart rootless Docker if config changed
|
|
||||||
ansible.builtin.command: systemctl --user restart docker
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
changed_when: true
|
|
||||||
when:
|
|
||||||
- docker_rootless_setup
|
|
||||||
- docker_ipv6_config is changed or docker_network_override is changed
|
|
||||||
|
|
||||||
- name: Wait for rootless Docker daemon to be ready
|
|
||||||
ansible.builtin.command: docker version
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
register: docker_ready
|
|
||||||
until: docker_ready.rc == 0
|
|
||||||
retries: 10
|
|
||||||
delay: 2
|
|
||||||
changed_when: false
|
|
||||||
when: docker_rootless_setup
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create systemd user service file
|
|
||||||
ansible.builtin.template:
|
|
||||||
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"
|
|
||||||
|
|
||||||
- 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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Docker prune for Gitea runner resources
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
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 }}"
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Health check for gitea-runner: verifies Docker daemon and runner service.
|
|
||||||
# Exits 0 if healthy, 1 if Docker is down (triggers restart), 2 if runner is down.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DOCKER_HOST="unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
|
||||||
XDG_RUNTIME_DIR="/run/user/{{ gitea_runner_uid }}"
|
|
||||||
export DOCKER_HOST XDG_RUNTIME_DIR
|
|
||||||
|
|
||||||
# 1. Check Docker daemon responsiveness
|
|
||||||
if ! docker info >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: Docker daemon not responding at ${DOCKER_HOST}"
|
|
||||||
systemctl --user restart docker.service
|
|
||||||
sleep 3
|
|
||||||
if ! docker info >/dev/null 2>&1; then
|
|
||||||
echo "CRITICAL: Docker daemon still down after restart"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "RECOVERED: Docker daemon restarted successfully"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Check gitea-runner service is active
|
|
||||||
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
|
||||||
if [[ "$runner_state" != "active" ]]; then
|
|
||||||
echo "ERROR: gitea-runner service is ${runner_state}, restarting"
|
|
||||||
systemctl --user restart gitea-runner.service
|
|
||||||
sleep 2
|
|
||||||
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
|
||||||
if [[ "$runner_state" != "active" ]]; then
|
|
||||||
echo "CRITICAL: gitea-runner service still down after restart"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
echo "RECOVERED: gitea-runner service restarted successfully"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Check disk space — prune aggressively if below threshold
|
|
||||||
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
|
||||||
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
|
|
||||||
echo "WARN: Disk usage at ${disk_pct}%, pruning all runner resources"
|
|
||||||
docker system prune -af --filter "label={{ gitea_runner_prune_label }}" --filter "until=1h" || true
|
|
||||||
docker volume prune -af --filter "label={{ gitea_runner_prune_label }}" || true
|
|
||||||
# Also prune dangling images (no label)
|
|
||||||
docker image prune -af || true
|
|
||||||
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
|
||||||
echo "INFO: Disk usage after prune: ${disk_pct}%"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "OK: runner healthy, disk at ${disk_pct}%"
|
|
||||||
exit 0
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
gitea_runner_version: "2.0.1"
|
||||||
|
gitea_runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-26.04"
|
||||||
|
gitea_runner_skip_registration: false
|
||||||
|
|
||||||
|
# Force re-registration even if .runner file exists.
|
||||||
|
# Use this when Gitea no longer recognizes the runner (e.g., after a Gitea
|
||||||
|
# server restore/reinstall or when the runner record was deleted from the
|
||||||
|
# admin UI). The existing .runner file is removed and a new registration is
|
||||||
|
# performed. Requires registration_token.
|
||||||
|
gitea_runner_force_reregister: false
|
||||||
|
|
||||||
|
# Per-runner user (rootless isolation)
|
||||||
|
gitea_runner_user_prefix: "grm-"
|
||||||
|
gitea_runner_base_home: "/home"
|
||||||
|
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ gitea_runner_name }}"
|
||||||
|
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
|
||||||
|
|
||||||
|
# Base paths (instance-scoped via gitea_runner_name)
|
||||||
|
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
||||||
|
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
||||||
|
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ gitea_runner_name }}"
|
||||||
|
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ gitea_runner_name }}"
|
||||||
|
gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
|
||||||
|
|
||||||
|
# Prune configuration
|
||||||
|
gitea_runner_prune_until: "24h"
|
||||||
|
# Every 6 hours — daily is insufficient for CI runners that build dozens
|
||||||
|
# of images per day. Accumulation between daily runs can trigger Docker
|
||||||
|
# daemon instability (containerd snapshotter GC holds locks, blocking
|
||||||
|
# container operations).
|
||||||
|
gitea_runner_prune_schedule: "*-*-* 00/6:00:00"
|
||||||
|
gitea_runner_prune_label: "gitea-runner=true"
|
||||||
|
|
||||||
|
# Service configuration
|
||||||
|
gitea_runner_service_restart_sec: "5"
|
||||||
|
|
||||||
|
# Health check configuration
|
||||||
|
# 2min interval — catches hung daemons before multiple CI jobs fail between checks.
|
||||||
|
# The previous 5min interval was too coarse: a stuck daemon could fail 3+ molecule
|
||||||
|
# jobs in the window between healthcheck runs.
|
||||||
|
gitea_runner_healthcheck_interval: "2min"
|
||||||
|
gitea_runner_healthcheck_boot_delay: "2min"
|
||||||
|
gitea_runner_healthcheck_disk_threshold: 70
|
||||||
|
# When disk reaches this level, prune EVERYTHING (no until-filter) — the
|
||||||
|
# runner is dangerously full and the gentle until=1h prune isn't enough.
|
||||||
|
# This removes all stopped containers and unused images regardless of age.
|
||||||
|
# At 75%+, molecule containers fail with "container is not running" because
|
||||||
|
# overlay2 runs out of space under parallel DinD load.
|
||||||
|
gitea_runner_healthcheck_disk_critical: 75
|
||||||
|
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
|
||||||
|
|
||||||
|
# Auto-recovery: when the healthcheck detects an unregistered runner, it
|
||||||
|
# can automatically re-register if a Gitea API token is provided.
|
||||||
|
# The token needs admin or org-level access to fetch registration tokens.
|
||||||
|
# Stored in a file readable by the runner user (mode 0400).
|
||||||
|
# Set to empty string to disable auto-recovery (manual re-registration required).
|
||||||
|
gitea_runner_auto_recover_api_token: ""
|
||||||
|
|
||||||
|
# Cooldown file to prevent auto-recovery loops (e.g., if Gitea is down).
|
||||||
|
# The healthcheck writes a timestamp to this file after a re-registration
|
||||||
|
# attempt and skips further attempts for the cooldown period.
|
||||||
|
gitea_runner_auto_recover_cooldown_sec: 300
|
||||||
|
|
||||||
|
# Docker daemon resilience settings (applied to daemon.json).
|
||||||
|
# live-restore: containers survive daemon restarts — prevents stuck container
|
||||||
|
# states when the healthcheck restarts a hung daemon.
|
||||||
|
# shutdown-timeout: grace period (seconds) for containers to stop on daemon
|
||||||
|
# shutdown/restart. Default 15s is too short for DinD containers with nested
|
||||||
|
# processes (molecule tests). 30s gives SIGTERM time to propagate.
|
||||||
|
# max-concurrent-downloads/uploads: limits parallel transfers to reduce daemon
|
||||||
|
# memory pressure when multiple CI jobs pull images simultaneously.
|
||||||
|
# default-ulimits: prevents FD exhaustion in container processes.
|
||||||
|
gitea_runner_docker_live_restore: true
|
||||||
|
gitea_runner_docker_shutdown_timeout: 30
|
||||||
|
gitea_runner_docker_max_concurrent_downloads: 3
|
||||||
|
gitea_runner_docker_max_concurrent_uploads: 3
|
||||||
|
gitea_runner_docker_default_nofile: 65536
|
||||||
|
|
||||||
|
# Admin token for runner deregistration via Gitea API.
|
||||||
|
# If not set, falls back to registration_token (which likely lacks admin scope).
|
||||||
|
# Set this to a token with admin scope to enable automatic runner cleanup on removal.
|
||||||
|
gitea_runner_admin_token: ""
|
||||||
|
|
||||||
|
# Removal defaults
|
||||||
|
gitea_runner_remove_systemd_template: true
|
||||||
|
gitea_runner_remove_user: true
|
||||||
|
|
||||||
|
# Runner configuration
|
||||||
|
gitea_runner_log_level: "info"
|
||||||
|
gitea_runner_container_label: "gitea-runner=true"
|
||||||
|
gitea_runner_file: ".runner"
|
||||||
|
|
||||||
|
# Containerd version pinning — Docker 28.x vendors containerd v2.1.x internally.
|
||||||
|
# containerd.io >= 2.3 ships a shim that returns a protobuf BootstrapResult which
|
||||||
|
# Docker 28.x's vendored containerd code cannot parse, causing:
|
||||||
|
# "failed to create TTRPC connection: unsupported protocol: \b\x03\x12Yunix"
|
||||||
|
# When Docker 29+ is installed (it vendors containerd 2.3+), this pin is not needed.
|
||||||
|
# Set to "" to skip the compatibility check and allow any containerd.io version.
|
||||||
|
gitea_runner_containerd_max_compatible_major: 2
|
||||||
|
gitea_runner_containerd_max_compatible_minor: 2
|
||||||
|
|
||||||
|
# Docker installation (for rootless dependencies)
|
||||||
|
gitea_runner_docker_gpg_key_path: "/etc/apt/keyrings/docker.gpg"
|
||||||
|
gitea_runner_docker_apt_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
|
||||||
|
gitea_runner_docker_apt_source_line: >-
|
||||||
|
deb [arch={{ gitea_runner_docker_apt_arch }} signed-by={{ gitea_runner_docker_gpg_key_path }}]
|
||||||
|
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
|
||||||
|
{{ ansible_facts['distribution_release'] }} stable
|
||||||
|
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
|
||||||
|
gitea_runner_docker_rootless_setup: true
|
||||||
|
|
||||||
|
# Rootless Docker helper scripts (dockerd-rootless-setuptool.sh / dockerd-rootless.sh).
|
||||||
|
# Arch Linux's "docker" package does not ship these (unlike Debian's docker-ce-rootless-extras),
|
||||||
|
# and no official Arch package provides them. They are fetched from the upstream moby/moby
|
||||||
|
# "contrib/" directory at the git ref below. The scripts are stable bash wrappers that are
|
||||||
|
# version-agnostic with respect to the dockerd binary, so a pinned ref is safe.
|
||||||
|
gitea_runner_rootless_scripts_ref: "v28.5.1"
|
||||||
|
# Install dir MUST match the location of the "docker" / "dockerd" / "rootlesskit" binaries so
|
||||||
|
# that dockerd-rootless-setuptool.sh (which derives BIN from its own dirname) finds them co-located.
|
||||||
|
gitea_runner_rootless_scripts_install_dir: "/usr/bin"
|
||||||
|
|
||||||
|
# Rootless Docker network driver: "slirp4netns" (default) or "pasta" (IPv6 support)
|
||||||
|
# slirp4netns is the default because pasta has a TCP proxy bug that sends RST
|
||||||
|
# packets with wrong sequence numbers, breaking TCP connections from Docker
|
||||||
|
# containers to external hosts. slirp4netns doesn't have IPv6 support.
|
||||||
|
# See: https://bugs.passt.top/show_bug.cgi?id=52
|
||||||
|
gitea_runner_docker_rootless_net_driver: "slirp4netns"
|
||||||
|
|
||||||
|
# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet)
|
||||||
|
gitea_runner_docker_ipv6_cidr: "fd00:dead:beef::/48"
|
||||||
|
|
||||||
|
# Pre-pull Docker images that CI runners need (avoids pulling on every CI run).
|
||||||
|
# The runner container image (ci-full) is large (~3.3GB) and the healthcheck's
|
||||||
|
# disk-space prune only removes dangling images, so pre-pulled tagged images persist.
|
||||||
|
# Set to [] to skip pre-pulling. Images are pulled as the runner user via rootless Docker.
|
||||||
|
gitea_runner_pre_pull_images: []
|
||||||
+1
-1
@@ -9,4 +9,4 @@
|
|||||||
when:
|
when:
|
||||||
- ansible_facts is defined
|
- ansible_facts is defined
|
||||||
- ansible_facts['service_mgr'] | default('') == 'systemd'
|
- ansible_facts['service_mgr'] | default('') == 'systemd'
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "molecule-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "molecule-test-runner"
|
gitea_runner_name: "molecule-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "deregister-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+5
-5
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "deregister-test-runner"
|
gitea_runner_name: "deregister-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: "{{ gitea_runner_data_dir }}/.runner"
|
dest: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
content: |
|
content: |
|
||||||
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"}
|
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ gitea_runner_name }}", "address": "http://localhost:3000"}
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
group: "{{ gitea_runner_service_user }}"
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "deregister-test-runner"
|
gitea_runner_name: "deregister-test-runner"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
skip_runner_registration: false
|
gitea_runner_skip_registration: false
|
||||||
tasks:
|
tasks:
|
||||||
- name: Include deregistration tasks
|
- name: Include deregistration tasks
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "deregister-test-runner"
|
gitea_runner_name: "deregister-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
- name: Check registration file was removed
|
- name: Check registration file was removed
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
register: runner_file_stat
|
register: gitea_runner_file_stat
|
||||||
|
|
||||||
- name: Assert registration file no longer exists
|
- name: Assert registration file no longer exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- not runner_file_stat.stat.exists
|
- not gitea_runner_file_stat.stat.exists
|
||||||
fail_msg: "Registration file (.runner) was not removed by deregistration"
|
fail_msg: "Registration file (.runner) was not removed by deregistration"
|
||||||
|
|
||||||
- name: Check systemd user service still exists
|
- name: Check systemd user service still exists
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "lifecycle-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "lifecycle-test-runner"
|
gitea_runner_name: "lifecycle-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "lifecycle-test-runner"
|
gitea_runner_name: "lifecycle-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "lifecycle-test-runner"
|
gitea_runner_name: "lifecycle-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
+8
-8
@@ -5,11 +5,11 @@
|
|||||||
vars:
|
vars:
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "molecule-runner-a"
|
gitea_runner_name: "molecule-runner-a"
|
||||||
skip_runner_registration: true
|
gitea_runner_skip_registration: true
|
||||||
docker_rootless_setup: false
|
gitea_runner_docker_rootless_setup: false
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea_runner
|
||||||
|
|
||||||
- name: Converge second runner instance
|
- name: Converge second runner instance
|
||||||
hosts: all
|
hosts: all
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
vars:
|
vars:
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "molecule-runner-b"
|
gitea_runner_name: "molecule-runner-b"
|
||||||
skip_runner_registration: true
|
gitea_runner_skip_registration: true
|
||||||
docker_rootless_setup: false
|
gitea_runner_docker_rootless_setup: false
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea_runner
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "remove-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
- name: Remove runner via remove-runner playbook
|
- name: Remove runner via remove-runner playbook
|
||||||
ansible.builtin.import_playbook: "../../../../remove-runner.yml"
|
ansible.builtin.import_playbook: "../../../../remove-runner.yml"
|
||||||
vars:
|
vars:
|
||||||
runner_name: "remove-test-runner"
|
gitea_runner_name: "remove-test-runner"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
skip_runner_registration: true
|
gitea_runner_skip_registration: true
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "remove-test-runner"
|
gitea_runner_name: "remove-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "template-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+14
-4
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "template-test-runner"
|
gitea_runner_name: "template-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -47,8 +47,11 @@
|
|||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'Type=oneshot' in prune_service.content | b64decode"
|
- "'Type=oneshot' in prune_service.content | b64decode"
|
||||||
- "'docker system prune' in prune_service.content | b64decode"
|
- "'docker rm -f' in prune_service.content | b64decode"
|
||||||
- "'docker volume prune' in prune_service.content | b64decode"
|
- "'GITEA-ACTIONS-TASK' in prune_service.content | b64decode"
|
||||||
|
- "'docker system prune -af' in prune_service.content | b64decode"
|
||||||
|
- "'docker network prune' in prune_service.content | b64decode"
|
||||||
|
- "'docker builder prune' in prune_service.content | b64decode"
|
||||||
fail_msg: "Prune service template is missing expected directives"
|
fail_msg: "Prune service template is missing expected directives"
|
||||||
|
|
||||||
- name: Read rendered prune timer template
|
- name: Read rendered prune timer template
|
||||||
@@ -99,8 +102,15 @@
|
|||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'docker info' in healthcheck_script.content | b64decode"
|
- "'docker info' in healthcheck_script.content | b64decode"
|
||||||
|
- "'timeout 10 docker info' in healthcheck_script.content | b64decode"
|
||||||
- "'systemctl --user restart docker.service' in healthcheck_script.content | b64decode"
|
- "'systemctl --user restart docker.service' in healthcheck_script.content | b64decode"
|
||||||
- "'systemctl --user restart gitea-runner.service' in healthcheck_script.content | b64decode"
|
- "'systemctl --user restart gitea-runner.service' in healthcheck_script.content | b64decode"
|
||||||
- "'docker system prune' in healthcheck_script.content | b64decode"
|
- "'docker rm -f' in healthcheck_script.content | b64decode"
|
||||||
|
- "'GITEA-ACTIONS-TASK' in healthcheck_script.content | b64decode"
|
||||||
|
- "'docker system prune -af' in healthcheck_script.content | b64decode"
|
||||||
|
- "'docker network prune' in healthcheck_script.content | b64decode"
|
||||||
|
- "'status=removing' in healthcheck_script.content | b64decode"
|
||||||
|
- "'status=stopping' in healthcheck_script.content | b64decode"
|
||||||
- "gitea_runner_healthcheck_disk_threshold | string in healthcheck_script.content | b64decode"
|
- "gitea_runner_healthcheck_disk_threshold | string in healthcheck_script.content | b64decode"
|
||||||
|
- "gitea_runner_healthcheck_disk_critical | string in healthcheck_script.content | b64decode"
|
||||||
fail_msg: "Healthcheck script template is missing expected content"
|
fail_msg: "Healthcheck script template is missing expected content"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
gitea_url: "http://localhost:3000"
|
||||||
|
registration_token: "fake-token-for-testing"
|
||||||
|
gitea_runner_name: "update-test-runner"
|
||||||
|
gitea_runner_skip_registration: true
|
||||||
|
gitea_runner_docker_rootless_setup: false
|
||||||
|
roles:
|
||||||
|
- role: gitea_runner
|
||||||
+2
-2
@@ -3,9 +3,9 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "update-test-runner"
|
gitea_runner_name: "update-test-runner"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Include update tasks
|
- name: Include update tasks
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: update_runner.yml
|
tasks_from: update_runner.yml
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "update-test-runner"
|
gitea_runner_name: "update-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
- name: Check if runner registration file exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
register: gitea_runner_file_stat
|
||||||
|
|
||||||
|
- name: Read runner registration file
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
register: gitea_runner_file_content
|
||||||
|
when: gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
|
||||||
|
- name: Parse runner registration data
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gitea_runner_reg: >
|
||||||
|
{{ (gitea_runner_file_content.content | b64decode | from_json)
|
||||||
|
if (gitea_runner_file_content is defined and gitea_runner_file_content.content is defined)
|
||||||
|
else {} }}
|
||||||
|
when: gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
|
||||||
|
- name: Deregister runner from Gitea via API
|
||||||
|
ansible.builtin.command: >
|
||||||
|
curl -sf --connect-timeout 5 --max-time 10 -X DELETE
|
||||||
|
-H "Authorization: token {{ gitea_runner_admin_token | default(registration_token) }}"
|
||||||
|
"{{ gitea_url }}/api/v1/admin/actions/runners/{{ gitea_runner_reg.id }}"
|
||||||
|
args:
|
||||||
|
chdir: "{{ gitea_runner_data_dir }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
|
||||||
|
when:
|
||||||
|
- gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
- not gitea_runner_skip_registration
|
||||||
|
- gitea_runner_reg.id is defined
|
||||||
|
register: gitea_runner_deregister_output
|
||||||
|
changed_when: gitea_runner_deregister_output.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Warn if deregistration failed
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
WARNING: Runner deregistration from Gitea failed (rc={{ gitea_runner_deregister_output.rc | default('N/A') }}).
|
||||||
|
The runner entry may remain in Gitea's admin UI as offline.
|
||||||
|
Use an admin token (gitea_runner_admin_token var) to enable automatic cleanup,
|
||||||
|
or remove it manually from {{ gitea_url }}/-/admin/actions/runners
|
||||||
|
when:
|
||||||
|
- gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
- not gitea_runner_skip_registration
|
||||||
|
- gitea_runner_deregister_output is defined
|
||||||
|
- gitea_runner_deregister_output.rc | default(1) != 0
|
||||||
|
|
||||||
|
- name: Remove runner registration file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
state: absent
|
||||||
|
when: gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
+22
-4
@@ -7,6 +7,22 @@
|
|||||||
group: "{{ gitea_runner_service_user }}"
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Write auto-recovery API token file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ gitea_runner_auto_recover_api_token }}"
|
||||||
|
dest: "{{ gitea_runner_config_dir }}/auto-recover.token"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
mode: "0400"
|
||||||
|
no_log: true
|
||||||
|
when: gitea_runner_auto_recover_api_token | length > 0
|
||||||
|
|
||||||
|
- name: Remove stale auto-recovery token file (if auto-recovery disabled)
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_config_dir }}/auto-recover.token"
|
||||||
|
state: absent
|
||||||
|
when: gitea_runner_auto_recover_api_token | length == 0
|
||||||
|
|
||||||
- name: Create healthcheck user service file
|
- name: Create healthcheck user service file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: runner-healthcheck.service.j2
|
src: runner-healthcheck.service.j2
|
||||||
@@ -29,10 +45,11 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
when:
|
when:
|
||||||
- systemd_available.stat.exists
|
- gitea_runner_systemd_available.stat.exists
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
- name: Enable and start healthcheck user timer
|
- name: Enable and start healthcheck user timer
|
||||||
ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer
|
ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer
|
||||||
@@ -40,7 +57,8 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
when:
|
when:
|
||||||
- systemd_available.stat.exists
|
- gitea_runner_systemd_available.stat.exists
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
- name: Include registration
|
- name: Include registration
|
||||||
ansible.builtin.include_tasks: register.yml
|
ansible.builtin.include_tasks: register.yml
|
||||||
when: not skip_runner_registration
|
when: not gitea_runner_skip_registration
|
||||||
|
|
||||||
- name: Include service setup
|
- name: Include service setup
|
||||||
ansible.builtin.include_tasks: service.yml
|
ansible.builtin.include_tasks: service.yml
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
- name: Check runner registration file exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
register: gitea_runner_file_stat
|
||||||
|
|
||||||
|
- name: Read runner registration file
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
register: gitea_runner_file_content
|
||||||
|
when: gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
|
||||||
|
- name: Parse runner registration data
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gitea_runner_reg: >
|
||||||
|
{{ (gitea_runner_file_content.content | b64decode | from_json)
|
||||||
|
if (gitea_runner_file_content is defined and gitea_runner_file_content.content is defined)
|
||||||
|
else {} }}
|
||||||
|
when: gitea_runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
|
||||||
|
- name: Wait for runner user service to be active
|
||||||
|
ansible.builtin.command: systemctl --user is-active gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
register: gitea_runner_service_check
|
||||||
|
changed_when: false
|
||||||
|
retries: 10
|
||||||
|
delay: 2
|
||||||
|
until: gitea_runner_service_check.stdout | default('') | trim == 'active'
|
||||||
|
when:
|
||||||
|
- gitea_runner_systemd_available.stat.exists
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Validate runner installation
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: >
|
||||||
|
Runner '{{ gitea_runner_name }}' is not properly installed:
|
||||||
|
{% if not (gitea_runner_file_stat.stat.exists | default(false)) %}
|
||||||
|
- Registration file (.runner) is missing. Registration may have failed.
|
||||||
|
{% endif %}
|
||||||
|
{% if gitea_runner_docker_rootless_setup and not (gitea_runner_service_check.stdout | default('') | trim) == 'active' %}
|
||||||
|
- Systemd user service is not active.
|
||||||
|
{% endif %}
|
||||||
|
when: >
|
||||||
|
not (gitea_runner_file_stat.stat.exists | default(false))
|
||||||
|
or (gitea_runner_docker_rootless_setup and not (gitea_runner_service_check.stdout | default('') | trim) == 'active')
|
||||||
|
|
||||||
|
- name: Report runner status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >
|
||||||
|
Runner '{{ gitea_runner_name }}' is installed and running.
|
||||||
|
Registered: {{ gitea_runner_file_stat.stat.exists | default(false) }}
|
||||||
|
{% if gitea_runner_reg.id is defined %}Runner ID: {{ gitea_runner_reg.id }}{% endif %}
|
||||||
|
{% if gitea_runner_reg.uuid is defined %}UUID: {{ gitea_runner_reg.uuid }}{% endif %}
|
||||||
|
{% if gitea_runner_reg.address is defined %}Gitea: {{ gitea_runner_reg.address }}{% endif %}
|
||||||
|
Service: {{ gitea_runner_service_check.stdout | default('unknown') | trim }}
|
||||||
|
|
||||||
|
- name: Optional Gitea API verification
|
||||||
|
when:
|
||||||
|
- gitea_url is defined
|
||||||
|
- gitea_runner_admin_token is defined
|
||||||
|
- gitea_runner_admin_token | length > 0
|
||||||
|
block:
|
||||||
|
- name: Check admin runners API
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ gitea_url }}/api/v1/admin/runners"
|
||||||
|
headers:
|
||||||
|
Authorization: "token {{ gitea_runner_admin_token }}"
|
||||||
|
method: GET
|
||||||
|
status_code: [200, 401, 403, 404]
|
||||||
|
return_content: true
|
||||||
|
body_format: json
|
||||||
|
register: gitea_runner_admin_api_response
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Check repo runners API
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ gitea_url }}/api/v1/repos/{{ gitea_runner_test_repo | default('oblachno-oss/grm') }}/actions/runners"
|
||||||
|
headers:
|
||||||
|
Authorization: "token {{ gitea_runner_admin_token }}"
|
||||||
|
method: GET
|
||||||
|
status_code: [200, 401, 403, 404]
|
||||||
|
return_content: true
|
||||||
|
body_format: json
|
||||||
|
register: gitea_runner_repo_api_response
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Report API status (informational only)
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >
|
||||||
|
API checks (informational only — not used for pass/fail):
|
||||||
|
Admin API: {{ gitea_runner_admin_api_response.status | default('no response') }}.
|
||||||
|
Repo API: {{ gitea_runner_repo_api_response.status | default('no response') }}.
|
||||||
|
{% if gitea_runner_admin_api_response.json.runners | default([]) | selectattr('name', 'equalto', gitea_runner_name) | list | length > 0 %}
|
||||||
|
Runner found in admin API.
|
||||||
|
{% endif %}
|
||||||
|
{% if gitea_runner_repo_api_response.json.runners | default([]) | selectattr('name', 'equalto', gitea_runner_name) | list | length > 0 %}
|
||||||
|
Runner found in repo API.
|
||||||
|
{% endif %}
|
||||||
|
rescue:
|
||||||
|
- name: API check failed
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "API verification skipped due to connection or permission error."
|
||||||
+4
-1
@@ -17,6 +17,9 @@
|
|||||||
- name: Include healthcheck setup
|
- name: Include healthcheck setup
|
||||||
ansible.builtin.include_tasks: healthcheck.yml
|
ansible.builtin.include_tasks: healthcheck.yml
|
||||||
|
|
||||||
|
- name: Include pre-pull images
|
||||||
|
ansible.builtin.include_tasks: pre_pull_images.yml
|
||||||
|
|
||||||
- name: Include integration test
|
- name: Include integration test
|
||||||
ansible.builtin.include_tasks: integration_test.yml
|
ansible.builtin.include_tasks: integration_test.yml
|
||||||
when: not skip_runner_registration
|
when: not gitea_runner_skip_registration
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
# Pre-pull Docker images that CI runners need to avoid pulling them on
|
||||||
|
# every CI run. The runner container image (ci-full) is large (~3.3GB)
|
||||||
|
# and pulling it on every run causes timeouts and disk pressure.
|
||||||
|
#
|
||||||
|
# The healthcheck script's disk-space prune only removes dangling images
|
||||||
|
# (not tagged ones), so pre-pulled images persist between CI runs.
|
||||||
|
#
|
||||||
|
# Set gitea_runner_pre_pull_images to a list of image refs to pull, or
|
||||||
|
# empty list to skip pre-pulling.
|
||||||
|
|
||||||
|
- name: Pre-pull Docker images for CI runner
|
||||||
|
ansible.builtin.command: "docker pull {{ item }}"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
register: gitea_runner_pre_pull_result
|
||||||
|
changed_when: "'Status: Downloaded' in gitea_runner_pre_pull_result.stdout or 'Status: Downloaded' in gitea_runner_pre_pull_result.stderr"
|
||||||
|
retries: 3
|
||||||
|
delay: 5
|
||||||
|
until: gitea_runner_pre_pull_result is success
|
||||||
|
loop: "{{ gitea_runner_pre_pull_images }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_pre_pull_images | length > 0
|
||||||
+9
-4
@@ -6,6 +6,7 @@
|
|||||||
owner: "{{ gitea_runner_service_user }}"
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
group: "{{ gitea_runner_service_user }}"
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
register: gitea_runner_prune_service
|
||||||
|
|
||||||
- name: Create docker-prune user timer file
|
- name: Create docker-prune user timer file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
owner: "{{ gitea_runner_service_user }}"
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
group: "{{ gitea_runner_service_user }}"
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
register: gitea_runner_prune_timer
|
||||||
|
|
||||||
- name: Reload systemd user daemon for prune timer
|
- name: Reload systemd user daemon for prune timer
|
||||||
ansible.builtin.command: systemctl --user daemon-reload
|
ansible.builtin.command: systemctl --user daemon-reload
|
||||||
@@ -21,10 +23,12 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
when:
|
when:
|
||||||
- systemd_available.stat.exists
|
- gitea_runner_systemd_available.stat.exists
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_prune_service is changed or gitea_runner_prune_timer is changed
|
||||||
|
|
||||||
- name: Enable and start docker-prune user timer
|
- name: Enable and start docker-prune user timer
|
||||||
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
|
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
|
||||||
@@ -32,7 +36,8 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
when:
|
when:
|
||||||
- systemd_available.stat.exists
|
- gitea_runner_systemd_available.stat.exists
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
- name: Ensure work 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: Check if runner registration file exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
register: gitea_runner_registered
|
||||||
|
|
||||||
|
- name: Remove stale runner registration file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- gitea_runner_registered.stat.exists
|
||||||
|
- gitea_runner_force_reregister | bool
|
||||||
|
register: gitea_runner_registration_removed
|
||||||
|
|
||||||
|
- name: Register runner with Gitea
|
||||||
|
ansible.builtin.command: >
|
||||||
|
{{ gitea_runner_binary_path }} register
|
||||||
|
--token {{ registration_token }}
|
||||||
|
--name {{ gitea_runner_name }}
|
||||||
|
--instance {{ gitea_url }}
|
||||||
|
--labels {{ gitea_runner_labels }}
|
||||||
|
--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) }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
|
||||||
|
when: not gitea_runner_registered.stat.exists or gitea_runner_force_reregister | bool
|
||||||
|
register: gitea_runner_register_output
|
||||||
|
changed_when: >-
|
||||||
|
gitea_runner_register_output.rc == 0 and
|
||||||
|
('already exists' not in gitea_runner_register_output.stdout | default(''))
|
||||||
|
timeout: 60
|
||||||
|
|
||||||
|
- name: Ensure runner service is running after registration
|
||||||
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_systemd_available.stat.exists
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_register_output is defined
|
||||||
|
- gitea_runner_register_output.rc | default(1) == 0
|
||||||
+3
-3
@@ -6,14 +6,14 @@
|
|||||||
|
|
||||||
- name: Resolve runner service user
|
- name: Resolve runner service user
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ runner_name }}"
|
gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ gitea_runner_name }}"
|
||||||
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
||||||
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
||||||
|
|
||||||
- name: Resolve runner data and config dirs
|
- name: Resolve runner data and config dirs
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
|
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ gitea_runner_name }}"
|
||||||
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
|
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ gitea_runner_name }}"
|
||||||
|
|
||||||
- name: Resolve runner service user UID
|
- name: Resolve runner service user UID
|
||||||
ansible.builtin.getent:
|
ansible.builtin.getent:
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
---
|
||||||
|
- name: Ensure keyrings directory exists (Debian/Ubuntu)
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/etc/apt/keyrings"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
when: ansible_facts['os_family'] == 'Debian'
|
||||||
|
|
||||||
|
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" \
|
||||||
|
| gpg --dearmor --yes -o {{ gitea_runner_docker_gpg_key_path }}
|
||||||
|
args:
|
||||||
|
creates: "{{ gitea_runner_docker_gpg_key_path }}"
|
||||||
|
executable: /bin/bash
|
||||||
|
when: ansible_facts['os_family'] == 'Debian'
|
||||||
|
|
||||||
|
- name: Add Docker APT repository (Debian/Ubuntu)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/apt/sources.list.d/docker.list
|
||||||
|
content: "{{ gitea_runner_docker_apt_source_line }}\n"
|
||||||
|
mode: "0644"
|
||||||
|
register: gitea_runner_docker_apt_repo
|
||||||
|
when: ansible_facts['os_family'] == 'Debian'
|
||||||
|
|
||||||
|
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: true
|
||||||
|
when:
|
||||||
|
- ansible_facts['os_family'] == 'Debian'
|
||||||
|
- gitea_runner_docker_apt_repo is changed
|
||||||
|
|
||||||
|
# Install Docker packages from the upstream Docker APT repository.
|
||||||
|
# We do NOT pin to 28.x because recent Ubuntu releases (e.g. 26.04/plucky)
|
||||||
|
# may not have 28.x packages in the Docker repo, and Docker 29 is safe
|
||||||
|
# for rootless mode when the daemon.json disables the containerd snapshotter
|
||||||
|
# and sets a conservative default nofile ulimit (see daemon.json tasks below).
|
||||||
|
- name: Install rootless Docker dependencies (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- uidmap
|
||||||
|
- slirp4netns
|
||||||
|
- passt
|
||||||
|
- fuse-overlayfs
|
||||||
|
- docker-ce
|
||||||
|
- docker-ce-cli
|
||||||
|
- docker-ce-rootless-extras
|
||||||
|
- containerd.io
|
||||||
|
- docker-compose-plugin
|
||||||
|
- rsync
|
||||||
|
state: present
|
||||||
|
register: gitea_runner_docker_install
|
||||||
|
when: ansible_facts['os_family'] == 'Debian'
|
||||||
|
|
||||||
|
- name: Update pacman cache (Arch Linux)
|
||||||
|
community.general.pacman:
|
||||||
|
update_cache: true
|
||||||
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Install rootless Docker dependencies (Arch Linux)
|
||||||
|
community.general.pacman:
|
||||||
|
name:
|
||||||
|
- docker
|
||||||
|
- docker-compose
|
||||||
|
- slirp4netns
|
||||||
|
- passt
|
||||||
|
- fuse-overlayfs
|
||||||
|
- rsync
|
||||||
|
# rootlesskit is the userspace networking/namespace driver for rootless Docker.
|
||||||
|
# It is NOT a dependency of the "docker" package on Arch and must be installed explicitly.
|
||||||
|
- rootlesskit
|
||||||
|
state: present
|
||||||
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
|
||||||
|
# The tun kernel module is required by rootlesskit (both slirp4netns and pasta
|
||||||
|
# drivers create a tap device inside a user namespace). On Arch Linux, CONFIG_TUN=m
|
||||||
|
# so the module must be loaded. If the running kernel doesn't match the installed
|
||||||
|
# kernel (e.g. after a pacman -Syu that upgraded linux but didn't reboot), modprobe
|
||||||
|
# will fail — in that case we warn but don't fail, as a reboot will fix it.
|
||||||
|
- name: Load tun kernel module for rootless networking
|
||||||
|
community.general.modprobe:
|
||||||
|
name: tun
|
||||||
|
state: present
|
||||||
|
ignore_errors: true
|
||||||
|
register: gitea_runner_tun_module
|
||||||
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
|
||||||
|
- name: Warn if tun module could not be loaded (kernel mismatch — reboot needed)
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
WARNING: Could not load the tun kernel module. This is likely because the
|
||||||
|
running kernel ({{ ansible_facts['kernel'] }}) does not match the installed
|
||||||
|
kernel modules. A reboot is required before rootless Docker can start.
|
||||||
|
when:
|
||||||
|
- ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
- gitea_runner_tun_module is failed
|
||||||
|
|
||||||
|
# Arch Linux's "docker" package does not ship the rootless setup scripts (dockerd-rootless-setuptool.sh
|
||||||
|
# and dockerd-rootless.sh), unlike Debian/Ubuntu's docker-ce-rootless-extras. No official Arch package
|
||||||
|
# provides them, so fetch them from the upstream moby/moby contrib/ directory. They are installed
|
||||||
|
# alongside the docker binaries (/usr/bin) because dockerd-rootless-setuptool.sh derives its BIN
|
||||||
|
# directory from its own location and expects docker/dockerd/rootlesskit to be co-located there.
|
||||||
|
- name: Fetch rootless Docker setup scripts (Arch Linux)
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://raw.githubusercontent.com/moby/moby/{{ gitea_runner_rootless_scripts_ref }}/contrib/{{ item.name }}"
|
||||||
|
dest: "{{ gitea_runner_rootless_scripts_install_dir }}/{{ item.name }}"
|
||||||
|
mode: "0755"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
loop:
|
||||||
|
- name: dockerd-rootless-setuptool.sh
|
||||||
|
- name: dockerd-rootless.sh
|
||||||
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
|
||||||
|
- name: Check if rootless Docker is already set up
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
||||||
|
register: gitea_runner_rootless_docker_check
|
||||||
|
|
||||||
|
# Create the systemd override BEFORE running the setuptool so that when
|
||||||
|
# the setuptool starts docker.service, it picks up the pasta network driver
|
||||||
|
# instead of the default slirp4netns (which may fail on some kernels).
|
||||||
|
- name: Ensure systemd user override directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- not gitea_runner_rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
- name: Pre-configure rootless Docker network driver override
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
|
||||||
|
content: |
|
||||||
|
[Service]
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
|
||||||
|
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
||||||
|
{% endif %}
|
||||||
|
mode: "0644"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- not gitea_runner_rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
# Write daemon.json BEFORE the setuptool starts dockerd, so Docker 29
|
||||||
|
# starts with containerd snapshotter disabled from the very first boot.
|
||||||
|
# Without this, Docker 29 uses containerd snapshots by default, which
|
||||||
|
# causes instability in rootless mode.
|
||||||
|
- name: Ensure Docker config directory exists (pre-setup)
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/docker"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- not gitea_runner_rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
- name: Pre-configure rootless Docker daemon.json (disable containerd snapshotter)
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
|
||||||
|
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
|
||||||
|
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
|
||||||
|
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
|
||||||
|
"default-ulimits": {
|
||||||
|
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"containerd-snapshotter": false
|
||||||
|
},
|
||||||
|
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||||
|
"ipv6": true,
|
||||||
|
"ip6tables": true,
|
||||||
|
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
|
||||||
|
"dns": ["10.0.2.3", "8.8.8.8"]
|
||||||
|
{% else %}
|
||||||
|
"ipv6": false,
|
||||||
|
"dns": ["8.8.8.8", "1.1.1.1"]
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- not gitea_runner_rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
- name: Set up rootless Docker for runner user
|
||||||
|
ansible.builtin.command: dockerd-rootless-setuptool.sh install
|
||||||
|
args:
|
||||||
|
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
DOCKERD_ROOTLESS_ROOTLESSKIT_NET: "{{ gitea_runner_docker_rootless_net_driver }}"
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- not gitea_runner_rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
- name: Start rootless Docker daemon (systemd user service)
|
||||||
|
ansible.builtin.command: systemctl --user start docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Enable rootless Docker daemon (systemd user service)
|
||||||
|
ansible.builtin.command: systemctl --user enable docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Ensure Docker config directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/docker"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Configure rootless Docker network driver
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
|
||||||
|
content: |
|
||||||
|
[Service]
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}"
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}"
|
||||||
|
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||||
|
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
||||||
|
{% endif %}
|
||||||
|
mode: "0644"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
register: gitea_runner_docker_network_override
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Reload systemd user daemon if network config changed
|
||||||
|
ansible.builtin.command: systemctl --user daemon-reload
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_docker_network_override is changed
|
||||||
|
|
||||||
|
- name: Configure rootless Docker daemon
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
"live-restore": {{ gitea_runner_docker_live_restore | to_json }},
|
||||||
|
"shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }},
|
||||||
|
"max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }},
|
||||||
|
"max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }},
|
||||||
|
"default-ulimits": {
|
||||||
|
"nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}}
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"containerd-snapshotter": false
|
||||||
|
},
|
||||||
|
{% if gitea_runner_docker_rootless_net_driver == 'pasta' %}
|
||||||
|
"ipv6": true,
|
||||||
|
"ip6tables": true,
|
||||||
|
"fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}",
|
||||||
|
"dns": ["10.0.2.3", "8.8.8.8"]
|
||||||
|
{% else %}
|
||||||
|
"ipv6": false,
|
||||||
|
"dns": ["8.8.8.8", "1.1.1.1"]
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
mode: "0644"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
register: gitea_runner_docker_ipv6_config
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
|
|
||||||
|
- name: Restart rootless Docker if config changed
|
||||||
|
ansible.builtin.command: systemctl --user restart docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_docker_ipv6_config is changed or gitea_runner_docker_network_override is changed
|
||||||
|
|
||||||
|
- name: Wait for rootless Docker daemon to be ready
|
||||||
|
ansible.builtin.command: docker version
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus"
|
||||||
|
register: gitea_runner_docker_ready
|
||||||
|
until: gitea_runner_docker_ready.rc == 0
|
||||||
|
retries: 10
|
||||||
|
delay: 2
|
||||||
|
changed_when: false
|
||||||
|
when: gitea_runner_docker_rootless_setup
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
- name: Create systemd user service file
|
||||||
|
ansible.builtin.template:
|
||||||
|
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"
|
||||||
|
register: gitea_runner_service_file
|
||||||
|
|
||||||
|
- 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 }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_systemd_available.stat.exists
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_service_file is changed
|
||||||
|
|
||||||
|
- name: Restart gitea-runner if service file changed
|
||||||
|
ansible.builtin.command: systemctl --user restart gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_systemd_available.stat.exists
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
|
- gitea_runner_service_file is changed
|
||||||
|
|
||||||
|
- name: Enable and start gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user enable --now gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- gitea_runner_systemd_available.stat.exists
|
||||||
|
- gitea_runner_docker_rootless_setup
|
||||||
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
- name: Check if systemd is available
|
- name: Check if systemd is available
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: /run/systemd/system
|
path: /run/systemd/system
|
||||||
register: systemd_available
|
register: gitea_runner_systemd_available
|
||||||
+3
-2
@@ -8,7 +8,8 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
when:
|
when:
|
||||||
- systemd_available.stat.exists | default(false) | bool
|
- gitea_runner_systemd_available.stat.exists | default(false) | bool
|
||||||
- docker_rootless_setup
|
- gitea_runner_docker_rootless_setup
|
||||||
changed_when: true
|
changed_when: true
|
||||||
+5
-5
@@ -6,21 +6,21 @@
|
|||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
system: true
|
system: true
|
||||||
create_home: true
|
create_home: true
|
||||||
register: runner_user
|
register: gitea_runner_user
|
||||||
|
|
||||||
- name: Set runner UID fact
|
- name: Set runner UID fact
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
gitea_runner_uid: "{{ runner_user.uid }}"
|
gitea_runner_uid: "{{ gitea_runner_user.uid }}"
|
||||||
|
|
||||||
- name: Check if lingering is already enabled
|
- name: Check if lingering is already enabled
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/var/lib/systemd/linger/{{ gitea_runner_service_user }}"
|
path: "/var/lib/systemd/linger/{{ gitea_runner_service_user }}"
|
||||||
register: linger_stat
|
register: gitea_runner_linger_stat
|
||||||
|
|
||||||
- name: Enable lingering for runner user
|
- name: Enable lingering for runner user
|
||||||
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
|
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
|
||||||
changed_when: not linger_stat.stat.exists
|
changed_when: not gitea_runner_linger_stat.stat.exists
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
|
|
||||||
- name: Ensure subuid entry for runner user
|
- name: Ensure subuid entry for runner user
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
+3
-2
@@ -21,6 +21,7 @@
|
|||||||
environment:
|
environment:
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
register: docker_version_output
|
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus"
|
||||||
|
register: gitea_runner_docker_version_output
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: docker_rootless_setup
|
when: gitea_runner_docker_rootless_setup
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Docker prune for Gitea runner resources
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||||
|
# Force-remove stale containers (including running ones) left behind by failed
|
||||||
|
# molecule tests. "docker container prune -f" only removes stopped containers,
|
||||||
|
# so running containers from crashed/interrupted CI jobs accumulate indefinitely,
|
||||||
|
# consuming disk and memory. We stop+rm everything first, then prune the rest.
|
||||||
|
# Exclude CI job containers (name starts with GITEA-ACTIONS-TASK) — removing
|
||||||
|
# them kills the active CI job and causes "RWLayer is unexpectedly nil" errors.
|
||||||
|
# Only remove containers older than 1 hour (grep for "hour/day/week/month/year
|
||||||
|
# ago" in RunningFor) to avoid killing molecule test containers that CI jobs
|
||||||
|
# are actively using.
|
||||||
|
ExecStart=/bin/sh -c 'docker ps -a --format "{% raw %}{{.ID}} {{.Names}} {{.RunningFor}}{% endraw %}" 2>/dev/null | grep -v "GITEA-ACTIONS-TASK" | grep -E "(hour|day|week|month|year)s? ago" | awk "{print $1}" | xargs -r docker rm -f 2>/dev/null || true'
|
||||||
|
ExecStart=/usr/bin/docker system prune -af --filter "until={{ gitea_runner_prune_until }}" --volumes
|
||||||
|
# Prune networks older than the prune-until threshold to avoid removing
|
||||||
|
# networks that molecule tests are actively creating (e.g. 'traefik' network
|
||||||
|
# created during molecule create phase before containers are attached).
|
||||||
|
ExecStart=/usr/bin/docker network prune -f --filter "until={{ gitea_runner_prune_until }}"
|
||||||
|
ExecStart=/usr/bin/docker builder prune -f
|
||||||
+2
-2
@@ -3,6 +3,8 @@ Description=Gitea Actions Runner (rootless)
|
|||||||
After=docker.service
|
After=docker.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
PartOf=docker.service
|
PartOf=docker.service
|
||||||
|
StartLimitIntervalSec=300
|
||||||
|
StartLimitBurst=10
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
@@ -14,8 +16,6 @@ ExecStop=/bin/kill -TERM $MAINPID
|
|||||||
TimeoutStopSec=30
|
TimeoutStopSec=30
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec={{ gitea_runner_service_restart_sec }}
|
RestartSec={{ gitea_runner_service_restart_sec }}
|
||||||
StartLimitIntervalSec=300
|
|
||||||
StartLimitBurst=10
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Health check for gitea-runner: verifies Docker daemon and runner service.
|
||||||
|
# Exits 0 if healthy, 1 if Docker is down (triggers restart), 2 if runner is down.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOCKER_HOST="unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
XDG_RUNTIME_DIR="/run/user/{{ gitea_runner_uid }}"
|
||||||
|
export DOCKER_HOST XDG_RUNTIME_DIR
|
||||||
|
|
||||||
|
# 1. Check Docker daemon responsiveness (with timeout — a bare `docker info` can
|
||||||
|
# hang indefinitely on a stuck daemon, blocking the healthcheck itself).
|
||||||
|
if ! timeout 10 docker info >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: Docker daemon not responding at ${DOCKER_HOST} (timed out after 10s)"
|
||||||
|
systemctl --user restart docker.service
|
||||||
|
sleep 3
|
||||||
|
if ! timeout 10 docker info >/dev/null 2>&1; then
|
||||||
|
echo "CRITICAL: Docker daemon still down after restart"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "RECOVERED: Docker daemon restarted successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1b. Clean up stuck containers — containers in "removing" or "stopping" state
|
||||||
|
# for too long cause "cannot kill container: did not receive an exit event"
|
||||||
|
# errors in molecule destroy phases. Force-remove them so subsequent CI jobs
|
||||||
|
# don't inherit the stuck state.
|
||||||
|
stuck_containers=$(timeout 10 docker ps -a --filter "status=removing" --filter "status=stopping" --format '{% raw %}{{.ID}}{% endraw %}' 2>/dev/null || true)
|
||||||
|
if [[ -n "$stuck_containers" ]]; then
|
||||||
|
echo "WARN: Found stuck containers (removing/stopping), force-cleaning"
|
||||||
|
echo "$stuck_containers" | xargs -r docker rm -f 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Check gitea-runner service is active
|
||||||
|
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
||||||
|
if [[ "$runner_state" != "active" ]]; then
|
||||||
|
echo "ERROR: gitea-runner service is ${runner_state}, restarting"
|
||||||
|
systemctl --user restart gitea-runner.service
|
||||||
|
sleep 2
|
||||||
|
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
||||||
|
if [[ "$runner_state" != "active" ]]; then
|
||||||
|
echo "CRITICAL: gitea-runner service still down after restart"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
echo "RECOVERED: gitea-runner service restarted successfully"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2b. Detect unregistered runner state. When Gitea no longer recognizes the
|
||||||
|
# runner (e.g., server restore, runner record deleted, Gitea restart with
|
||||||
|
# token salt change), the runner logs "unregistered runner" every few seconds.
|
||||||
|
# A service restart will not fix this; re-registration is required.
|
||||||
|
#
|
||||||
|
# Detection method: query the Gitea API to verify the runner's UUID still
|
||||||
|
# exists. This is more reliable than parsing journal logs (which requires
|
||||||
|
# journal access permissions that runner users may not have — see the
|
||||||
|
# 2026-08-08 incident where journalctl --user returned "No journal files
|
||||||
|
# were opened due to insufficient permissions" for all runner users,
|
||||||
|
# causing the healthcheck to always report "OK: runner healthy" even
|
||||||
|
# though all runners were unregistered).
|
||||||
|
{% if gitea_runner_auto_recover_api_token %}
|
||||||
|
# Auto-recovery is enabled: fetch a new registration token from the Gitea API
|
||||||
|
# and re-register the runner automatically. A cooldown prevents infinite loops.
|
||||||
|
GITEA_API_TOKEN_FILE="{{ gitea_runner_config_dir }}/auto-recover.token"
|
||||||
|
COOLDOWN_FILE="{{ gitea_runner_data_dir }}/auto-recover.cooldown"
|
||||||
|
COOLDOWN_SEC={{ gitea_runner_auto_recover_cooldown_sec }}
|
||||||
|
GITEA_URL="{{ gitea_url }}"
|
||||||
|
RUNNER_NAME="{{ gitea_runner_name }}"
|
||||||
|
RUNNER_LABELS="{{ gitea_runner_labels }}"
|
||||||
|
BINARY="{{ gitea_runner_binary_path }}"
|
||||||
|
RUNNER_FILE="{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
{% endif %}
|
||||||
|
runner_unregistered=0
|
||||||
|
|
||||||
|
# Primary detection: query the Gitea API to check if the runner's ID
|
||||||
|
# still exists in Gitea's runner list. This works regardless of journal
|
||||||
|
# permissions.
|
||||||
|
{% if gitea_runner_auto_recover_api_token %}
|
||||||
|
if [[ -f "$GITEA_API_TOKEN_FILE" && -f "$RUNNER_FILE" ]]; then
|
||||||
|
API_TOKEN=$(cat "$GITEA_API_TOKEN_FILE" 2>/dev/null || true)
|
||||||
|
RUNNER_ID=$(python3 -c "import json; print(json.load(open('$RUNNER_FILE')).get('id',''))" 2>/dev/null || true)
|
||||||
|
if [[ -n "$API_TOKEN" && -n "$RUNNER_ID" ]]; then
|
||||||
|
# List all runners and check if our ID is present
|
||||||
|
runner_found=$(curl -sf --connect-timeout 5 --max-time 10 \
|
||||||
|
-H "Authorization: token $API_TOKEN" \
|
||||||
|
"${GITEA_URL}/api/v1/admin/actions/runners" 2>/dev/null \
|
||||||
|
| python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
runners = data if isinstance(data, list) else data.get('runners', [])
|
||||||
|
ids = [str(r.get('id', '')) for r in runners]
|
||||||
|
print('1' if '$RUNNER_ID' in ids else '0')
|
||||||
|
except Exception:
|
||||||
|
print('0')
|
||||||
|
" 2>/dev/null || echo "0")
|
||||||
|
if [[ "$runner_found" != "1" ]]; then
|
||||||
|
runner_unregistered=1
|
||||||
|
echo "CRITICAL: runner ID $RUNNER_ID not found in Gitea (unregistered)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Fallback detection: check journal logs (if accessible)
|
||||||
|
if [[ "$runner_unregistered" -eq 0 ]]; then
|
||||||
|
recent_errors=$(journalctl --user -u gitea-runner.service --since "5 minutes ago" --no-pager -q 2>/dev/null | grep -c "unregistered runner" || true)
|
||||||
|
if [[ "$recent_errors" -ge 3 ]]; then
|
||||||
|
runner_unregistered=1
|
||||||
|
echo "CRITICAL: runner is unregistered in Gitea (re-login failed $recent_errors times in 5 minutes)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$runner_unregistered" -ge 1 ]]; then
|
||||||
|
{% if gitea_runner_auto_recover_api_token %}
|
||||||
|
# Check cooldown — skip if we recently attempted recovery
|
||||||
|
if [[ -f "$COOLDOWN_FILE" ]]; then
|
||||||
|
last_attempt=$(cat "$COOLDOWN_FILE" 2>/dev/null || echo 0)
|
||||||
|
now=$(date +%s)
|
||||||
|
elapsed=$((now - last_attempt))
|
||||||
|
if [[ "$elapsed" -lt "$COOLDOWN_SEC" ]]; then
|
||||||
|
echo "SKIP: auto-recovery cooldown active (${elapsed}s < ${COOLDOWN_SEC}s). Will retry later."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mark attempt time BEFORE trying (so failures also get cooldown)
|
||||||
|
date +%s > "$COOLDOWN_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Read the API token
|
||||||
|
if [[ ! -f "$GITEA_API_TOKEN_FILE" ]]; then
|
||||||
|
echo "ERROR: auto-recover token file not found at $GITEA_API_TOKEN_FILE. Manual re-registration required."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
API_TOKEN=$(cat "$GITEA_API_TOKEN_FILE" 2>/dev/null || true)
|
||||||
|
if [[ -z "$API_TOKEN" ]]; then
|
||||||
|
echo "ERROR: auto-recover token file is empty. Manual re-registration required."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ATTEMPT: auto-recovering by fetching new registration token and re-registering..."
|
||||||
|
|
||||||
|
# Fetch a new registration token from the Gitea API
|
||||||
|
# Try org-level first (for org-scoped runners), then instance-level
|
||||||
|
REG_TOKEN=""
|
||||||
|
for endpoint in \
|
||||||
|
"api/v1/orgs/{{ gitea_runner_org | default('oblachno') }}/actions/runners/registration-token" \
|
||||||
|
"api/v1/admin/actions/runners/registration-token"; do
|
||||||
|
REG_TOKEN=$(curl -sf --connect-timeout 5 --max-time 10 -X POST \
|
||||||
|
-H "Authorization: token $API_TOKEN" \
|
||||||
|
"${GITEA_URL}/${endpoint}" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || true)
|
||||||
|
if [[ -n "$REG_TOKEN" ]]; then
|
||||||
|
echo "INFO: fetched registration token from ${endpoint}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$REG_TOKEN" ]]; then
|
||||||
|
echo "ERROR: failed to fetch registration token from Gitea API. Is Gitea reachable?"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the runner service
|
||||||
|
systemctl --user stop gitea-runner.service 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Remove the stale .runner file
|
||||||
|
rm -f "{{ gitea_runner_data_dir }}/.runner" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-register
|
||||||
|
cd "{{ gitea_runner_data_dir }}"
|
||||||
|
if "$BINARY" register \
|
||||||
|
--token "$REG_TOKEN" \
|
||||||
|
--name "$RUNNER_NAME" \
|
||||||
|
--instance "$GITEA_URL" \
|
||||||
|
--labels "$RUNNER_LABELS" \
|
||||||
|
--no-interactive 2>&1; then
|
||||||
|
echo "RECOVERED: runner re-registered successfully"
|
||||||
|
else
|
||||||
|
echo "ERROR: re-registration failed. Manual intervention required."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the runner service
|
||||||
|
systemctl --user start gitea-runner.service
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Verify recovery — query the Gitea API to confirm the new ID is registered
|
||||||
|
NEW_ID=$(python3 -c "import json; print(json.load(open('$RUNNER_FILE')).get('id',''))" 2>/dev/null || true)
|
||||||
|
if [[ -n "$NEW_ID" ]]; then
|
||||||
|
new_found=$(curl -sf --connect-timeout 5 --max-time 10 \
|
||||||
|
-H "Authorization: token $API_TOKEN" \
|
||||||
|
"${GITEA_URL}/api/v1/admin/actions/runners" 2>/dev/null \
|
||||||
|
| python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
runners = data if isinstance(data, list) else data.get('runners', [])
|
||||||
|
ids = [str(r.get('id', '')) for r in runners]
|
||||||
|
print('1' if '$NEW_ID' in ids else '0')
|
||||||
|
except Exception:
|
||||||
|
print('0')
|
||||||
|
" 2>/dev/null || echo "0")
|
||||||
|
if [[ "$new_found" == "1" ]]; then
|
||||||
|
echo "OK: runner recovered and registered with new ID $NEW_ID"
|
||||||
|
# Clear cooldown on success
|
||||||
|
rm -f "$COOLDOWN_FILE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "WARN: runner re-registered but ID not found in Gitea API. Will retry after cooldown."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARN: could not read new .runner file after re-registration. Will retry after cooldown."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
{% else %}
|
||||||
|
echo "Manual re-registration required: re-run gitea_runner role with gitea_runner_force_reregister=true."
|
||||||
|
# Restart the service once in case it is a transient token refresh issue,
|
||||||
|
# but this cannot recover an unregistered runner without re-registration.
|
||||||
|
systemctl --user restart gitea-runner.service
|
||||||
|
sleep 2
|
||||||
|
exit 3
|
||||||
|
{% endif %}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Check disk space — prune aggressively if below threshold
|
||||||
|
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||||
|
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_critical }} ]]; then
|
||||||
|
echo "CRITICAL: Disk usage at ${disk_pct}% (>= {{ gitea_runner_healthcheck_disk_critical }}%), full prune"
|
||||||
|
# Critical level: remove ALL stopped containers (no age filter) and ALL
|
||||||
|
# unused images/volumes. The until=1h gentle prune is insufficient here.
|
||||||
|
# Stop+rm stale non-CI containers regardless of age (failed molecule tests
|
||||||
|
# from the last 59 minutes also consume disk).
|
||||||
|
docker ps -a --format '{% raw %}{{.ID}} {{.Names}}{% endraw %}' 2>/dev/null \
|
||||||
|
| grep -v 'GITEA-ACTIONS-TASK' \
|
||||||
|
| awk '{print $1}' \
|
||||||
|
| xargs -r docker rm -f 2>/dev/null || true
|
||||||
|
docker system prune -af --volumes || true
|
||||||
|
docker network prune -f || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||||
|
echo "INFO: Disk usage after full prune: ${disk_pct}%"
|
||||||
|
elif [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
|
||||||
|
echo "WARN: Disk usage at ${disk_pct}%, pruning runner resources (until=1h)"
|
||||||
|
# Force-remove stale containers (including running ones from failed molecule tests)
|
||||||
|
# that are older than 1 hour. "docker container prune -f" only removes stopped
|
||||||
|
# containers, so running containers from crashed CI jobs accumulate and consume
|
||||||
|
# disk/memory. Exclude CI job containers (name starts with GITEA-ACTIONS-TASK).
|
||||||
|
# Only remove containers older than 1 hour to avoid killing molecule test
|
||||||
|
# containers that CI jobs are actively using.
|
||||||
|
docker ps -a --format '{% raw %}{{.ID}} {{.Names}} {{.RunningFor}}{% endraw %}' 2>/dev/null \
|
||||||
|
| grep -v 'GITEA-ACTIONS-TASK' \
|
||||||
|
| grep -E '(hour|day|week|month|year)s? ago' \
|
||||||
|
| awk '{print $1}' \
|
||||||
|
| xargs -r docker rm -f 2>/dev/null || true
|
||||||
|
# Prune images and containers older than 1h (until filter is NOT
|
||||||
|
# supported with --volumes, so prune volumes separately without a filter).
|
||||||
|
docker image prune -af --filter "until=1h" 2>/dev/null || true
|
||||||
|
docker container prune -f --filter "until=1h" 2>/dev/null || true
|
||||||
|
docker volume prune -f 2>/dev/null || true
|
||||||
|
# Prune networks older than 1 hour to avoid removing networks that
|
||||||
|
# molecule tests are actively creating (e.g. 'traefik' network created
|
||||||
|
# during molecule create phase before containers are attached).
|
||||||
|
docker network prune -f --filter "until=1h" || true
|
||||||
|
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||||
|
echo "INFO: Disk usage after prune: ${disk_pct}%"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK: runner healthy, disk at ${disk_pct}%"
|
||||||
|
exit 0
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Resolve runner UID
|
- name: Resolve runner UID
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: resolve_uid.yml
|
tasks_from: resolve_uid.yml
|
||||||
|
|
||||||
- name: Check if runner is already registered
|
- name: Check if runner is already registered
|
||||||
@@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
- name: Include registration if not registered
|
- name: Include registration if not registered
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: register.yml
|
tasks_from: register.yml
|
||||||
when:
|
when:
|
||||||
- not runner_registered.stat.exists
|
- not runner_registered.stat.exists
|
||||||
- not skip_runner_registration | default(false)
|
- not gitea_runner_skip_registration | default(false)
|
||||||
|
|
||||||
- name: Start gitea-runner user service
|
- name: Start gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user start gitea-runner
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
@@ -33,5 +33,5 @@
|
|||||||
become_user: "{{ gitea_runner_service_user }}"
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Resolve runner UID
|
- name: Resolve runner UID
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: resolve_uid.yml
|
tasks_from: resolve_uid.yml
|
||||||
|
|
||||||
- name: Check systemd user service status
|
- name: Check systemd user service status
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Resolve runner UID
|
- name: Resolve runner UID
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: resolve_uid.yml
|
tasks_from: resolve_uid.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Update runner
|
- name: Update runner
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: update_runner.yml
|
tasks_from: update_runner.yml
|
||||||
|
|||||||
+6
-6
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
|||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
[](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/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/wiki)
|
||||||
[](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/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# Retrospective: CI Consolidation and devx Adoption
|
||||||
|
|
||||||
|
## Date
|
||||||
|
2026-07-12
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The grm repo (Gitea Runner Manager) underwent CI workflow consolidation
|
||||||
|
and adopted the latest devx package (v0.40.0 → v0.40.1) during this
|
||||||
|
period. The self-approval fallback fix in devx v0.40.1 required passing
|
||||||
|
`CI_GITEA_API_TOKEN` to the approval step in grm's CI workflow. This
|
||||||
|
retrospective covers grm v0.17.2 through v0.18.0.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
PRs: GRM-144 (IPv6/pasta), GRM-145 (devx v0.40.0 adoption), GRM-146 (CI
|
||||||
|
consolidation). Plus the self-approval fallback cherry-pick. ~12 commits.
|
||||||
|
|
||||||
|
## Timeline of Key Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| GRM-144 merged | IPv6 support via pasta network driver (v0.18.0) |
|
||||||
|
| GRM-145 merged | Adopted devx v0.40.0, removed redundant crypto/vault wrappers |
|
||||||
|
| GRM-146 merged | Consolidated CI and post-merge workflows (7→2 jobs) |
|
||||||
|
| devx v0.40.1 bump | Cherry-picked self-approval fallback into grm CI |
|
||||||
|
|
||||||
|
No CI failures were specific to grm during this period. The grm CI
|
||||||
|
passed cleanly on all runs. The only issue was the cross-repo
|
||||||
|
self-approval bug (inherited from devx), which was fixed by bumping
|
||||||
|
devx to v0.40.1 and passing `CI_GITEA_API_TOKEN` to the approval step.
|
||||||
|
|
||||||
|
## What Served Us Well
|
||||||
|
|
||||||
|
- **Clean CI consolidation.** GRM-146 merged 7 CI jobs into 2
|
||||||
|
(validate + molecule-tests) without any CI failures. The
|
||||||
|
consolidation pattern was already proven in devx (DEVX-126), so the
|
||||||
|
application to grm was straightforward.
|
||||||
|
- **devx adoption was smooth.** GRM-145 adopted devx v0.40.0 and removed
|
||||||
|
redundant crypto/vault wrappers. The refactoring was clean — no test
|
||||||
|
failures, no coverage drops.
|
||||||
|
- **Molecule tests stable.** All 6 molecule scenarios passed on every
|
||||||
|
CI run. The pasta networking change (GRM-144) was well-tested with
|
||||||
|
molecule before merge.
|
||||||
|
- **Pre-push hook caught missing Vikunja tasks.** The pre-push hook
|
||||||
|
validates Vikunja task existence, preventing pushes without
|
||||||
|
corresponding tasks.
|
||||||
|
|
||||||
|
## What Could Be Improved
|
||||||
|
|
||||||
|
### 1. Cross-Repo Dependency Propagation
|
||||||
|
|
||||||
|
When devx v0.40.1 was released with the self-approval fix, grm needed
|
||||||
|
to bump its devx version and update the CI workflow to pass
|
||||||
|
`CI_GITEA_API_TOKEN`. This was a manual process — there's no automated
|
||||||
|
mechanism to detect that a devx release affects downstream repos.
|
||||||
|
|
||||||
|
**Impact:** The self-approval fix was in devx for ~30 min before grm
|
||||||
|
was updated. If the timing had been different, grm PRs could have been
|
||||||
|
blocked.
|
||||||
|
|
||||||
|
**Lesson:** When releasing a devx fix that affects CI workflows in
|
||||||
|
downstream repos, bump devx in all repos in the same session. Consider
|
||||||
|
a "dependabot" style check that flags outdated devx versions.
|
||||||
|
|
||||||
|
### 2. No Repo-Specific Retrospective Directory
|
||||||
|
|
||||||
|
The grm repo didn't have a `docs/retrospectives/` directory until now.
|
||||||
|
Previous retrospectives were only in the infra repo. This meant grm-
|
||||||
|
specific lessons weren't being captured.
|
||||||
|
|
||||||
|
**Impact:** Low — grm had fewer issues during this period. But going
|
||||||
|
forward, grm-specific learnings should be documented here.
|
||||||
|
|
||||||
|
**Fix:** Created `docs/retrospectives/` directory with this
|
||||||
|
retrospective.
|
||||||
|
|
||||||
|
## Improvements Implemented
|
||||||
|
|
||||||
|
### 1. CI Workflow Consolidation (MEDIUM impact)
|
||||||
|
|
||||||
|
Merged 7 CI jobs into 2 (validate + molecule-tests), matching the
|
||||||
|
pattern established in devx. Reduced runner overhead by ~4 min per CI
|
||||||
|
run.
|
||||||
|
|
||||||
|
### 2. devx v0.40.1 Adoption (HIGH impact)
|
||||||
|
|
||||||
|
Bumped devx to v0.40.1, picking up the self-approval fallback fix.
|
||||||
|
Updated CI workflow to pass `CI_GITEA_API_TOKEN` to the approval step.
|
||||||
|
|
||||||
|
### 3. IPv6 Support via Pasta (MEDIUM impact)
|
||||||
|
|
||||||
|
GRM-144 enabled IPv6 in rootless Docker via the pasta network driver,
|
||||||
|
replacing the previous slirp4netns setup. This improves network
|
||||||
|
performance and enables IPv6 connectivity for runner containers.
|
||||||
|
|
||||||
|
## Action Items for Future Sessions
|
||||||
|
|
||||||
|
1. **Bump devx in all downstream repos when a CI-affecting fix is
|
||||||
|
released.** Don't leave repos on stale devx versions.
|
||||||
|
2. **Document grm-specific lessons in this retrospective directory.**
|
||||||
|
Don't rely on the infra retrospective to cover grm issues.
|
||||||
|
3. **When consolidating CI workflows, verify that all status checks
|
||||||
|
referenced by branch protection are still present.** The
|
||||||
|
consolidation renamed `quality` to `validate`, requiring a branch
|
||||||
|
protection update.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
GRM consists of two layers:
|
GRM consists of two layers:
|
||||||
|
|
||||||
1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
|
1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
|
||||||
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea.
|
2. **Ansible Role** (`ansible/roles/gitea_runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea.
|
||||||
|
|
||||||
## High-Level Design
|
## High-Level Design
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ The Ansible role handles all remote state: user creation, package installation,
|
|||||||
grm install <host>
|
grm install <host>
|
||||||
└── RunnerManager.install()
|
└── RunnerManager.install()
|
||||||
└── ansible-playbook ansible/install-runner.yml
|
└── ansible-playbook ansible/install-runner.yml
|
||||||
└── role: gitea-runner
|
└── role: gitea_runner
|
||||||
├── user_setup.yml (create per-runner system user + lingering)
|
├── user_setup.yml (create per-runner system user + lingering)
|
||||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||||
├── install_runner.yml (download binary, config, register, service)
|
├── install_runner.yml (download binary, config, register, service)
|
||||||
@@ -53,7 +53,7 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
|
|||||||
| `main.yml` | Entry point — includes all other task files in order |
|
| `main.yml` | Entry point — includes all other task files in order |
|
||||||
| `systemd_check.yml` | Verifies systemd is available on the target host |
|
| `systemd_check.yml` | Verifies systemd is available on the target host |
|
||||||
| `user_setup.yml` | Creates the per-runner system user, enables lingering, configures subuid/subgid, creates data and config directories |
|
| `user_setup.yml` | Creates the per-runner system user, enables lingering, configures subuid/subgid, creates data and config directories |
|
||||||
| `rootless_docker.yml` | Installs Docker packages (apt for Debian/Ubuntu, pacman for Arch), runs `dockerd-rootless-setuptool.sh install`, starts and enables the rootless Docker daemon |
|
| `rootless_docker.yml` | Installs Docker packages (apt for Debian/Ubuntu, pacman for Arch), provisions the rootless setup scripts on Arch (not shipped by the `docker` package), runs `dockerd-rootless-setuptool.sh install`, starts and enables the rootless Docker daemon |
|
||||||
| `install_runner.yml` | Downloads the gitea_runner binary, creates the config file, validates the binary, registers the runner with Gitea, creates and starts the systemd user service |
|
| `install_runner.yml` | Downloads the gitea_runner binary, creates the config file, validates the binary, registers the runner with Gitea, creates and starts the systemd user service |
|
||||||
| `download_gitea_runner.yml` | Downloads the gitea_runner binary from GitHub releases |
|
| `download_gitea_runner.yml` | Downloads the gitea_runner binary from GitHub releases |
|
||||||
| `validate.yml` | Validates the downloaded binary |
|
| `validate.yml` | Validates the downloaded binary |
|
||||||
@@ -83,8 +83,8 @@ Each runner runs as a systemd user service under a dedicated system user (`grm-<
|
|||||||
|
|
||||||
- **User**: `grm-<name>` (dedicated system user with lingering enabled)
|
- **User**: `grm-<name>` (dedicated system user with lingering enabled)
|
||||||
- **Home**: `/home/grm-<name>/`
|
- **Home**: `/home/grm-<name>/`
|
||||||
- **Data**: `/var/lib/gitea-runner/<name>/`
|
- **Data**: `/var/lib/gitea_runner/<name>/`
|
||||||
- **Config**: `/etc/gitea-runner/<name>/`
|
- **Config**: `/etc/gitea_runner/<name>/`
|
||||||
- **Service**: `gitea-runner.service` (systemd user service)
|
- **Service**: `gitea-runner.service` (systemd user service)
|
||||||
- **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner)
|
- **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner)
|
||||||
- **subuid/subgid**: `grm-<name>:100000:65536` (user namespace mapping)
|
- **subuid/subgid**: `grm-<name>:100000:65536` (user namespace mapping)
|
||||||
@@ -100,7 +100,7 @@ flowchart TD
|
|||||||
EXEC["Executor<br/>executor.py"]
|
EXEC["Executor<br/>executor.py"]
|
||||||
REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"]
|
REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"]
|
||||||
ANS["ansible-playbook subprocess"]
|
ANS["ansible-playbook subprocess"]
|
||||||
ROLE["Ansible Role<br/>ansible/roles/gitea-runner/"]
|
ROLE["Ansible Role<br/>ansible/roles/gitea_runner/"]
|
||||||
USER["user_setup.yml<br/>create system user + lingering"]
|
USER["user_setup.yml<br/>create system user + lingering"]
|
||||||
DOCKER["rootless_docker.yml<br/>rootless Docker setup"]
|
DOCKER["rootless_docker.yml<br/>rootless Docker setup"]
|
||||||
INSTALL["install_runner.yml<br/>download, config, register, service"]
|
INSTALL["install_runner.yml<br/>download, config, register, service"]
|
||||||
@@ -190,6 +190,13 @@ per-workaround DNS hacks.
|
|||||||
|
|
||||||
Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering.
|
Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering.
|
||||||
|
|
||||||
|
#### Platform-specific rootless provisioning
|
||||||
|
|
||||||
|
The rootless setup scripts (`dockerd-rootless-setuptool.sh` and `dockerd-rootless.sh`) are provided differently per OS:
|
||||||
|
|
||||||
|
- **Debian/Ubuntu** — shipped by the `docker-ce-rootless-extras` package (installed via the Docker APT repo).
|
||||||
|
- **Arch Linux** — the `docker` package does **not** include these scripts, and no official Arch package provides them. The role fetches them from the upstream `moby/moby` `contrib/` directory at a pinned, overridable git ref (`gitea_runner_rootless_scripts_ref`, default `v28.5.1`) and installs them into `/usr/bin` — co-located with `docker`/`dockerd`/`rootlesskit`, which is required because `dockerd-rootless-setuptool.sh` derives its `BIN` directory from its own location and expects those binaries alongside it. The `rootlesskit` package (a required rootless runtime dependency that is not pulled in by Arch's `docker` package) is also installed explicitly.
|
||||||
|
|
||||||
### Secret handling
|
### Secret handling
|
||||||
|
|
||||||
Registration tokens and admin API tokens are never exposed on the command line. The `RunnerManager._extra_vars_file()` context manager:
|
Registration tokens and admin API tokens are never exposed on the command line. The `RunnerManager._extra_vars_file()` context manager:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user