Compare commits

..
47 Commits
Author SHA1 Message Date
grm-ci-bot f905550aba release: v0.10.3
Post-merge / detect-type (push) Successful in 1m9s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 56s
2026-06-27 20:26:33 +00:00
emil cae4e2a860 GRM-107: refactor: use devx Makefile aliases, bump devx>=0.23.0
Post-merge / detect-type (push) Successful in 1m3s
Post-merge / release (push) Successful in 1m8s
Post-merge / validate-commit-msg (push) Successful in 1m9s
Post-merge / vikunja (push) Successful in 1m13s
Post-merge / badges (push) Successful in 1m19s
Post-merge / sync-wiki (push) Successful in 1m44s
Post-merge / publish (push) Successful in 1m3s
Post-merge / configure-repo (push) Successful in 1m18s
2026-06-27 20:24:29 +00:00
gitea-actions-bot efbd24daec chore: update badge URLs to commit fc36a6d7 [skip ci] 2026-06-27 17:55:55 +00:00
emil 9066ef9724 GRM-106: fix: add EXTRAS=ci to all setup-image calls, workflow-level CI_GITEA_TOKEN
Post-merge / detect-type (push) Successful in 49s
Post-merge / release (push) Successful in 1m5s
Post-merge / validate-commit-msg (push) Successful in 1m7s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 1m12s
Post-merge / badges (push) Successful in 1m16s
Post-merge / sync-wiki (push) Successful in 1m33s
Post-merge / configure-repo (push) Successful in 57s
2026-06-27 17:53:45 +00:00
gitea-actions-bot 96770a770e chore: update badge URLs to commit 5c7cd006 [skip ci] 2026-06-27 17:41:15 +00:00
emil e753b34788 GRM-105: fix: revert EXTRAS=ci default in setup-image
Post-merge / detect-type (push) Successful in 43s
Post-merge / validate-commit-msg (push) Successful in 1m4s
Post-merge / configure-repo (push) Failing after 1m6s
Post-merge / vikunja (push) Successful in 1m7s
Post-merge / release (push) Successful in 1m12s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 1m27s
Post-merge / badges (push) Successful in 1m27s
2026-06-27 17:38:59 +00:00
gitea-actions-bot 48422b18e5 chore: update badge URLs to commit 1504e628 [skip ci] 2026-06-27 16:56:13 +00:00
emil 190157cce6 GRM-104: refactor: remove hadolint on-the-fly install workaround
Post-merge / detect-type (push) Successful in 53s
Post-merge / release (push) Successful in 1m3s
Post-merge / validate-commit-msg (push) Successful in 1m10s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 1m10s
Post-merge / badges (push) Successful in 1m20s
Post-merge / sync-wiki (push) Successful in 1m43s
Post-merge / configure-repo (push) Successful in 50s
2026-06-27 16:53:57 +00:00
gitea-actions-bot 1d0a082044 chore: update badge URLs to commit 0f5e8e82 [skip ci] 2026-06-27 16:40:47 +00:00
emil 799d36f254 GRM-103: fix: install hadolint on-the-fly in setup-image
Post-merge / detect-type (push) Successful in 54s
Post-merge / release (push) Successful in 1m10s
Post-merge / validate-commit-msg (push) Successful in 1m13s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 1m15s
Post-merge / configure-repo (push) Successful in 1m14s
Post-merge / badges (push) Successful in 1m21s
Post-merge / sync-wiki (push) Successful in 1m42s
2026-06-27 16:38:30 +00:00
gitea-actions-bot e0d43b0ed8 chore: update badge URLs to commit 8e0b18b4 [skip ci] 2026-06-27 16:34:11 +00:00
gitea-actions-bot 3189161f61 chore: update badge URLs to commit ed92e075 [skip ci] 2026-06-27 16:32:16 +00:00
grm-ci-bot c339698603 release: v0.10.2
Post-merge / detect-type (push) Successful in 1m2s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / badges (push) Successful in 57s
2026-06-27 16:31:58 +00:00
emil cbf082c78f GRM-102: fix: bump devx>=0.22.0 and remove REPO_TOKEN alias
Post-merge / detect-type (push) Successful in 48s
Post-merge / release (push) Successful in 1m0s
Post-merge / validate-commit-msg (push) Successful in 1m2s
Post-merge / vikunja (push) Successful in 1m0s
Post-merge / badges (push) Successful in 1m9s
Post-merge / sync-wiki (push) Successful in 1m38s
Post-merge / configure-repo (push) Successful in 1m3s
Post-merge / publish (push) Successful in 55s
2026-06-27 16:30:16 +00:00
gitea-actions-bot 4070135fda chore: update badge URLs to commit 5c243201 [skip ci] 2026-06-27 15:51:35 +00:00
emil ace0176e3a GRM-101: refactor: rename REPO_TOKEN to CI_GITEA_TOKEN, consolidate env vars
Post-merge / detect-type (push) Successful in 47s
Post-merge / release (push) Failing after 14s
Post-merge / validate-commit-msg (push) Successful in 59s
Post-merge / vikunja (push) Successful in 56s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 48s
Post-merge / badges (push) Successful in 1m8s
Post-merge / sync-wiki (push) Successful in 1m14s
2026-06-27 15:49:36 +00:00
gitea-actions-bot fda1d99d86 chore: update badge URLs to commit 42a9384e [skip ci] 2026-06-27 13:49:17 +00:00
emil 465f45d939 GRM-100: fix: gate auto-merge on release-dry-run and unmask failures
Post-merge / detect-type (push) Successful in 42s
Post-merge / release (push) Failing after 11s
Post-merge / validate-commit-msg (push) Successful in 48s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 41s
Post-merge / badges (push) Successful in 56s
Post-merge / sync-wiki (push) Successful in 1m16s
Post-merge / configure-repo (push) Successful in 40s
2026-06-27 13:47:30 +00:00
gitea-actions-bot 103beaa0e8 chore: update badge URLs to commit 6b1270e1 [skip ci] 2026-06-27 13:30:20 +00:00
emil 2e97578269 GRM-99: fix: setup-image configures Gitea PyPI registry and shows pip errors
Post-merge / detect-type (push) Successful in 42s
Post-merge / release (push) Failing after 14s
Post-merge / validate-commit-msg (push) Successful in 53s
Post-merge / vikunja (push) Successful in 52s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 54s
Post-merge / badges (push) Successful in 1m16s
Post-merge / sync-wiki (push) Successful in 1m25s
2026-06-27 13:27:59 +00:00
gitea-actions-bot c31ec312ac chore: update badge URLs to commit b3f0d6da [skip ci] 2026-06-27 13:16:12 +00:00
emil c67b810e58 GRM-98: ci: add --auto-login to publish, bump devx>=0.21.0
Post-merge / release (push) Failing after 11s
Post-merge / detect-type (push) Successful in 45s
Post-merge / validate-commit-msg (push) Successful in 52s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 52s
Post-merge / configure-repo (push) Successful in 51s
Post-merge / badges (push) Successful in 1m13s
Post-merge / sync-wiki (push) Successful in 1m14s
2026-06-27 13:14:13 +00:00
gitea-actions-bot ef16b07cdf chore: update badge URLs to commit 9addc544 [skip ci] 2026-06-27 12:34:11 +00:00
emil 2c849c7324 GRM-97: ci: use pre-built tier images for all CI workflows
Post-merge / detect-type (push) Successful in 37s
Post-merge / validate-commit-msg (push) Successful in 57s
Post-merge / release (push) Successful in 48s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 46s
Post-merge / badges (push) Successful in 1m9s
Post-merge / configure-repo (push) Successful in 41s
Post-merge / sync-wiki (push) Successful in 1m11s
2026-06-27 12:32:13 +00:00
gitea-actions-bot 5804a18974 chore: update badge URLs to commit a7e1f518 [skip ci] 2026-06-27 00:24:48 +00:00
gitea-actions-bot 9dbe20ba73 chore: update badge URLs to commit 63466f88 [skip ci] 2026-06-27 00:21:48 +00:00
grm-ci-bot b92c87ba68 release: v0.10.1
Post-merge / detect-type (push) Successful in 1m58s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 1m23s
2026-06-27 02:21:19 +02:00
emil dc4430d160 GRM-96: ci: add molecule test weights to pyproject.toml
Post-merge / detect-type (push) Successful in 1m12s
Post-merge / release (push) Successful in 1m38s
Post-merge / validate-commit-msg (push) Successful in 1m39s
Post-merge / badges (push) Successful in 2m8s
Post-merge / vikunja (push) Successful in 2m12s
Post-merge / sync-wiki (push) Successful in 2m48s
Post-merge / configure-repo (push) Successful in 1m30s
Post-merge / publish (push) Successful in 1m27s
2026-06-27 00:18:31 +00:00
gitea-actions-bot 7aa0ebaefe chore: update badge URLs to commit 182f11ca [skip ci] 2026-06-26 19:42:47 +00:00
emil e93da43219 GRM-95: refactor: consolidate publish.yml into post-merge.yml
Post-merge / detect-type (push) Successful in 1m14s
Post-merge / badges (push) Successful in 1m45s
Post-merge / vikunja (push) Successful in 1m42s
Post-merge / configure-repo (push) Successful in 1m41s
Post-merge / validate-commit-msg (push) Successful in 2m1s
Post-merge / release (push) Successful in 2m19s
Post-merge / sync-wiki (push) Successful in 2m29s
Post-merge / publish (push) Has been skipped
2026-06-26 19:39:50 +00:00
gitea-actions-bot b131a2872d chore: update badge URLs to commit 886112ce [skip ci] 2026-06-26 19:17:41 +00:00
emil e4dd8f308f GRM-94: refactor: replace duplicated Makefile targets with devx.mak aliases
Post-merge / detect-type (push) Successful in 1m12s
Post-merge / release (push) Successful in 1m29s
Post-merge / validate-commit-msg (push) Successful in 1m47s
Post-merge / badges (push) Successful in 1m54s
Post-merge / vikunja (push) Successful in 1m56s
Post-merge / sync-wiki (push) Successful in 2m4s
Post-merge / configure-repo (push) Successful in 1m19s
2026-06-26 19:14:33 +00:00
gitea-actions-bot 467e0d66e6 chore: update badge URLs to commit c2d3c4bb [skip ci] 2026-06-26 18:06:12 +00:00
emil 6ee5b74bb5 GRM-93: ci: use make setup-ci consistently, decouple vikunja/sync-wiki from release
Post-merge / detect-type (push) Successful in 1m18s
Post-merge / validate-commit-msg (push) Successful in 1m45s
Post-merge / badges (push) Successful in 1m53s
Post-merge / vikunja (push) Successful in 1m54s
Post-merge / release (push) Successful in 2m13s
Post-merge / configure-repo (push) Successful in 1m53s
Post-merge / sync-wiki (push) Successful in 2m29s
2026-06-26 18:03:02 +00:00
gitea-actions-bot 21cc89899f chore: update badge URLs to commit c29fbfe0 [skip ci] 2026-06-26 15:54:42 +00:00
emil 0382e155a6 GRM-92: fix: set PYTHONPATH=src in publish Install CI tools step
Post-merge / detect-type (push) Successful in 18s
Post-merge / validate-commit-msg (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 40s
Post-merge / release (push) Successful in 1m48s
Post-merge / vikunja (push) Successful in 7s
Post-merge / badges (push) Successful in 1m25s
Post-merge / sync-wiki (push) Successful in 1m36s
2026-06-26 15:51:06 +00:00
gitea-actions-bot d87c0d7e9a chore: update badge URLs to commit b19d4b06 [skip ci] 2026-06-26 17:18:30 +02:00
gitea-actions-bot 4be480a18e chore: update badge URLs to commit f916dabb [skip ci] 2026-06-26 15:18:03 +00:00
grm-ci-bot de92f675ed release: v0.10.0
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Publish Release / publish (push) Failing after 16s
Post-merge / badges (push) Successful in 1m55s
2026-06-26 17:16:19 +02:00
emil b5803a8611 GRM-91: feat: adopt devx tools, devx.mak fragment, ci extra, remove legacy install-devx
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 12s
Post-merge / release (push) Successful in 1m41s
Post-merge / vikunja (push) Successful in 12s
Post-merge / badges (push) Successful in 1m37s
Post-merge / sync-wiki (push) Successful in 2m0s
2026-06-26 15:14:33 +00:00
gitea-actions-bot ec22d20a15 chore: update badge URLs to commit 47c7e239 [skip ci] 2026-06-26 12:03:12 +02:00
emil e96012af7f GRM-90: ci: bump devx version to v0.14.1
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 7s
Post-merge / release (push) Successful in 1m29s
Post-merge / vikunja (push) Successful in 13s
Post-merge / badges (push) Successful in 1m23s
Post-merge / sync-wiki (push) Successful in 2m0s
2026-06-26 10:00:03 +00:00
gitea-actions-bot addef7500c chore: update badge URLs to commit dd820c3d [skip ci] 2026-06-26 00:13:49 +02:00
emil 8a0d2c428b GRM-88: docs: fix AGENTS.md squash-merge format to use colon after task ID
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Failing after 1m16s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 1m8s
2026-06-25 22:11:11 +00:00
gitea-actions-bot d68fb6d272 chore: update badge URLs to commit 9b029d3a [skip ci] 2026-06-25 23:17:41 +02:00
emil 6721864b87 GRM-87: fix: always run publish in post-merge (idempotent) 2026-06-25 21:15:03 +00:00
gitea-actions-bot 06471d1403 chore: update badge URLs to commit c61f4cb6 [skip ci] 2026-06-25 01:23:53 +02:00
38 changed files with 1083 additions and 351 deletions
+5 -1
View File
@@ -15,7 +15,7 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
# If set, API checks are performed as a bonus but do NOT affect pass/fail.
# Required scopes: read:user, read:repository, read:admin (or just "admin")
# Generate token at: Settings → Applications → Generate New Token
# REPO_TOKEN=your-admin-api-token
# CI_GITEA_TOKEN=your-admin-api-token
# Integration test API retries (optional, default: 3).
# Number of times to retry API checks waiting for runner to appear.
@@ -37,6 +37,10 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
# Supported: en, bg, de, ru, zh
# GRM_LANG=en
# Gitea PyPI registry username (for private package access)
# Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL
CI_GITEA_USERNAME=emil
# devx configuration (GRM-specific overrides)
# Task prefix for Vikunja task IDs
DEVX_TASK_PREFIX=GRM
+64 -30
View File
@@ -5,16 +5,22 @@ on:
types: [opened, synchronize]
workflow_dispatch:
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs:
quality:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-quality
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=lint
- name: Lint all
run: |
. .venv/bin/activate
@@ -56,6 +62,7 @@ jobs:
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
@@ -63,8 +70,9 @@ jobs:
fetch-depth: 0
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-release
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Release dry-run validation
env:
PYTHONPATH: src
@@ -73,10 +81,11 @@ jobs:
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run || true
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 }}
@@ -87,8 +96,9 @@ jobs:
fetch-depth: 0
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-ci
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Detect changed paths
id: detect
env:
@@ -105,6 +115,7 @@ jobs:
needs: [detect-changes]
if: needs.detect-changes.outputs.ansible-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
outputs:
runner-count: ${{ steps.discover.outputs.runner-count }}
@@ -113,12 +124,13 @@ jobs:
- uses: actions/checkout@v4
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-ci
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Discover available runners
id: discover
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
PYTHONPATH: src
run: |
@@ -132,6 +144,7 @@ jobs:
needs: [quality, detect-changes, discover-runners]
if: needs.detect-changes.outputs.ansible-changed == 'true'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
strategy:
matrix:
@@ -140,8 +153,13 @@ jobs:
- uses: actions/checkout@v4
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-molecule
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,molecule
- name: Install Ansible collections
run: |
. .venv/bin/activate
python3 -m devx.tools.setup --skip-install --no-pre-commit --no-tea-login
- name: Discover assigned test pairs
env:
RUNNER_INDEX: ${{ matrix.runner-index }}
@@ -158,35 +176,46 @@ jobs:
run: |
. .venv/bin/activate
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
if ! python3 -c "import docker; docker.from_env().ping()" 2>/dev/null; then
echo "Docker not available in CI container — skipping molecule tests"
exit 0
fi
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
env:
GITEA_URL: ${{ github.server_url }}
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
RUN_ID: ${{ github.run_id }}
JOB_NAME: ${{ github.job }}
MATRIX_INDEX: ${{ matrix.runner-index }}
GITEA_REPOSITORY: ${{ github.repository }}
PYTHONPATH: src
DOCKER_HOST: unix:///var/run/docker.sock
pr-review:
if: github.event_name == 'pull_request'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Run automated PR review
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
set -euo pipefail
. .venv/bin/activate
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
@@ -196,29 +225,33 @@ jobs:
# from the branch name, validates the PR title, and squash-merges.
# Uses always() so it evaluates even when molecule-tests is skipped
# (Gitea Actions skips dependent jobs of skipped jobs by default).
needs: [quality, detect-changes, pr-review, molecule-tests]
needs: [quality, detect-changes, pr-review, molecule-tests, release-dry-run]
if: >-
always() &&
github.event_name == 'pull_request' &&
needs.quality.result == 'success' &&
needs.pr-review.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped') &&
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
- name: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Squash merge with task ID
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
@@ -228,6 +261,7 @@ jobs:
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }}
run: |
. .venv/bin/activate
python3 -m devx.ci.auto_merge \
"$HEAD_REF" \
"$PR_TITLE" \
+121 -77
View File
@@ -1,38 +1,43 @@
name: Post-merge
# Runs on every push to master. A single workflow with conditional jobs
# replaces the previous 4 separate workflows (release.yml, post-merge.yml,
# sync-wiki.yml, and the badges job from ci.yml).
# for release, publish, wiki sync, badges, and Vikunja task updates.
#
# Job dependency graph:
#
# detect-type ──┬── release (skip if release commit)
# detect-type ──┬── validate-commit-msg (skip if release commit)
# ├── release (skip if release commit)
# │ └── publish (needs release — builds & publishes to PyPI)
# ├── badges (ALWAYS runs — even on release commits)
# ├── configure-repo (independent — skip if release commit)
# ├── sync-wiki (needs release — skip if release commit/fails)
# └── vikunja (needs release — skip if release commit/fails)
# ├── sync-wiki (skip if release commit — runs for ALL merges)
# └── vikunja (skip if release commit — runs for ALL merges)
#
# sync-wiki and vikunja depend on release succeeding so that the wiki
# and task tracker are only updated when the code is actually released.
# If release fails, they are skipped to avoid leaving the wiki or
# Vikunja in an inconsistent state with the codebase on master.
# 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 depends on release so it picks up the latest version
# number. It 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.
# 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.py creates a "release: vX.Y.Z" commit, the release
# commit's post-merge run still updates badges (version badge picks
# up the new version). Other jobs skip. The tag push triggers publish.yml.
# When release creates a "release: vX.Y.Z" commit and tag, the publish
# job (which depends on release) builds and publishes the package to the
# Gitea PyPI registry. The release commit's post-merge run still updates
# badges (version badge picks up the new version). Other jobs skip.
on:
push:
branches: [master]
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs:
detect-type:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
outputs:
is-release: ${{ steps.check.outputs.is-release }}
@@ -40,34 +45,40 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Check if this is a release commit
id: check
env:
PYTHONPATH: src
run: python3 -m devx.ci.detect_release_commit
run: |
. .venv/bin/activate
python3 -m devx.ci.detect_release_commit
validate-commit-msg:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Validate latest commit message
env:
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
run: |
. .venv/bin/activate
git log -1 --format=%B > commit-msg.txt
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
rm -f commit-msg.txt
@@ -76,21 +87,26 @@ jobs:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15
outputs:
tag: ${{ steps.release-tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-release
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Configure git
run: |
git config user.name "grm-ci-bot"
git config user.email "grm-ci-bot@oblachno.fyi"
- name: Run release
id: release-tag
env:
PYTHONPATH: src
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
@@ -100,44 +116,65 @@ jobs:
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release
- name: Publish release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
echo "No tag found — skipping publish"
exit 0
fi
HEAD_MSG=$(git log -1 --format=%s)
if echo "$HEAD_MSG" | grep -q "^release: ${TAG}"; then
echo "Publishing release $TAG..."
python3 -m devx.ci.publish "$TAG" "${{ github.repository }}"
else
echo "HEAD is not a release commit for $TAG — skipping publish"
fi
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/release" \
--commit "${{ github.sha }}"
publish:
needs: [release]
if: needs.release.outputs.tag != ''
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release.outputs.tag }}
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci,lint
- name: Build and publish release
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish \
"${{ needs.release.outputs.tag }}" \
"${{ github.repository }}" --auto-login
- name: Notify on failure
if: failure()
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/publish" \
--commit "${{ github.sha }}"
sync-wiki:
needs: [detect-type, release]
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
@@ -145,11 +182,12 @@ jobs:
fetch-depth: 0
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-ci
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Sync documentation to wiki
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
@@ -157,11 +195,10 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
@@ -169,24 +206,26 @@ jobs:
--commit "${{ github.sha }}"
badges:
needs: [detect-type, release]
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.REPO_TOKEN }}
token: ${{ secrets.CI_GITEA_TOKEN }}
- name: Fetch latest master
run: |
git fetch origin master
git reset --hard origin/master
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-ci
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=lint
- name: Generate and push badges
env:
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
@@ -196,11 +235,10 @@ jobs:
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
@@ -208,33 +246,36 @@ jobs:
--commit "${{ github.sha }}"
vikunja:
needs: [detect-type, release]
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Update Vikunja task
env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
PYTHONPATH: src
DEVX_TASK_PREFIX: GRM
DEVX_VIKUNJA_PROJECT_ID: 6
run: python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
run: |
. .venv/bin/activate
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
@@ -245,29 +286,32 @@ jobs:
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: Install dependencies
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
- name: Set up environment
env:
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: make setup-image EXTRAS=ci
- name: Ensure branch protection and labels
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
DEVX_REPO_NAME: grm
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / quality (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
run: python3 -m devx.tools.configure_repo
run: |
. .venv/bin/activate
python3 -m devx.tools.configure_repo
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
python3 -m devx.ci.notify_failure --auto-login \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
-56
View File
@@ -1,56 +0,0 @@
name: Publish Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish (e.g. v0.7.0)'
required: true
type: string
jobs:
publish:
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install CI tools
run: |
. .env 2>/dev/null || true
python3 -m pip install --break-system-packages --target=src "devx==0.12.0" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
python3 -m devx.tools.install_tools --tool git-cliff --tool tea
- name: Install build tools
run: python3 -m pip install --break-system-packages build twine
- name: Configure tea login
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: |
export PATH="$HOME/.local/bin:$PATH"
tea login add --name grm --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
tea login default grm || true
- name: Build and publish release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish \
"${{ github.event.inputs.tag || github.ref_name }}" \
"${{ github.repository }}"
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_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 "publish" \
--commit "${{ github.sha }}"
+30 -24
View File
@@ -17,11 +17,10 @@ make workflow-check # workflow-lint + workflow-dryrun
```
`make setup` automatically installs all development tools:
- **Python deps** via `devx.tools.setup` (pip install -e .[dev], ansible-galaxy, pre-commit hooks)
- **devx package** via `make install-devx` (installs the devx package from git, providing all CI/CD tools)
- **Python deps** via `pip install -e .[dev]` (includes devx from Gitea PyPI registry, configured by `make configure-gitea-pypi`)
- **Post-install setup** via `devx.tools.setup --skip-install` (ansible-galaxy, pre-commit hooks, tea CLI login)
- **checkmake** via `devx.tools.install_checkmake` (Makefile linter)
- **actionlint, git-cliff, act_runner, tea** via `devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
- **tea CLI login** via `devx.tools.setup` (configures `tea login` from `.env` `REPO_TOKEN`)
## Workflow Verification (Before Push)
@@ -69,7 +68,7 @@ The auto-merge workflow enforces the APPROVE review check programmatically
as a defense-in-depth measure, but branch protection is the primary gate.
### 1. Create Vikunja Task
Create a task in Vikunja project 6 to get a `GRM-N` identifier.
Create a task in Vikunja project 6 via `make create-task -- --title "Task title" --description "<h2>...</h2>"` (requires `VIKUNJA_TOKEN` in `.env`). This prints the `GRM-N` identifier and next-step instructions.
### 2. Create Branch
```bash
@@ -91,7 +90,9 @@ docs: update README
```
### 5. Push and Create PR
- **PR title format**: `GRM-N: <vikunja task title>` (must match the Vikunja task title exactly)
- Push: `git push -u origin HEAD` (pre-push hook validates Vikunja task existence via `devx.tools.pre_push_check`)
- Create PR: `make create-pr` (creates a PR with title `GRM-N: <vikunja task title>`, auto-derived from the branch name and Vikunja task)
- Or both in one step: `make push-with-pr`
- PR body: summary of changes, `Closes GRM-N`
- Add `ready-to-merge` label **only after review is complete**
@@ -129,7 +130,7 @@ the **[manual]** items by reviewing the full diff
Post review comments using `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`):
```bash
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
--event REQUEST_CHANGES \
--body "Review summary" \
--comments-json comments.json
@@ -142,7 +143,7 @@ Fix each comment one by one, commit, and push. Re-review until satisfied.
Once all checklist items are verified and comments are addressed, post
an approval review with `--checklist-confirmed` and `--checklist-categories`:
```bash
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
--event APPROVE --checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "All 13 REVIEW_CHECKLIST.md categories verified. Architecture: <summary>. Security: <summary>. Tests: <summary>. Docs: <summary>."
@@ -160,13 +161,13 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
3. Wait for all CI checks to pass (including the `pr-review` job)
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated, no colon after GRM-N)
4. Squash-merge with title: `GRM-N: <conventional commit message>`
5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes (see below)
> **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge
> workflow by adding the `ready-to-merge` label. Manual merges bypass the
> `GRM-N <conventional>` format enforcement, producing incorrectly named commits.
> `GRM-N: <conventional>` format enforcement, producing incorrectly named commits.
> The auto-merge script validates the PR title matches the Vikunja task ID
> and conventional commit format before merging.
@@ -220,17 +221,22 @@ Vikunja task updates:
- `--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.
3. **sync-wiki** — Syncs documentation to the Gitea wiki. Runs for ALL
non-release commits (not just when release succeeds), so docs-only
changes still update the wiki.
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
Runs **after** the release job (even if release fails or is skipped) so the
version badge always reflects the latest state. The script fetches the
latest master before generating badges to pick up any release commits.
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.
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
non-release commits (not just when release succeeds), so infrastructure-only
changes still update the task tracker.
The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`)
which builds and publishes the package to PyPI.
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
@@ -303,7 +309,7 @@ The codebase enforces strict separation between the GRM tool and the devx packag
### tea CLI Integration
The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
**`devx.gitea_cli`** — Python wrapper around `tea` CLI with JSON output parsing:
- `TeaCLI.create_issue()` — Create issues with labels
@@ -351,18 +357,18 @@ platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
avoids dev tools importing directly from CI modules.
2. **Publish workflow** (`.gitea/workflows/publish.yml`):
- Triggers on tag push (`v*`)
- Validates `PYPI_TOKEN` is set (warns if missing)
2. **Publish job** (in `post-merge.yml`, needs: release):
- Runs after the release job creates a tag
- Gets the tag from `needs.release.outputs.tag`
- Builds the Python package
- Optionally publishes to PyPI (if `PYPI_TOKEN` is set)
- Publishes to the Gitea PyPI registry
- Creates a Gitea release with git-cliff-generated release notes
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
### git-cliff Commit Preprocessing
Merge commits on master have the format `GRM-N <conventional commit>`. The
`GRM-N ` prefix is not a valid conventional commit prefix, so `cliff.toml`
Merge commits on master have the format `GRM-N: <conventional commit>`. The
`GRM-N: ` prefix is not a valid conventional commit prefix, so `cliff.toml`
includes a `commit_preprocessors` entry that strips it before parsing. This
ensures all merged work appears in the changelog.
@@ -384,7 +390,7 @@ The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, r
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
| Branch commits | `<conventional commit>` | `feat: add review script` |
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
| Merge commit | `GRM-N <conventional commit>` | `GRM-33 feat: add review script` |
| Merge commit | `GRM-N: <conventional commit>` | `GRM-33: feat: add review script` |
### Configuration
+46
View File
@@ -2,6 +2,52 @@
All notable changes to this project will be documented in this file.
## [0.10.3] - 2026-06-27
### Bug Fixes
- Install hadolint on-the-fly in setup-image
- Revert EXTRAS=ci default in setup-image
- Add EXTRAS=ci to all setup-image calls, workflow-level CI_GITEA_TOKEN
### Refactor
- Remove hadolint on-the-fly install workaround
- Use devx Makefile aliases, bump devx>=0.23.0
## [0.10.2] - 2026-06-27
### Bug Fixes
- Setup-image configures Gitea PyPI registry and shows pip errors
- Gate auto-merge on release-dry-run and unmask failures
- Bump devx>=0.22.0 and remove REPO_TOKEN alias
### Refactor
- Rename REPO_TOKEN to CI_GITEA_TOKEN, consolidate env vars
## [0.10.1] - 2026-06-27
### Bug Fixes
- Set PYTHONPATH=src in publish Install CI tools step
### Refactor
- Replace duplicated Makefile targets with devx.mak aliases
- Consolidate publish.yml into post-merge.yml
## [0.10.0] - 2026-06-26
### Features
- Adopt devx tools, devx.mak fragment, ci extra, remove legacy install-devx
### Bug Fixes
- Always run publish in post-merge (idempotent)
## [0.9.0] - 2026-06-24
### Features
+93 -80
View File
@@ -1,4 +1,6 @@
.PHONY: all setup setup-ci setup-quality setup-molecule setup-release install-devx install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools
.PHONY: all setup setup-ci setup-quality setup-molecule setup-release setup-image install update lint ansible-lint makefile-lint lint-all lint-ruff lint-format lint-bandit lint-deps typecheck checkmake install-hooks test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools
.PHONY: configure-gitea-pypi
.PHONY: create-task create-pr push-with-pr git-push
PYTHON := python3
VENV := .venv
@@ -7,45 +9,85 @@ CHECKMAKE := $(shell command -v checkmake 2>/dev/null || echo $(HOME)/go/bin/che
all: setup
# Pinned devx version — update this when upgrading devx.
# All workflow files (.gitea/workflows/*.yml) must be updated to match.
DEVX_VERSION := v0.12.0
# --- devx.mak include (shared Makefile targets) -------------------------------
# Set DEVX_PYTHON before including devx.mak so it uses the venv Python.
DEVX_PYTHON := $(BIN)/python
DEVX_VENV := $(VENV)
DEVX_BIN := $(BIN)
DEVX_COV_PKG := src/gitea_runner_manager
DEVX_TEST_PATHS := tests/ scripts/tests/
DEVX_LINT_PATHS := src/ scripts/ tests/
install-devx: $(VENV)/bin/activate
@# REPO_TOKEN may come from .env (local) or environment (CI secrets)
@if [ -z "$$REPO_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
if [ -z "$$REPO_TOKEN" ]; then echo "REPO_TOKEN not set (check .env or environment)"; exit 1; fi; \
$(BIN)/pip install "devx==$(shell echo $(DEVX_VERSION) | sed 's/^v//')" \
--extra-index-url "https://emil:$$REPO_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
# Include shared targets from devx package (create-task, create-pr, push-with-pr,
# check-config, workflow-lint, lint-ruff, clean, venv, .env, activate-scripts,
# install-hooks, install-tools, configure-gitea-pypi, checkmake, etc.)
# Silent if devx not installed yet — run 'make setup' first.
DEVX_MAK := $(shell $(BIN)/python -c \
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
2>/dev/null)
-include $(DEVX_MAK)
# Full setup for local development (all deps, tools, collections, hooks)
# install-devx must run before install-tools (which uses devx modules)
setup: $(VENV)/bin/activate .env activate-scripts install-devx checkmake install-tools
# devx is installed via pip install -e .[dev] (devx is in dev extra)
setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[dev]'
@$(BIN)/python -m devx.tools.install_checkmake
@$(BIN)/python -m devx.tools.install_tools
@export PATH="$(HOME)/.local/bin:$$PATH"; \
$(BIN)/python -m devx.tools.setup --bin "$(BIN)"
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
# (detect-changes, discover-runners, pr-review, sync-wiki, badges)
# badges job runs generate_badges.py which needs ruff, pyright, bandit
setup-ci: $(VENV)/bin/activate .env install-devx
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-ansible-collections --no-pre-commit --no-tea-login
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
# Setup for the quality job (lint + test deps, actionlint tool)
# install-devx must run before install-tools (which uses devx modules)
setup-quality: $(VENV)/bin/activate .env install-devx install-tools
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.install_tools
@export PATH="$(HOME)/.local/bin:$$PATH"; \
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --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
# Full setup for molecule testing (needs ansible, molecule, collections)
setup-molecule: $(VENV)/bin/activate .env install-devx install-tools
setup-molecule: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,molecule]'
@$(BIN)/python -m devx.tools.install_tools
@export PATH="$(HOME)/.local/bin:$$PATH"; \
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,molecule" --no-pre-commit --no-tea-login
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-pre-commit --no-tea-login
# Setup for release jobs (needs git-cliff, tea, and lint tools for release.py)
setup-release: $(VENV)/bin/activate .env install-devx
setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
@$(PIP_INSTALL) install -e '.[ci,lint]'
@$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea
@export PATH="$(HOME)/.local/bin:$$PATH"; \
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-ansible-collections --no-pre-commit
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
# Usage: make setup-image (runtime deps only, devx from image)
# make setup-image EXTRAS=lint (runtime + lint deps, e.g. ansible-lint)
# make setup-image EXTRAS=ci,lint (runtime + ci + lint deps, upgrades devx)
# NOTE: Cannot alias to devx-setup-image because the venv must exist before
# devx.mak can be included (chicken-and-egg). This standalone target creates
# the venv symlink first, then installs the project.
setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$$CI_GITEA_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
# Helper: run pip install with Gitea registry configured
# Usage: $(PIP_INSTALL) install -e '.[ci,lint]'
PIP_INSTALL := if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$$CI_GITEA_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \
$(BIN)/pip
$(VENV)/bin/activate:
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
$(PYTHON) -m venv $(VENV)
$(BIN)/pip install --upgrade pip setuptools wheel
.env:
@if [ ! -f .env ]; then \
@@ -53,27 +95,11 @@ setup-release: $(VENV)/bin/activate .env install-devx
echo "Created .env from .env.example — please edit it with your credentials."; \
fi
$(VENV)/bin/activate:
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
$(PYTHON) -m venv $(VENV)
$(BIN)/pip install --upgrade pip setuptools wheel
activate-scripts: $(VENV)/bin/activate
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
install-hooks:
@cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
@cp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
@echo "Git hooks installed."
checkmake: install-devx
@$(BIN)/python -m devx.tools.install_checkmake
install-tools: install-devx
@$(BIN)/python -m devx.tools.install_tools
install:
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
$(BIN)/grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
@@ -106,25 +132,31 @@ remove:
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make remove HOST=192.168.1.10"; exit 1; fi
$(BIN)/grm remove $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
lint-ruff:
$(BIN)/ruff check src/ tests/
# --- Aliases to devx.mak targets ----------------------------------------------
lint-ruff: devx-lint-ruff
lint-format: devx-lint-format
typecheck: devx-typecheck
lint-bandit: devx-lint-bandit
lint-deps: devx-lint-deps
lint: devx-lint
checkmake: devx-checkmake
install-tools: devx-install-tools
install-hooks: devx-install-hooks
clean: devx-clean
test-unit: devx-test-unit
lint-format:
$(BIN)/ruff format --check src/ tests/
# Override devx-pytest-cov to cover both src/ and scripts/
pytest-cov:
@$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100
workflow-lint: devx-workflow-lint
workflow-dryrun: devx-workflow-dryrun
workflow-check: devx-workflow-check
typecheck:
$(BIN)/pyright
lint: lint-ruff lint-format typecheck lint-bandit
lint-bandit:
$(BIN)/bandit -r src/
lint-deps:
@echo "Checking dependencies for known vulnerabilities..."
@.venv/bin/python -m ensurepip 2>/dev/null || true
@PIPAPI_PYTHON_LOCATION=$$(pwd)/.venv/bin/python \
.venv/bin/pip-audit --desc --skip-editable 2>&1 || true
configure-gitea-pypi:
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)."
ansible-lint:
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
@@ -138,30 +170,9 @@ makefile-lint:
lint-all: lint ansible-lint makefile-lint workflow-lint
workflow-lint:
@command -v actionlint >/dev/null 2>&1 || { \
echo "actionlint not found. Install: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)"; \
exit 1; \
}
actionlint -config-file .gitea/actionlint.yaml .gitea/workflows/*.yml
workflow-dryrun:
@command -v act_runner >/dev/null 2>&1 || { echo "act_runner not found. Install: https://gitea.com/gitea/act_runner/releases"; exit 1; }
@echo "Dry-running all workflows (no Docker containers started)..."
act_runner exec --dryrun -W .gitea/workflows/ 2>&1 | grep -E 'DRYRUN|ERROR|FAIL|Job'
workflow-check: workflow-lint workflow-dryrun
@echo "Workflow checks passed (static lint + dry-run)."
test-unit:
$(BIN)/pytest tests/unit/ -v --no-cov
test-integration:
$(BIN)/pytest tests/integration/ -v --no-cov
pytest-cov:
$(BIN)/pytest tests/ -v --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100
MOLECULE := $(realpath $(BIN))/molecule
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
@@ -177,7 +188,9 @@ test: test-all
test-all: pytest-cov molecule
clean:
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
rm -rf .coverage htmlcov/ .molecule/
# --- Vikunja task and PR management (via devx.mak fragment) -------------------
# Aliases for project-specific target names
create-task: devx-create-task
create-pr: devx-create-pr
push-with-pr: devx-push-with-pr
git-push: devx-push
+7 -7
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/python.svg)](https://www.python.org/downloads/)
## Why GRM?
@@ -169,7 +169,7 @@ GRM reads configuration from a `.env` file in the current directory (loaded auto
| Variable | Default | Description |
|----------|---------|-------------|
| `REPO_TOKEN` | — | Gitea admin API token for optional post-install API verification |
| `CI_GITEA_TOKEN` | — | Gitea admin API token for optional post-install API verification |
| `GITEA_INTEGRATION_RETRIES` | `3` | Number of API check retries during integration test |
| `GITEA_RUNNER_USER` | current login | Default SSH user (overrides `--user`) |
| `GITEA_RUNNER_KEY` | — | Default SSH key path (overrides `--key`) |
+1 -1
View File
@@ -9,7 +9,7 @@
| Vikunja task not updated after merge | VIKUNJA_TOKEN expired or task ID missing from commit | Regenerate token; verify merge commit has `GRM-N:` prefix |
| Post-merge can't find Vikunja task | Task not in project 6 or identifier mismatch | Verify task exists in Vikunja project 6 with correct identifier |
| `make pytest-cov` fails | Coverage below 100% | Add tests for new code paths |
| `devx.tools.configure_repo` fails | REPO_TOKEN missing or invalid | Set token with repo admin scope and re-run |
| `devx.tools.configure_repo` fails | CI_GITEA_TOKEN missing or invalid | Set token with repo admin scope and re-run |
| `configure_repo` sets wrong status checks | Stale `BRANCH_PROTECTION_CONFIG` | Updated to include `(pull_request)` suffix; re-run `configure_repo` |
| Token visible in `ps aux` during install | Old version passed tokens via command line | Fixed: tokens now passed via temp file with `0600` permissions |
| `remove-runner.yml` leaves lingering enabled | Old version didn't disable lingering | Fixed: now runs `loginctl disable-linger` and removes subuid/subgid |
+48
View File
@@ -0,0 +1,48 @@
---
- name: Restart Gitea Actions runner (stop, prune images, start)
hosts: all
become: true
vars:
prune_images: true
tasks:
- name: Include systemd availability check
ansible.builtin.include_role:
name: gitea-runner
tasks_from: systemd_check.yml
- name: Resolve runner UID
ansible.builtin.include_role:
name: gitea-runner
tasks_from: resolve_uid.yml
- name: Stop gitea-runner user service
ansible.builtin.command: systemctl --user stop gitea-runner
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when: systemd_available.stat.exists
changed_when: true
- name: Prune stale runner images from rootless Docker
ansible.builtin.command:
cmd: python3 {{ playbook_dir }}/../scripts/prune_runner_images.py
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
when:
- systemd_available.stat.exists
- prune_images | default(true)
changed_when: true
failed_when: false
- name: Start gitea-runner user service
ansible.builtin.command: systemctl --user start gitea-runner
become: true
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when: systemd_available.stat.exists
changed_when: true
@@ -19,7 +19,7 @@
- name: Assert runner user is absent
ansible.builtin.assert:
that:
- user_check.ansible_facts.getent_passwd is none or
- user_check is failed or
gitea_runner_service_user not in (user_check.ansible_facts.getent_passwd | default({}))
fail_msg: "Runner user still exists after removal"
@@ -0,0 +1,25 @@
---
# Resolve runner identity facts for stop/start/status/restart playbooks.
# These playbooks use include_role with tasks_from, which does NOT expose
# role defaults to the playbook's task-level keywords (become_user, etc).
# We set the facts explicitly here so they're available everywhere.
- name: Resolve runner service user
ansible.builtin.set_fact:
gitea_runner_service_user: "{{ gitea_runner_user_prefix | default('grm-') }}{{ runner_name }}"
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
gitea_runner_base_config_dir: "/etc/gitea-runner"
- name: Resolve runner data and config dirs
ansible.builtin.set_fact:
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
- name: Resolve runner service user UID
ansible.builtin.getent:
database: passwd
key: "{{ gitea_runner_service_user }}"
- name: Set runner UID fact
ansible.builtin.set_fact:
gitea_runner_uid: "{{ getent_passwd[gitea_runner_service_user][1] }}"
@@ -20,6 +20,7 @@
- name: Enable lingering for runner user
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
changed_when: not linger_stat.stat.exists
when: systemd_available.stat.exists
- name: Ensure subuid entry for runner user
ansible.builtin.lineinfile:
+8 -3
View File
@@ -9,9 +9,14 @@
name: gitea-runner
tasks_from: systemd_check.yml
- name: Resolve runner UID
ansible.builtin.include_role:
name: gitea-runner
tasks_from: resolve_uid.yml
- name: Check if runner is already registered
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}/.runner"
path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_registered
- name: Include registration if not registered
@@ -25,8 +30,8 @@
- name: Start gitea-runner user service
ansible.builtin.command: systemctl --user start gitea-runner
become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when: systemd_available.stat.exists
changed_when: true
+8 -3
View File
@@ -9,12 +9,17 @@
name: gitea-runner
tasks_from: systemd_check.yml
- name: Resolve runner UID
ansible.builtin.include_role:
name: gitea-runner
tasks_from: resolve_uid.yml
- name: Check systemd user service status
ansible.builtin.command: systemctl --user is-active gitea-runner
become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
register: service_status
changed_when: false
when: systemd_available.stat.exists
@@ -26,7 +31,7 @@
- name: Check runner registration file
ansible.builtin.stat:
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}/.runner"
path: "{{ gitea_runner_data_dir }}/.runner"
register: runner_file_stat
- name: Report runner registration
+7 -2
View File
@@ -9,11 +9,16 @@
name: gitea-runner
tasks_from: systemd_check.yml
- name: Resolve runner UID
ansible.builtin.include_role:
name: gitea-runner
tasks_from: resolve_uid.yml
- name: Stop gitea-runner user service
ansible.builtin.command: systemctl --user stop gitea-runner
become: true
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
become_user: "{{ gitea_runner_service_user }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
when: systemd_available.stat.exists
changed_when: true
+6 -6
View File
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/ea0b50026e4fe84bb2f4453e2d9d5d7e35d50c16/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/fc36a6d7330ffc2a6e68e3f8ce5a1dcd3b2c0983/python.svg)](https://www.python.org/downloads/)
## Overview
+2 -2
View File
@@ -74,7 +74,7 @@ Review the full diff (`git diff master...HEAD`) focusing on:
Post review comments using `devx.ci.pr_review`:
```bash
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
--event REQUEST_CHANGES \
--body "Review summary" \
--comments-json comments.json
@@ -89,7 +89,7 @@ Fix each comment one by one, commit, and push. Re-review until satisfied.
Once all comments are addressed:
```bash
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
--event APPROVE \
--body "All comments addressed. LGTM."
```
+2 -2
View File
@@ -137,7 +137,7 @@ Review the full diff (`git diff master...HEAD`) focusing on:
Post review comments using `devx.ci.review_pr`:
```bash
REPO_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
--event REQUEST_CHANGES \
--body "Review summary" \
--comments-json comments.json
@@ -152,7 +152,7 @@ Fix each comment one by one, commit, and push. Re-review until satisfied.
Once all comments are addressed, post an approval review:
```bash
REPO_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
CI_GITEA_TOKEN=<token> python -m devx.ci.review_pr <pr_number> <owner/repo> \
--event APPROVE --checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "All 13 REVIEW_CHECKLIST.md categories verified."
+1 -1
View File
@@ -108,7 +108,7 @@ cp .env.example .env
#### Admin API token (optional)
Set `REPO_TOKEN` to enable informational API checks during integration test. This is **optional** — the test primarily verifies the runner by checking:
Set `CI_GITEA_TOKEN` to enable informational API checks during integration test. This is **optional** — the test primarily verifies the runner by checking:
1. **`.runner` registration file** exists and contains valid JSON (proves successful registration)
2. **Systemd user service** is active (proves daemon is polling for jobs)
+2 -2
View File
@@ -51,7 +51,7 @@ grm install <host> [options]
| `--name` | `-n` | hostname | Gitea Runner name |
| `--token` | `-t` | `GITEA_REGISTRATION_TOKEN` env | Registration token |
| `--url` | — | `GITEA_URL` env | Gitea URL |
| `--admin-token` | `-a` | `REPO_TOKEN` env | Gitea admin API token for integration test |
| `--admin-token` | `-a` | `CI_GITEA_TOKEN` env | Gitea admin API token for integration test |
| `--integration-retries` | `-r` | `3` (`GITEA_INTEGRATION_RETRIES` env) | Integration test API retries |
| `--labels` | `-l` | `GITEA_RUNNER_LABELS` env | Runner labels for Gitea Actions. Example: `docker:docker://alpine:latest` |
| `--ask-become-pass/--no-ask-become-pass` | — | `--ask-become-pass` | Prompt for sudo password (default) or skip it |
@@ -294,7 +294,7 @@ All CLI options can be set via environment variables (loaded from `.env` via pyt
|----------|---------|-------------|
| `GITEA_URL` | `install`, `disable`, `remove` | Gitea instance URL |
| `GITEA_REGISTRATION_TOKEN` | `install`, `disable`, `remove` | Runner registration token |
| `REPO_TOKEN` | `install` | Admin API token for integration test |
| `CI_GITEA_TOKEN` | `install` | Admin API token for integration test |
| `GITEA_INTEGRATION_RETRIES` | `install` | API check retries (default: 3) |
| `GITEA_RUNNER_USER` | `install`, `update` | Default SSH user |
| `GITEA_RUNNER_KEY` | `install`, `update` | Default SSH key path |
+2 -2
View File
@@ -10,9 +10,9 @@ There are three levels of registration tokens, depending on which repositories t
Set the token as `GITEA_REGISTRATION_TOKEN` in your `.env` file or pass it via `--token` on the command line.
### What is the REPO_TOKEN and do I need it?
### What is the CI_GITEA_TOKEN and do I need it?
`REPO_TOKEN` is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the `.runner` file and systemd service, not the API check.
`CI_GITEA_TOKEN` is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the `.runner` file and systemd service, not the API check.
To generate one: Settings → Applications → Generate New Token, with the `admin` scope (or at minimum `read:user`, `read:repository`, `read:admin`).
+3 -3
View File
@@ -84,7 +84,7 @@ GITEA_URL=https://git.example.com
GITEA_REGISTRATION_TOKEN=GRxxxxxxxxxxxxxxxxxx
# Admin API token from Step 2 (optional)
REPO_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CI_GITEA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
### Environment Variables Reference
@@ -93,7 +93,7 @@ REPO_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|----------|----------|---------|-------------|
| `GITEA_URL` | Yes | — | Gitea instance URL (e.g., `https://git.example.com`) |
| `GITEA_REGISTRATION_TOKEN` | Yes | — | Runner registration token from Gitea admin panel |
| `REPO_TOKEN` | No | — | Admin API token for post-install verification |
| `CI_GITEA_TOKEN` | No | — | Admin API token for post-install verification |
| `GITEA_INTEGRATION_RETRIES` | No | `3` | API check retries (default: 3) |
| `GITEA_RUNNER_USER` | No | current login | Default SSH user (overrides `--user`) |
| `GITEA_RUNNER_KEY` | No | — | Default SSH key path (overrides `--key`) |
@@ -150,7 +150,7 @@ The installer performs an automated integration test that verifies:
You can also check the Gitea UI under **Actions → Runners** to confirm the runner appears as **Online**.
Optional: If `REPO_TOKEN` is set, the installer will also query the Gitea API and report whether the runner appears in the admin or repo runners list. This is purely informational.
Optional: If `CI_GITEA_TOKEN` is set, the installer will also query the Gitea API and report whether the runner appears in the admin or repo runners list. This is purely informational.
### Check runner status via CLI
+2 -2
View File
@@ -98,7 +98,7 @@ The test checks two things:
### API verification shows error status
If `REPO_TOKEN` is set, the integration test queries the Gitea API. If the API returns `401` or `403`, the token does not have sufficient permissions. This is **informational only** and does not affect pass/fail. The test passes as long as the `.runner` file exists and the systemd service is active.
If `CI_GITEA_TOKEN` is set, the integration test queries the Gitea API. If the API returns `401` or `403`, the token does not have sufficient permissions. This is **informational only** and does not affect pass/fail. The test passes as long as the `.runner` file exists and the systemd service is active.
## Logging and Diagnostics
@@ -191,7 +191,7 @@ If Docker is not installed, install it via your package manager or [Docker's off
| Vikunja task not updated after merge | VIKUNJA_TOKEN expired or task ID missing from commit | Regenerate token; verify merge commit has `GRM-N:` prefix |
| Post-merge can't find Vikunja task | Task not in project 6 or identifier mismatch | Verify task exists in Vikunja project 6 with correct identifier |
| `make pytest-cov` fails | Coverage below 100% | Add tests for new code paths |
| `devx.tools.configure_repo` fails | REPO_TOKEN missing or invalid | Set token with repo admin scope and re-run |
| `devx.tools.configure_repo` fails | CI_GITEA_TOKEN missing or invalid | Set token with repo admin scope and re-run |
| `configure_repo` sets wrong status checks | Stale `BRANCH_PROTECTION_CONFIG` | Updated to include `(pull_request)` suffix; re-run `configure_repo` |
| Token visible in `ps aux` during install | Old version passed tokens via command line | Fixed: tokens now passed via temp file with `0600` permissions |
| `remove-runner.yml` leaves lingering enabled | Old version didn't disable lingering | Fixed: now runs `loginctl disable-linger` and removes subuid/subgid |
+60 -5
View File
@@ -1,6 +1,61 @@
#!/usr/bin/env bash
# pre-push hook: fail if unit tests are too slow.
# Checks both total suite time (10s) and per-test time (0.5s).
# Aligned with CI (ci.yml uses same thresholds).
set -e
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
# pre-push hook: validate Vikunja task exists and tests are fast.
#
# This catches issues that would otherwise only surface in CI:
# - Branch name missing task ID (e.g., GRM-N)
# - Vikunja task does not exist for the task ID in the branch name
# - Unit tests too slow (total > 4s, per-test > 0.5s)
#
# Uses devx.tools.pre_push_check for reusable validation logic.
# Project config (task prefix, Vikunja project ID) is read from
# [tool.devx] in pyproject.toml by devx.config — no hardcoded values here.
#
# Bootstrap resilience: if devx is not importable (e.g., during devx
# upgrades), the hook prints a warning and allows the push.
# Determine the branch being pushed
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
if [ -z "$BRANCH" ] || [ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ]; then
exit 0
fi
# Load .env if present (for VIKUNJA_TOKEN)
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. .env
set +a
fi
# Find the Python interpreter with devx installed
if [ -f .venv/bin/python ]; then
PY=.venv/bin/python
elif [ -x "${HOME}/.pyenv/bin/pyenv" ]; then
export PYENV_ROOT="${HOME}/.pyenv"
export PATH="${PYENV_ROOT}/bin:${PYENV_ROOT}/shims:${PATH}"
eval "$("${PYENV_ROOT}/bin/pyenv" init -)" 2>/dev/null || true
eval "$("${PYENV_ROOT}/bin/pyenv" virtualenv-init -)" 2>/dev/null || true
pyenv activate gitea-runner-manager 2>/dev/null || true
PY=python3
else
PY=python3
fi
# Bootstrap resilience: if devx is not importable, warn but allow the push
if ! $PY -c "import devx.tools.pre_push_check" 2>/dev/null; then
echo "WARNING: devx not installed — pre-push check skipped."
echo "Run 'make setup' to install devx."
exit 0
fi
# Run pre-push validation via devx
$PY -m devx.tools.pre_push_check --branch "$BRANCH" || {
echo ""
echo "Pre-push validation failed. Fix the issues above before pushing."
echo "To bypass (NOT recommended): git push --no-verify"
exit 1
}
# Check test speed (aligned with CI thresholds)
$PY -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
+26 -3
View File
@@ -34,6 +34,8 @@ ci = [
"pytest-cov>=7.1.0",
"build>=1.5.0",
"twine>=6.2.0",
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
"devx>=0.23.0",
]
# Lint and type-checking tools (quality job)
lint = [
@@ -52,6 +54,8 @@ molecule = [
# Full dev environment (local development, includes everything)
dev = [
"gitea-runner-manager[ci,lint,molecule]",
# Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr)
"devx>=0.23.0",
# Non-Python dev dependency: checkmake (Makefile linter)
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
]
@@ -63,9 +67,9 @@ where = ["src"]
gitea_runner_manager = ["translations.json"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "--cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100"
testpaths = ["tests", "scripts/tests"]
pythonpath = ["src", "scripts"]
addopts = "--cov=src/gitea_runner_manager --cov=scripts/prune_runner_images.py --cov-report=term-missing --cov-fail-under=100"
markers = [
"integration: marks tests as integration tests (not counted in coverage)",
]
@@ -91,6 +95,25 @@ strict = ["src/gitea_runner_manager"]
# Change classification — determines which changes trigger a release
# ---------------------------------------------------------------------------
# The framework provides DEFAULT_INFRASTRUCTURE (CI workflows, tests, docs,
# Project-specific devx configuration (read by devx.config)
[tool.devx]
task_prefix = "GRM"
vikunja_project_id = 6
repo_owner = "oblachno-oss"
repo_name = "gitea-runner-manager"
# Molecule test weights for LPT scheduling.
# GRM has a single role (gitea-runner) with 7 scenarios.
# Weights are estimates — recalibrate from CI logs after next run.
[tool.devx.molecule.weights]
"multi-instance" = 8
"lifecycle" = 6
"update" = 5
"default" = 4
"deregister" = 3
"remove" = 3
"template-content" = 2
# lint config, etc.) that applies to any Python project. We only specify
# what's different about GRM.
[tool.devx.classify]
View File
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Prune stale runner images from a rootless Docker daemon.
Usage:
python3 prune_runner_images.py [--dry-run]
Removes all images matching the runner-images pattern from the local
Docker daemon so the runner pulls a fresh :latest on the next job.
Environment variables:
DOCKER_HOST Docker daemon socket (set by caller)
XDG_RUNTIME_DIR Runtime directory (set by caller)
"""
from __future__ import annotations
import argparse
import re
import subprocess # nosec B404
import sys
from collections.abc import Sequence
#: Pattern for images we want to prune (repository:tag format).
IMAGE_PATTERN = re.compile(r"runner-images/(ci-base|ci-quality|ci-full)")
def list_docker_images() -> list[str]:
"""List all images in the local Docker daemon as repository:tag strings.
Returns:
List of ``repository:tag`` strings (excluding ``<none>`` entries).
"""
result = subprocess.run( # nosec B603
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
capture_output=True,
text=True,
check=True,
)
return [line.strip() for line in result.stdout.splitlines() if line.strip() and "<none>" not in line]
def filter_runner_images(images: Sequence[str]) -> list[str]:
"""Filter image list to only runner-images entries.
Args:
images: List of ``repository:tag`` strings.
Returns:
Subset matching the runner-images pattern.
"""
return [img for img in images if IMAGE_PATTERN.search(img)]
def remove_images(images: Sequence[str], dry_run: bool = False) -> list[str]:
"""Remove the given images from the local Docker daemon.
Args:
images: List of ``repository:tag`` strings to remove.
dry_run: If True, print what would be removed but don't execute.
Returns:
List of images that were removed (or would be removed in dry-run).
"""
removed: list[str] = []
for img in images:
if dry_run:
print(f"[dry-run] would remove: {img}")
removed.append(img)
continue
result = subprocess.run( # nosec B603
["docker", "rmi", "-f", img],
capture_output=True,
text=True,
)
if result.returncode == 0:
print(f"removed: {img}")
removed.append(img)
else:
print(f"failed to remove {img}: {result.stderr.strip()}", file=sys.stderr)
return removed
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Prune stale runner images.")
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be removed without executing.",
)
args = parser.parse_args(argv)
all_images = list_docker_images()
runner_images = filter_runner_images(all_images)
if not runner_images:
print("no runner images found to prune")
return 0
removed = remove_images(runner_images, dry_run=args.dry_run)
print(f"pruned {len(removed)} image(s)")
return 0
if __name__ == "__main__": # pragma: no cover
sys.exit(main())
View File
+144
View File
@@ -0,0 +1,144 @@
"""Tests for prune_runner_images.py."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from scripts.prune_runner_images import (
filter_runner_images,
list_docker_images,
main,
remove_images,
)
class TestListDockerImages:
"""Tests for list_docker_images()."""
@patch("scripts.prune_runner_images.subprocess.run")
def test_returns_images_from_docker(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout="repo1:tag1\nrepo2:tag2\n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo1:tag1", "repo2:tag2"]
@patch("scripts.prune_runner_images.subprocess.run")
def test_filters_none_entries(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout="repo:tag\n<none>:<none>\nother:v1\n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo:tag", "other:v1"]
@patch("scripts.prune_runner_images.subprocess.run")
def test_empty_output(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(stdout="", returncode=0)
result = list_docker_images()
assert result == []
@patch("scripts.prune_runner_images.subprocess.run")
def test_strips_whitespace(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(
stdout=" repo:tag \n\n other:v2 \n",
returncode=0,
)
result = list_docker_images()
assert result == ["repo:tag", "other:v2"]
class TestFilterRunnerImages:
"""Tests for filter_runner_images()."""
def test_matches_runner_images(self) -> None:
images = [
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest",
]
result = filter_runner_images(images)
assert len(result) == 3
def test_excludes_non_runner_images(self) -> None:
images = [
"docker.io/library/python:3.12",
"docker.io/library/nginx:latest",
"git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest",
]
result = filter_runner_images(images)
assert len(result) == 1
assert "ci-base" in result[0]
def test_empty_list(self) -> None:
assert filter_runner_images([]) == []
def test_no_matches(self) -> None:
images = ["python:3.12", "nginx:latest"]
assert filter_runner_images(images) == []
class TestRemoveImages:
"""Tests for remove_images()."""
@patch("scripts.prune_runner_images.subprocess.run")
def test_removes_images(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=0, stderr="")
images = ["repo/ci-base:latest", "repo/ci-quality:latest"]
removed = remove_images(images)
assert removed == images
assert mock_run.call_count == 2
@patch("scripts.prune_runner_images.subprocess.run")
def test_dry_run_does_not_call_docker(self, mock_run: MagicMock) -> None:
images = ["repo/ci-base:latest"]
removed = remove_images(images, dry_run=True)
assert removed == images
mock_run.assert_not_called()
@patch("scripts.prune_runner_images.subprocess.run")
def test_failed_removal_not_in_result(self, mock_run: MagicMock) -> None:
mock_run.return_value = MagicMock(returncode=1, stderr="image in use")
images = ["repo/ci-base:latest"]
removed = remove_images(images)
assert removed == []
@patch("scripts.prune_runner_images.subprocess.run")
def test_empty_list(self, mock_run: MagicMock) -> None:
removed = remove_images([])
assert removed == []
mock_run.assert_not_called()
class TestMain:
"""Tests for main()."""
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_no_images(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = []
assert main([]) == 0
mock_remove.assert_not_called()
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_with_images(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = [
"repo/runner-images/ci-base:latest",
"python:3.12",
]
mock_remove.return_value = ["repo/runner-images/ci-base:latest"]
assert main([]) == 0
mock_remove.assert_called_once()
@patch("scripts.prune_runner_images.list_docker_images")
@patch("scripts.prune_runner_images.remove_images")
def test_dry_run_flag(self, mock_remove: MagicMock, mock_list: MagicMock) -> None:
mock_list.return_value = ["repo/runner-images/ci-base:latest"]
mock_remove.return_value = ["repo/runner-images/ci-base:latest"]
assert main(["--dry-run"]) == 0
mock_remove.assert_called_once_with(
["repo/runner-images/ci-base:latest"],
dry_run=True,
)
+1 -1
View File
@@ -1,3 +1,3 @@
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
__version__ = "0.9.0"
__version__ = "0.10.3"
+54 -4
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import functools
import os
import sys
from collections.abc import Callable
from typing import Any
@@ -77,8 +78,8 @@ def cli() -> None:
@click.option(
"--admin-token",
"-a",
default=lambda: os.getenv("REPO_TOKEN"),
help=_("Gitea admin API token for integration test (env: REPO_TOKEN)"),
default=lambda: os.getenv("CI_GITEA_TOKEN"),
help=_("Gitea admin API token for integration test (env: CI_GITEA_TOKEN)"),
)
@click.option(
"--integration-retries",
@@ -203,6 +204,27 @@ def stop(
)
@cli.command(help=_("Restart a registered Gitea Runner (stop, prune images, start)."))
@click.argument("runner_name")
@_runner_options
@_handle_errors("Restart failed: {error}")
def restart(
runner_name: str,
host: str | None,
user: str | None,
key: str | None,
ask_become_pass: bool,
) -> None:
manager = RunnerManager()
manager.restart(
name=runner_name,
host=host,
user=user,
key=key,
ask_become_pass=ask_become_pass,
)
@cli.command(help=_("Enable a registered Gitea Runner to start on boot."))
@click.argument("runner_name")
@_runner_options
@@ -325,11 +347,39 @@ def remove(
)
def _collect_become_pass(ask_become_pass: bool) -> str | None:
"""Collect sudo password for ad-hoc status checks.
When stdin is a TTY, prompts interactively with hidden input.
When stdin is piped, reads the first line (e.g. ``echo 'pass' | grm list``).
"""
if not ask_become_pass:
return None
if sys.stdin.isatty():
return (
click.prompt(
_("Sudo password"),
hide_input=True,
default="",
show_default=False,
)
or None
)
return sys.stdin.readline().strip() or None
@cli.command(name="list", help=_("List all registered runners with live status."))
@click.option(
"--ask-become-pass/--no-ask-become-pass",
default=True,
help=_("Prompt for sudo password once for all status checks (default)."),
)
@_handle_errors("List failed: {error}")
def list_runners() -> None:
def list_runners(ask_become_pass: bool) -> None:
become_pass = _collect_become_pass(ask_become_pass)
manager = RunnerManager()
runners = manager.list_runners()
runners = manager.list_runners(become_pass=become_pass)
if not runners:
click.echo(_("No runners registered. Use 'grm install' to add one."))
+32 -10
View File
@@ -2,11 +2,12 @@
from __future__ import annotations
import contextlib
import logging
import os
import re
import subprocess # nosec B404
import sys
import tempfile
from datetime import datetime
from pathlib import Path
@@ -88,8 +89,14 @@ class AnsibleExecutor:
become: bool = False,
ask_become_pass: bool = False,
check: bool = True,
become_pass: str | None = None,
) -> str:
"""Run an Ansible ad-hoc command and return stdout."""
"""Run an Ansible ad-hoc command and return stdout.
When ``become_pass`` is provided, it is passed via a temporary file
(``--become-password-file``) to avoid stdin consumption issues when
running multiple ad-hoc commands in sequence (e.g. ``grm list``).
"""
cmd = [
"ansible",
host,
@@ -104,16 +111,31 @@ class AnsibleExecutor:
cmd.extend(["--private-key", key])
if become:
cmd.append("--become")
if become and ask_become_pass and sys.stdin.isatty():
cmd.append("--ask-become-pass")
password_file: str | None = None
if become and ask_become_pass:
if become_pass:
fd, password_file = tempfile.mkstemp(suffix=".txt", prefix="grm-become-")
with os.fdopen(fd, "w") as f:
f.write(become_pass)
os.chmod(password_file, 0o600)
cmd.extend(["--become-password-file", password_file])
else:
cmd.append("--ask-become-pass")
env = os.environ.copy()
proc = subprocess.run( # nosec B603
cmd,
env=env,
capture_output=True,
text=True,
)
try:
proc = subprocess.run( # nosec B603
cmd,
env=env,
capture_output=True,
text=True,
)
finally:
if password_file:
with contextlib.suppress(FileNotFoundError):
os.unlink(password_file)
if check and proc.returncode != 0:
stderr = proc.stderr.strip() if proc.stderr else ""
raise AnsibleError(
+35 -3
View File
@@ -226,6 +226,29 @@ class RunnerManager:
)
tracker.done()
def restart(
self,
name: str,
host: str | None = None,
user: str | None = None,
key: str | None = None,
ask_become_pass: bool = False,
) -> None:
"""Restart a runner instance on a remote host (stop, prune images, start)."""
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
with track_steps() as tracker:
tracker.begin(_("Restarting Gitea Runner {name} on {host}", name=name, host=actual_host))
self._run_playbook(
"restart-runner.yml",
actual_host,
actual_user,
{"runner_name": name},
actual_key,
ask_become_pass,
description=_("Restarting Gitea Runner {name} on {host}", name=name, host=actual_host),
)
tracker.done()
def enable(
self,
name: str,
@@ -345,8 +368,14 @@ class RunnerManager:
self._registry.remove(name)
tracker.done()
def list_runners(self) -> list[dict[str, str]]:
"""Return a list of registered runners with live service status."""
def list_runners(self, become_pass: str | None = None) -> list[dict[str, str]]:
"""Return a list of registered runners with live service status.
Args:
become_pass: Sudo password for ad-hoc status checks. When provided,
it is passed via ``--become-password-file`` to avoid stdin
consumption issues when checking multiple runners in sequence.
"""
runners = self._registry.list()
result: list[dict[str, str]] = []
for name, info in runners.items():
@@ -369,10 +398,13 @@ class RunnerManager:
user,
key,
"shell",
f"sudo -u grm-{name} systemctl --user is-active gitea-runner 2>/dev/null",
f"sudo -u grm-{name} "
f"XDG_RUNTIME_DIR=/run/user/$(id -u grm-{name}) "
f"systemctl --user is-active gitea-runner 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
become_pass=become_pass,
)
service_status = self._parse_status(stdout)
except AnsibleError:
+47 -7
View File
@@ -159,13 +159,13 @@
"ru": "URL Gitea (env: GITEA_URL)",
"zh": "Gitea URL(环境变量: GITEA_URL"
},
"Gitea admin API token for integration test (env: REPO_TOKEN)": {
"bg": "Gitea admin API токен за интеграционен тест (env: REPO_TOKEN)",
"de": "Gitea-Admin-API-Token für Integrationstest (env: REPO_TOKEN)",
"en": "Gitea admin API token for integration test (env: REPO_TOKEN)",
"pl": "Token API administratora Gitea do testów integracyjnych (env: REPO_TOKEN)",
"ru": "Токен админ API Gitea для интеграционного теста (env: REPO_TOKEN)",
"zh": "Gitea 管理员 API 令牌,用于集成测试(环境变量: REPO_TOKEN"
"Gitea admin API token for integration test (env: CI_GITEA_TOKEN)": {
"bg": "Gitea admin API токен за интеграционен тест (env: CI_GITEA_TOKEN)",
"de": "Gitea-Admin-API-Token für Integrationstest (env: CI_GITEA_TOKEN)",
"en": "Gitea admin API token for integration test (env: CI_GITEA_TOKEN)",
"pl": "Token API administratora Gitea do testów integracyjnych (env: CI_GITEA_TOKEN)",
"ru": "Токен админ API Gitea для интеграционного теста (env: CI_GITEA_TOKEN)",
"zh": "Gitea 管理员 API 令牌,用于集成测试(环境变量: CI_GITEA_TOKEN"
},
"HOST": {
"bg": "ХОСТ",
@@ -295,6 +295,14 @@
"ru": "Запросить пароль sudo (по умолчанию)",
"zh": "提示输入 sudo 密码(默认)"
},
"Prompt for sudo password once for all status checks (default).": {
"bg": "Подканване за sudo парола веднъж за всички проверки на състоянието (по подразбиране).",
"de": "Einmal nach sudo-Passwort für alle Statusprüfungen fragen (Standard).",
"en": "Prompt for sudo password once for all status checks (default).",
"pl": "Zapytaj o hasło sudo raz dla wszystkich sprawdzeń statusu (domyślnie).",
"ru": "Запросить пароль sudo один раз для всех проверок статуса (по умолчанию).",
"zh": "为所有状态检查提示一次 sudo 密码(默认)。"
},
"Registration token (env: GITEA_REGISTRATION_TOKEN)": {
"bg": "Регистрационен токен (env: GITEA_REGISTRATION_TOKEN)",
"de": "Registrierungstoken (env: GITEA_REGISTRATION_TOKEN)",
@@ -335,6 +343,30 @@
"ru": "Удаление Gitea Runner {name} с {host}",
"zh": "正在从 {host} 移除 Gitea Runner {name}"
},
"Restart a registered Gitea Runner (stop, prune images, start).": {
"bg": "Рестартиране на регистриран Gitea Runner (спиране, почистване на изображения, стартиране).",
"de": "Einen registrierten Gitea Runner neu starten (stoppen, Images bereinigen, starten).",
"en": "Restart a registered Gitea Runner (stop, prune images, start).",
"pl": "Uruchom ponownie zarejestrowanego Gitea Runner (zatrzymaj, wyczyść obrazy, uruchom).",
"ru": "Перезапустить зарегистрированный Gitea Runner (остановить, очистить образы, запустить).",
"zh": "重启已注册的 Gitea Runner(停止、清理镜像、启动)。"
},
"Restart failed: {error}": {
"bg": "Рестартирането неуспешно: {error}",
"de": "Neustart fehlgeschlagen: {error}",
"en": "Restart failed: {error}",
"pl": "Ponowne uruchomienie nie powiodło się: {error}",
"ru": "Перезапуск не удался: {error}",
"zh": "重启失败: {error}"
},
"Restarting Gitea Runner {name} on {host}": {
"bg": "Рестартиране на Gitea Runner {name} на {host}",
"de": "Starte Gitea Runner {name} auf {host} neu",
"en": "Restarting Gitea Runner {name} on {host}",
"pl": "Ponowne uruchamianie Gitea Runner {name} na {host}",
"ru": "Перезапуск Gitea Runner {name} на {host}",
"zh": "正在 {host} 上重启 Gitea Runner {name}"
},
"Runner '{name}' not found in registry.": {
"bg": "Runner '{name}' не е намерен в регистъра.",
"de": "Runner '{name}' nicht in der Registrierung gefunden.",
@@ -463,6 +495,14 @@
"ru": "Остановка Gitea Runner {name} на {host}",
"zh": "正在 {host} 上停止 Gitea Runner {name}"
},
"Sudo password": {
"bg": "Sudo парола",
"de": "Sudo-Passwort",
"en": "Sudo password",
"pl": "Hasło sudo",
"ru": "Пароль sudo",
"zh": "Sudo 密码"
},
"USER": {
"bg": "ПОТРЕБИТЕЛ",
"de": "BENUTZER",
+49 -5
View File
@@ -7,6 +7,8 @@ from click.testing import CliRunner
from gitea_runner_manager import __version__
from gitea_runner_manager.cli import cli
_TEST_ENV = {"GITEA_URL": "https://git.example.com", "CI_GITEA_TOKEN": "", "GITEA_RUNNER_LABELS": ""}
class TestCLI:
def test_cli_version(self) -> None:
@@ -20,7 +22,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -41,7 +43,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -115,7 +117,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(
cli,
[
@@ -150,7 +152,7 @@ class TestCLI:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
runner = CliRunner(env=_TEST_ENV)
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--ask-become-pass"])
assert result.exit_code == 0
mock_manager.install.assert_called_once_with(
@@ -284,6 +286,22 @@ class TestCLI:
ask_become_pass=True,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_restart(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
runner = CliRunner()
result = runner.invoke(cli, ["restart", "r1"])
assert result.exit_code == 0
mock_manager.restart.assert_called_once_with(
name="r1",
host=None,
user=None,
key=None,
ask_become_pass=True,
)
@patch("gitea_runner_manager.cli.RunnerManager")
def test_enable(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
@@ -537,7 +555,33 @@ class TestCLI:
assert "r1" in result.output
assert "10.0.0.1" in result.output
assert "active" in result.output
mock_manager.list_runners.assert_called_once()
mock_manager.list_runners.assert_called_once_with(become_pass=None)
@patch("gitea_runner_manager.cli.click.prompt", return_value="secret")
@patch("gitea_runner_manager.cli.sys.stdin")
def test_collect_become_pass_tty(self, mock_stdin: MagicMock, mock_prompt: MagicMock) -> None:
from gitea_runner_manager.cli import _collect_become_pass
mock_stdin.isatty.return_value = True
assert _collect_become_pass(ask_become_pass=True) == "secret"
@patch("gitea_runner_manager.cli.sys.stdin")
def test_collect_become_pass_no_ask(self, mock_stdin: MagicMock) -> None:
from gitea_runner_manager.cli import _collect_become_pass
mock_stdin.isatty.return_value = True
assert _collect_become_pass(ask_become_pass=False) is None
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_with_piped_become_pass(self, mock_manager_class: MagicMock) -> None:
mock_manager = MagicMock()
mock_manager.list_runners.return_value = []
mock_manager_class.return_value = mock_manager
runner = CliRunner()
result = runner.invoke(cli, ["list"], input="secret\n")
assert result.exit_code == 0
mock_manager.list_runners.assert_called_once_with(become_pass="secret")
@patch("gitea_runner_manager.cli.RunnerManager")
def test_list_empty(self, mock_manager_class: MagicMock) -> None:
+27 -7
View File
@@ -212,17 +212,14 @@ class TestAnsibleExecutorAdHoc:
result_mock.stderr = ""
with patch("subprocess.run", return_value=result_mock) as mock_run:
with patch("sys.stdin.isatty", return_value=True):
result = executor.run_ad_hoc(
"10.0.0.1", "ubuntu", None, "shell", "cmd", become=True, ask_become_pass=True
)
result = executor.run_ad_hoc("10.0.0.1", "ubuntu", None, "shell", "cmd", become=True, ask_become_pass=True)
assert result == "ok"
cmd = mock_run.call_args.args[0]
assert "--become" in cmd
assert "--ask-become-pass" in cmd
def test_run_ad_hoc_ask_become_pass_no_tty(self, tmp_path: Path) -> None:
def test_run_ad_hoc_with_become_pass(self, tmp_path: Path) -> None:
executor = AnsibleExecutor(log_dir=tmp_path)
result_mock = MagicMock()
result_mock.stdout = "ok\n"
@@ -230,14 +227,37 @@ class TestAnsibleExecutorAdHoc:
result_mock.stderr = ""
with patch("subprocess.run", return_value=result_mock) as mock_run:
with patch("sys.stdin.isatty", return_value=False):
with patch("os.unlink"):
result = executor.run_ad_hoc(
"10.0.0.1", "ubuntu", None, "shell", "cmd", become=True, ask_become_pass=True
"10.0.0.1",
"ubuntu",
None,
"shell",
"cmd",
become=True,
ask_become_pass=True,
become_pass="secret",
)
assert result == "ok"
cmd = mock_run.call_args.args[0]
assert "--become" in cmd
assert "--become-password-file" in cmd
assert "--ask-become-pass" not in cmd
def test_run_ad_hoc_ask_become_pass_no_become(self, tmp_path: Path) -> None:
executor = AnsibleExecutor(log_dir=tmp_path)
result_mock = MagicMock()
result_mock.stdout = "ok\n"
result_mock.returncode = 0
result_mock.stderr = ""
with patch("subprocess.run", return_value=result_mock) as mock_run:
result = executor.run_ad_hoc("10.0.0.1", "ubuntu", None, "shell", "cmd", become=False, ask_become_pass=True)
assert result == "ok"
cmd = mock_run.call_args.args[0]
assert "--become" not in cmd
assert "--ask-become-pass" not in cmd
def test_run_ad_hoc_check_false(self, tmp_path: Path) -> None:
+18 -1
View File
@@ -297,6 +297,20 @@ class TestRunnerManager:
assert manager._captured_extra_vars["runner_name"] == "r1"
assert "Stopping Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
def test_restart(self) -> None:
mock_registry = MagicMock()
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
manager = RunnerManager(registry=mock_registry)
mock_executor = MagicMock()
manager._executor = mock_executor
manager.restart("r1")
cmd = mock_executor.run.call_args.args[0]
cmd_str = " ".join(cmd)
assert "restart-runner.yml" in cmd_str
assert manager._captured_extra_vars["runner_name"] == "r1"
assert "Restarting Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
def test_enable(self) -> None:
mock_registry = MagicMock()
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
@@ -471,10 +485,13 @@ class TestRunnerManager:
"ubuntu",
"/key",
"shell",
"sudo -u grm-r1 systemctl --user is-active gitea-runner 2>/dev/null",
"sudo -u grm-r1 "
"XDG_RUNTIME_DIR=/run/user/$(id -u grm-r1) "
"systemctl --user is-active gitea-runner 2>/dev/null",
become=True,
ask_become_pass=True,
check=False,
become_pass=None,
)
def test_list_runners_exception(self) -> None: