refactor: rootless Docker, fix auto-merge, molecule platform matrix
Three major improvements: 1. Rootless Docker refactor: Removes docker/binary modes, unifies to rootless Docker with per-runner system users. Each runner gets its own rootless Docker daemon, systemd user service, and isolated environment. Simplifies CLI (removes --mode option), Ansible role (single code path), and molecule scenarios (removes binary scenario). 2. Auto-merge fix: Fixes status check context mismatch in branch protection (was requiring "lint", "unit-tests", "molecule-tests" but actual contexts are "CI / quality", "CI / molecule-tests*"). Adds retry/wait logic to auto_merge.py that polls commit statuses for up to 15 minutes before attempting merge, eliminating the chicken-and-egg problem where auto-merge would fail because CI hadn't completed yet. 3. Molecule platform matrix: Adds OS platform matrix to CI — all 6 scenarios now run on all 4 supported OSes (ubuntu-2204, ubuntu-2404, debian-12, archlinux) = 24 test pairs distributed across 3 parallel runners. Updates distribute_molecule.py to distribute (scenario, platform) pairs. Updates Makefile with molecule-all target for local multi-platform testing. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
0c6c735000
commit
55c2746569
@@ -5,3 +5,6 @@ exclude_paths:
|
|||||||
- molecule/
|
- molecule/
|
||||||
- .molecule/
|
- .molecule/
|
||||||
- .pytest_cache/
|
- .pytest_cache/
|
||||||
|
skip_list:
|
||||||
|
# Rootless Docker uses `systemctl --user` which the systemd module doesn't support
|
||||||
|
- command-instead-of-module
|
||||||
|
|||||||
+17
-7
@@ -36,22 +36,32 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
run: make setup
|
run: make setup
|
||||||
- name: Discover assigned scenarios
|
- name: Discover assigned test pairs
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
SCENARIOS=$(python3 scripts/distribute_molecule.py --runner-index ${{ matrix.runner-index }} --max-runners 3)
|
PAIRS=$(python3 scripts/distribute_molecule.py --runner-index ${{ matrix.runner-index }} --max-runners 3)
|
||||||
echo "Assigned scenarios: $SCENARIOS"
|
echo "Assigned pairs: $PAIRS"
|
||||||
echo "SCENARIOS=$SCENARIOS" >> $GITHUB_ENV
|
echo "TEST_PAIRS=$PAIRS" >> $GITHUB_ENV
|
||||||
- name: Run molecule tests
|
- name: Run molecule tests
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
|
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
|
||||||
export ANSIBLE_INJECT_INVOCATION=1
|
export ANSIBLE_INJECT_INVOCATION=1
|
||||||
cd ansible/roles/gitea-runner
|
cd ansible/roles/gitea-runner
|
||||||
for s in $SCENARIOS; do
|
for pair in $TEST_PAIRS; do
|
||||||
if [ "$s" = "default" ]; then
|
IFS='|' read -r scenario platform_name platform_image platform_command <<< "$pair"
|
||||||
|
export MOLECULE_PLATFORM_NAME="$platform_name"
|
||||||
|
export MOLECULE_PLATFORM_IMAGE="$platform_image"
|
||||||
|
if [ -n "$platform_command" ]; then
|
||||||
|
export MOLECULE_PLATFORM_COMMAND="$platform_command"
|
||||||
|
else
|
||||||
|
unset MOLECULE_PLATFORM_COMMAND
|
||||||
|
fi
|
||||||
|
echo "::group::Molecule: $scenario on $platform_name"
|
||||||
|
if [ "$scenario" = "default" ]; then
|
||||||
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test
|
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test
|
||||||
else
|
else
|
||||||
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test -s "$s"
|
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true molecule test -s "$scenario"
|
||||||
fi
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.11.11
|
3.12
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ make test-all # Runs pytest-cov + molecule
|
|||||||
make lint-all # Runs ruff, pyright, bandit, ansible-lint, checkmake
|
make lint-all # Runs ruff, pyright, bandit, ansible-lint, checkmake
|
||||||
make lint-bandit # Security scan with bandit
|
make lint-bandit # Security scan with bandit
|
||||||
make pytest-cov # Unit tests with 100% coverage enforcement
|
make pytest-cov # Unit tests with 100% coverage enforcement
|
||||||
make molecule # All 7 molecule scenarios
|
make molecule # All 6 molecule scenarios
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Quality
|
## Code Quality
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: all setup install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-docker molecule-binary molecule-all test-all clean
|
.PHONY: all setup install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-all test-all clean
|
||||||
|
|
||||||
PYTHON := python3
|
PYTHON := python3
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
@@ -17,7 +17,7 @@ setup: $(VENV)/bin/activate .env activate-scripts checkmake
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
$(VENV)/bin/activate:
|
$(VENV)/bin/activate:
|
||||||
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 11), f'Python 3.11+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
|
@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)
|
$(PYTHON) -m venv $(VENV)
|
||||||
$(BIN)/pip install --upgrade pip setuptools wheel
|
$(BIN)/pip install --upgrade pip setuptools wheel
|
||||||
|
|
||||||
@@ -36,35 +36,35 @@ checkmake:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
|
@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 $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm install $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
update:
|
update:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make update HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make update HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
start:
|
start:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make start HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make start HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm start $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm start $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make stop HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make stop HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm stop $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm stop $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
enable:
|
enable:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make enable HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make enable HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm enable $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm enable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
disable:
|
disable:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make disable HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make disable HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm disable $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm disable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
status:
|
status:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make status HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make status HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm status $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm status $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
remove:
|
remove:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make remove HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make remove HOST=192.168.1.10"; exit 1; fi
|
||||||
$(BIN)/grm remove $(HOST) $(if $(USER),--user $(USER),) $(if $(NAME),--name $(NAME),) $(if $(TOKEN),--token $(TOKEN),) $(if $(MODE),--mode $(MODE),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(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:
|
lint-ruff:
|
||||||
$(BIN)/ruff check src/ tests/
|
$(BIN)/ruff check src/ tests/
|
||||||
@@ -98,22 +98,15 @@ pytest-cov:
|
|||||||
$(BIN)/pytest tests/unit/ -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100
|
$(BIN)/pytest tests/unit/ -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100
|
||||||
|
|
||||||
MOLECULE := $(realpath $(BIN))/molecule
|
MOLECULE := $(realpath $(BIN))/molecule
|
||||||
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true $(MOLECULE)
|
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
|
||||||
|
|
||||||
# Quick local test: Ubuntu 22.04 only
|
# Quick local test: Ubuntu 22.04 only, all scenarios
|
||||||
molecule:
|
molecule:
|
||||||
@set -e; for s in default binary multi-instance lifecycle template-content deregister update; do if [ "$$s" = "default" ]; then $(MOLECULE_BASE) test; else $(MOLECULE_BASE) test -s $$s; fi; done
|
@set -e; for s in default multi-instance lifecycle template-content deregister update; do if [ "$$s" = "default" ]; then $(MOLECULE_BASE) test; else $(MOLECULE_BASE) test -s $$s; fi; done
|
||||||
|
|
||||||
# Docker mode tests for all platforms (sequential; override env vars for CI matrix parallelisation)
|
# All scenarios on all supported platforms (sequential; use CI matrix for parallel execution)
|
||||||
molecule-docker:
|
molecule-all:
|
||||||
@set -e; for p in ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest:'' ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest:'' ubuntu-2604:geerlingguy/docker-ubuntu2604-ansible:latest:'' debian-12:geerlingguy/docker-debian12-ansible:latest:'' debian-13:geerlingguy/docker-debian13-ansible:latest:'' archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd; do IFS=":"; set -- $$p; MOLECULE_PLATFORM_NAME=$$1 MOLECULE_PLATFORM_IMAGE=$$2 MOLECULE_PLATFORM_COMMAND=$$3 $(MOLECULE_BASE) test; done
|
@bash scripts/molecule_all.sh
|
||||||
|
|
||||||
# Binary mode tests for all platforms (sequential; override env vars for CI matrix parallelisation)
|
|
||||||
molecule-binary:
|
|
||||||
@set -e; for p in ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest:'' ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest:'' ubuntu-2604:geerlingguy/docker-ubuntu2604-ansible:latest:'' debian-12:geerlingguy/docker-debian12-ansible:latest:'' debian-13:geerlingguy/docker-debian13-ansible:latest:'' archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd; do IFS=":"; set -- $$p; MOLECULE_PLATFORM_NAME=$$1 MOLECULE_PLATFORM_IMAGE=$$2 MOLECULE_PLATFORM_COMMAND=$$3 $(MOLECULE_BASE) test -s binary; done
|
|
||||||
|
|
||||||
# All platform tests (sequential; use CI matrix for parallel execution)
|
|
||||||
molecule-all: molecule-docker molecule-binary
|
|
||||||
|
|
||||||
test: test-all
|
test: test-all
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts.
|
A lean command-line tool to automate the installation, configuration, and lifecycle management of Gitea Actions runners on Arch Linux, Ubuntu, and Debian hosts.
|
||||||
|
|
||||||
By default, runners are deployed as **Docker containers** using the official `gitea/runner` image. A traditional **binary** deployment mode is also available.
|
Each runner runs in an isolated **rootless Docker** environment under a dedicated system user, enabling multiple runners to operate in parallel on the same host without conflicts. The runner binary (`gitea_runner`) is installed directly and managed as a systemd user service.
|
||||||
|
|
||||||
> **Pronunciation note:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM" in Latin letters) — the Bulgarian word for **thunder**. Wherever there are clouds, there may be thunders. This is an open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
> **Pronunciation note:** GRM is short for *Gitea Runner Manager*, but say it like **ГРЪМ** (roughly "GRUM" in Latin letters) — the Bulgarian word for **thunder**. Wherever there are clouds, there may be thunders. This is an open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||||
|
|
||||||
@@ -23,19 +23,22 @@ This project uses **conventional commits** and **GRM-N branch prefixes**. See [C
|
|||||||
- **Flexible** — accepts a plain IP address or hostname, and allows specifying the SSH user and private key.
|
- **Flexible** — accepts a plain IP address or hostname, and allows specifying the SSH user and private key.
|
||||||
- **Runner registry** — stores runner connection metadata locally after installation. Subsequent commands need only the runner name.
|
- **Runner registry** — stores runner connection metadata locally after installation. Subsequent commands need only the runner name.
|
||||||
- **Lifecycle management** — start, stop, enable, disable, status, and remove runners via CLI.
|
- **Lifecycle management** — start, stop, enable, disable, status, and remove runners via CLI.
|
||||||
- **Multi-instance** — run multiple isolated runners on the same host, each with its own data directory and systemd service.
|
- **Multi-instance** — run multiple isolated runners on the same host, each with its own system user, rootless Docker daemon, data directory, and systemd user service.
|
||||||
- **Systemd-managed** — both Docker and binary modes run under systemd template units (`gitea-runner@<name>.service`).
|
- **Rootless Docker** — each runner gets its own rootless Docker daemon, avoiding conflicts with the host's Docker installation and enabling true parallel execution.
|
||||||
|
- **Systemd-managed** — runners run as systemd user services (`gitea-runner.service`) under dedicated per-runner system users.
|
||||||
|
|
||||||
## Supported Operating Systems
|
## Supported Operating Systems
|
||||||
|
|
||||||
- Arch Linux
|
- Arch Linux
|
||||||
- Ubuntu 22.04 / 24.04 / 26.04
|
- Ubuntu 22.04 / 24.04
|
||||||
- Debian 12 / 13
|
- Debian 12
|
||||||
|
|
||||||
|
All supported OSes are tested in CI via molecule scenarios on every PR.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **SSH key authentication** — The remote host must be reachable via SSH using the user specified with `--user` and the private key specified with `--key`. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail.
|
- **SSH key authentication** — The remote host must be reachable via SSH using the user specified with `--user` and the private key specified with `--key`. GRM uses Ansible under the hood, which connects to the target host over SSH to execute all installation and configuration tasks. Without valid SSH credentials, Ansible cannot establish a connection and the deployment will fail.
|
||||||
- **Sudo access** — GRM requires root privileges on the remote host to install packages, create systemd services, and manage Docker. By default, you will be prompted interactively for the sudo password. For automation or uninterrupted workflows, configure passwordless sudo on the remote host and pass `--no-ask-become-pass`.
|
- **Sudo access** — GRM requires root privileges on the remote host to create system users, install packages, and configure rootless Docker. By default, you will be prompted interactively for the sudo password. For automation or uninterrupted workflows, configure passwordless sudo on the remote host and pass `--no-ask-become-pass`.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -44,8 +47,8 @@ This project uses **conventional commits** and **GRM-N branch prefixes**. See [C
|
|||||||
```bash
|
```bash
|
||||||
git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git
|
git clone https://git.oblachno.oblachno.com/oblachno/gitea-runner-manager.git
|
||||||
cd gitea-runner-manager
|
cd gitea-runner-manager
|
||||||
pyenv install 3.11.11
|
pyenv install 3.12
|
||||||
pyenv local 3.11.11
|
pyenv local 3.12
|
||||||
make setup
|
make setup
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ cp .env.example .env
|
|||||||
Set `GITEA_ADMIN_TOKEN` to enable informational API checks during integration test. This is **optional** — the test primarily verifies the runner by checking:
|
Set `GITEA_ADMIN_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)
|
1. **`.runner` registration file** exists and contains valid JSON (proves successful registration)
|
||||||
2. **Container/service** is running (proves daemon is polling for jobs)
|
2. **Systemd user service** is active (proves daemon is polling for jobs)
|
||||||
|
|
||||||
API checks, if enabled, are purely informational and do not affect pass/fail.
|
API checks, if enabled, are purely informational and do not affect pass/fail.
|
||||||
|
|
||||||
@@ -74,11 +77,7 @@ API checks, if enabled, are purely informational and do not affect pass/fail.
|
|||||||
Using the CLI (you will be prompted for the sudo password by default):
|
Using the CLI (you will be prompted for the sudo password by default):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Docker mode (default) — deploys gitea_runner as a container
|
|
||||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner
|
||||||
|
|
||||||
# Binary mode — downloads and installs the gitea_runner binary with systemd
|
|
||||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --mode binary
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Automation tip:** Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to skip the password prompt. This is recommended for CI/CD pipelines.
|
> **Automation tip:** Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to skip the password prompt. This is recommended for CI/CD pipelines.
|
||||||
@@ -86,16 +85,12 @@ grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runne
|
|||||||
Using Make:
|
Using Make:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Docker mode (default)
|
|
||||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner
|
||||||
|
|
||||||
# Binary mode
|
|
||||||
make install HOST=192.168.1.10 USER=ubuntu KEY=~/.ssh/id_ed25519 NAME=prod-runner MODE=binary
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Runner Registry
|
### Runner Registry
|
||||||
|
|
||||||
After installation, GRM stores each runner's connection details (host, user, SSH key, mode, Gitea URL) in a local JSON registry at `~/.local/share/grm/runners.json`. This means you rarely need to repeat connection arguments:
|
After installation, GRM stores each runner's connection details (host, user, SSH key, Gitea URL) in a local JSON registry at `~/.local/share/grm/runners.json`. This means you rarely need to repeat connection arguments:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all registered runners with live systemd status
|
# List all registered runners with live systemd status
|
||||||
@@ -129,19 +124,19 @@ grm remove prod-runner --token <token>
|
|||||||
You can override any stored value by passing the corresponding flag:
|
You can override any stored value by passing the corresponding flag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grm start prod-runner --host 192.168.1.11 --user root --mode binary
|
grm start prod-runner --host 192.168.1.11 --user root
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Automation tip:** If the remote host has passwordless sudo configured, pass `--no-ask-become-pass`.
|
> **Automation tip:** If the remote host has passwordless sudo configured, pass `--no-ask-become-pass`.
|
||||||
|
|
||||||
### Multiple Instances on the Same Host
|
### Multiple Instances on the Same Host
|
||||||
|
|
||||||
Each runner instance is fully isolated with its own data directory and systemd service:
|
Each runner instance is fully isolated with its own system user, rootless Docker daemon, data directory, and systemd user service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install two runners on the same host
|
# Install two runners on the same host
|
||||||
grm install 192.168.1.10 --user ubuntu --name workflow-runner
|
grm install 192.168.1.10 --user ubuntu --name workflow-runner
|
||||||
grm install 192.168.1.10 --user ubuntu --name build-runner --mode binary
|
grm install 192.168.1.10 --user ubuntu --name build-runner
|
||||||
|
|
||||||
# Manage them independently by name
|
# Manage them independently by name
|
||||||
grm stop workflow-runner
|
grm stop workflow-runner
|
||||||
@@ -153,7 +148,7 @@ grm status build-runner
|
|||||||
The installer performs an automated integration test that verifies:
|
The installer performs an automated integration test that verifies:
|
||||||
|
|
||||||
1. **`.runner` file exists** with valid JSON containing `id`, `uuid`, `token`, `address` — this proves successful registration with Gitea
|
1. **`.runner` file exists** with valid JSON containing `id`, `uuid`, `token`, `address` — this proves successful registration with Gitea
|
||||||
2. **Container/service is running** — this proves the daemon is active and polling for jobs
|
2. **Systemd user service is active** — this proves the daemon is polling for jobs
|
||||||
|
|
||||||
You can also check the Gitea UI under **Actions → Runners** to confirm the runner appears as **Online**.
|
You can also check the Gitea UI under **Actions → Runners** to confirm the runner appears as **Online**.
|
||||||
|
|
||||||
@@ -174,11 +169,8 @@ GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
|
|||||||
**Runner logs** (on the remote host):
|
**Runner logs** (on the remote host):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Binary mode logs (via systemd template unit)
|
# Runner logs (via systemd user service)
|
||||||
sudo journalctl -u gitea-runner@<name> -f
|
sudo -u grm-<name> journalctl --user -u gitea-runner -f
|
||||||
|
|
||||||
# Docker mode logs
|
|
||||||
docker logs gitea-runner-<name> -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The GRM application writes to two destinations:
|
The GRM application writes to two destinations:
|
||||||
@@ -197,27 +189,28 @@ Console output is automatically colorised via ``click.echo``: operation headers
|
|||||||
GRM consists of two layers:
|
GRM consists of two layers:
|
||||||
|
|
||||||
1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
|
1. **Python CLI** (`src/gitea_runner_manager/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
|
||||||
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that installs Docker, configures the runner (binary or container), creates systemd template units, and registers the runner with Gitea.
|
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, and registers the runner with Gitea.
|
||||||
|
|
||||||
```
|
```
|
||||||
grm install <host>
|
grm install <host>
|
||||||
└── RunnerManager.install()
|
└── RunnerManager.install()
|
||||||
└── ansible-playbook ansible/install-runner.yml
|
└── ansible-playbook ansible/install-runner.yml
|
||||||
└── role: gitea-runner
|
└── role: gitea-runner
|
||||||
├── docker.yml (Docker installation)
|
├── user_setup.yml (create per-runner system user + lingering)
|
||||||
├── docker_mode.yml (container deployment)
|
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||||
├── binary_mode.yml (binary + systemd deployment)
|
├── install_runner.yml (download binary, config, register, service)
|
||||||
├── service.yml (systemd template unit)
|
|
||||||
├── deregister.yml (deregistration from Gitea)
|
|
||||||
├── prune.yml (Docker prune timer)
|
├── prune.yml (Docker prune timer)
|
||||||
└── integration_test.yml (validate online status)
|
└── integration_test.yml (validate service is active)
|
||||||
```
|
```
|
||||||
|
|
||||||
Both Docker and binary modes run under a single systemd template unit (`gitea-runner@.service`), instantiated per runner name (e.g., `gitea-runner@prod-runner.service`). Each instance has fully isolated directories:
|
Each runner runs as a systemd user service under a dedicated system user (`grm-<name>`). Each instance has fully isolated resources:
|
||||||
|
|
||||||
|
- **User**: `grm-<name>` (dedicated system user with lingering enabled)
|
||||||
|
- **Home**: `/home/grm-<name>/`
|
||||||
- **Data**: `/var/lib/gitea-runner/<name>/`
|
- **Data**: `/var/lib/gitea-runner/<name>/`
|
||||||
- **Config**: `/etc/gitea-runner/<name>/` (binary mode)
|
- **Config**: `/etc/gitea-runner/<name>/`
|
||||||
- **Service**: `gitea-runner@<name>.service`
|
- **Service**: `gitea-runner.service` (systemd user service)
|
||||||
|
- **Docker socket**: `/run/user/<UID>/docker.sock` (rootless, per-runner)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -225,11 +218,13 @@ All tunable values are exposed as Ansible variables in `ansible/roles/gitea-runn
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `runner_mode` | `docker` | Deployment mode: `docker` or `binary` |
|
| `gitea_runner_version` | `1.0.8` | Runner binary version |
|
||||||
| `gitea_runner_version` | `1.0.8` | Docker image tag / binary version |
|
|
||||||
| `gitea_runner_docker_image` | `gitea/runner` | Docker image name |
|
|
||||||
| `runner_labels` | `ubuntu-latest:docker://runner-images:ubuntu-22.04` | Runner labels |
|
| `runner_labels` | `ubuntu-latest:docker://runner-images:ubuntu-22.04` | Runner labels |
|
||||||
| `skip_runner_registration` | `false` | Skip API registration (useful for tests) |
|
| `skip_runner_registration` | `false` | Skip API registration (useful for tests) |
|
||||||
|
| `gitea_runner_user_prefix` | `grm-` | Prefix for per-runner system users |
|
||||||
|
| `gitea_runner_base_home` | `/home` | Base directory for runner user homes |
|
||||||
|
| `gitea_runner_service_user` | `{{ prefix }}{{ runner_name }}` | Per-runner system user |
|
||||||
|
| `gitea_runner_home` | `{{ base_home }}/{{ service_user }}` | Runner user home directory |
|
||||||
| `gitea_runner_base_data_dir` | `/var/lib/gitea-runner` | Base data directory (instance-scoped) |
|
| `gitea_runner_base_data_dir` | `/var/lib/gitea-runner` | Base data directory (instance-scoped) |
|
||||||
| `gitea_runner_base_config_dir` | `/etc/gitea-runner` | Base config directory (instance-scoped) |
|
| `gitea_runner_base_config_dir` | `/etc/gitea-runner` | Base config directory (instance-scoped) |
|
||||||
| `gitea_runner_data_dir` | `{{ base }}/{{ runner_name }}` | Runtime data directory per instance |
|
| `gitea_runner_data_dir` | `{{ base }}/{{ runner_name }}` | Runtime data directory per instance |
|
||||||
@@ -239,7 +234,6 @@ All tunable values are exposed as Ansible variables in `ansible/roles/gitea-runn
|
|||||||
| `gitea_runner_prune_schedule` | `daily` | systemd timer schedule |
|
| `gitea_runner_prune_schedule` | `daily` | systemd timer schedule |
|
||||||
| `gitea_runner_prune_label` | `gitea-runner=true` | Docker label for pruning |
|
| `gitea_runner_prune_label` | `gitea-runner=true` | Docker label for pruning |
|
||||||
| `gitea_runner_service_restart_sec` | `5` | systemd RestartSec value |
|
| `gitea_runner_service_restart_sec` | `5` | systemd RestartSec value |
|
||||||
| `gitea_runner_service_user` | `{{ ansible_user \| default('root') }}` | Service user |
|
|
||||||
| `gitea_runner_log_level` | `info` | Runner log level |
|
| `gitea_runner_log_level` | `info` | Runner log level |
|
||||||
| `gitea_runner_container_label` | `gitea-runner=true` | Container label |
|
| `gitea_runner_container_label` | `gitea-runner=true` | Container label |
|
||||||
| `docker_gpg_key_path` | `/etc/apt/keyrings/docker.asc` | Docker GPG key path |
|
| `docker_gpg_key_path` | `/etc/apt/keyrings/docker.asc` | Docker GPG key path |
|
||||||
@@ -311,18 +305,23 @@ Runs pytest with 100% coverage requirement.
|
|||||||
### Molecule Tests
|
### Molecule Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make molecule
|
make molecule # Quick: all 6 scenarios on Ubuntu 22.04
|
||||||
|
make molecule-all # Full: all 6 scenarios on all 4 supported OSes
|
||||||
```
|
```
|
||||||
|
|
||||||
Runs four scenarios:
|
Runs six scenarios:
|
||||||
|
|
||||||
- **default** — Docker mode installation in an Ubuntu 22.04 container
|
- **default** — Rootless Docker runner installation
|
||||||
- **binary** — Binary mode installation in an Ubuntu 22.04 container
|
|
||||||
- **multi-instance** — Two isolated runner instances on the same host
|
- **multi-instance** — Two isolated runner instances on the same host
|
||||||
- **lifecycle** — Stop, disable, re-enable, and start sequence
|
- **lifecycle** — Stop, disable, re-enable, and start sequence
|
||||||
|
- **template-content** — Verify rendered systemd user service and prune templates
|
||||||
|
- **deregister** — Runner deregistration
|
||||||
|
- **update** — Runner binary update
|
||||||
|
|
||||||
All scenarios test idempotence (second run produces zero changes).
|
All scenarios test idempotence (second run produces zero changes).
|
||||||
|
|
||||||
|
CI runs all 6 scenarios × 4 platforms (24 test pairs) distributed across 3 parallel runners.
|
||||||
|
|
||||||
### Integration Tests
|
### Integration Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -346,7 +345,7 @@ This is a harmless cleanup traceback from Molecule's Docker driver when the test
|
|||||||
### Runner appears offline after installation
|
### Runner appears offline after installation
|
||||||
|
|
||||||
- Check that the `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` environment variables are correct.
|
- Check that the `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` environment variables are correct.
|
||||||
- Verify the runner container or service is running: `docker ps` or `systemctl status gitea-runner@<name>`.
|
- Verify the runner service is running: `sudo -u grm-<name> systemctl --user status gitea-runner`.
|
||||||
- Check logs for registration errors.
|
- Check logs for registration errors.
|
||||||
|
|
||||||
### Integration test fails
|
### Integration test fails
|
||||||
@@ -358,22 +357,17 @@ The test checks two things:
|
|||||||
- Runner logs for registration errors
|
- Runner logs for registration errors
|
||||||
- The `.runner` file should exist at `/var/lib/gitea-runner/<name>/.runner`
|
- The `.runner` file should exist at `/var/lib/gitea-runner/<name>/.runner`
|
||||||
|
|
||||||
2. **Container/service not running** — Daemon failed to start. Check:
|
2. **Service not running** — Daemon failed to start. Check:
|
||||||
- `docker ps` or `systemctl status gitea-runner@<name>`
|
- `sudo -u grm-<name> systemctl --user status gitea-runner`
|
||||||
- Logs for connection errors
|
- Logs for connection errors
|
||||||
|
|
||||||
### Docker mode: container won't start
|
### Rootless Docker: service fails to start
|
||||||
|
|
||||||
- Ensure Docker is installed and running on the host.
|
- Check the service status: `sudo -u grm-<name> systemctl --user status gitea-runner`.
|
||||||
- Verify the Docker socket is accessible: `docker version`.
|
- Verify the rootless Docker daemon is running: `sudo -u grm-<name> systemctl --user status docker`.
|
||||||
- Check systemd status: `systemctl status gitea-runner@<name>`.
|
- Verify the Docker socket exists: `ls /run/user/$(id -u grm-<name>)/docker.sock`.
|
||||||
|
- Check logs: `sudo -u grm-<name> journalctl --user -u gitea-runner -f`.
|
||||||
### Binary mode: systemd service fails
|
- Ensure lingering is enabled for the runner user: `loginctl show-user grm-<name> | grep Linger`.
|
||||||
|
|
||||||
- Check the service status: `systemctl status gitea-runner@<name>`.
|
|
||||||
- Verify the binary exists at `gitea_runner_binary_path`.
|
|
||||||
- Ensure the service user is in the `docker` group.
|
|
||||||
- Check logs: `journalctl -u gitea-runner@<name> -f`.
|
|
||||||
|
|
||||||
## Makefile Targets
|
## Makefile Targets
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -9,12 +9,14 @@
|
|||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner systemd instance
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
state: stopped
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
daemon_reload: true
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
- name: Include deregistration
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
@@ -22,9 +24,11 @@
|
|||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not skip_runner_registration | default(false)
|
||||||
|
|
||||||
- name: Disable gitea-runner systemd instance
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
enabled: false
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
daemon_reload: true
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|||||||
@@ -9,9 +9,20 @@
|
|||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Enable gitea-runner systemd instance
|
- name: Enable gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user enable gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
enabled: true
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
daemon_reload: true
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Start gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
# ansible_ssh_private_key_file — Path to SSH private key
|
# ansible_ssh_private_key_file — Path to SSH private key
|
||||||
#
|
#
|
||||||
# Optional variables per host:
|
# Optional variables per host:
|
||||||
# runner_mode=docker — Deployment mode: docker or binary
|
# gitea_runner_version=1.0.8 — Runner binary version
|
||||||
# gitea_runner_version=1.0.8 — Runner image/binary version
|
|
||||||
|
|
||||||
[runners]
|
[runners]
|
||||||
192.168.1.10 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
192.168.1.10 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||||
runner.example.com ansible_user=arch ansible_ssh_private_key_file=~/.ssh/id_ed25519 runner_mode=binary
|
runner.example.com ansible_user=arch ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||||
|
|||||||
+52
-20
@@ -9,31 +9,45 @@
|
|||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner systemd instance
|
- name: Get runner user UID
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}"
|
register: runner_uid_result
|
||||||
state: stopped
|
changed_when: false
|
||||||
daemon_reload: true
|
failed_when: false
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Disable gitea-runner systemd instance
|
- name: Set runner UID fact
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.set_fact:
|
||||||
name: "gitea-runner-{{ runner_mode | default('docker') }}@{{ runner_name }}"
|
gitea_runner_uid: "{{ runner_uid_result.stdout }}"
|
||||||
enabled: false
|
when: runner_uid_result.rc == 0
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Stop Docker container directly
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.command: "docker stop -t 30 gitea-runner-{{ runner_name }}"
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
|
when: systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
when: runner_mode | default('docker') == 'docker'
|
|
||||||
|
|
||||||
- name: Remove Docker container directly
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.command: "docker rm -f gitea-runner-{{ runner_name }}"
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Stop rootless Docker daemon
|
||||||
|
ansible.builtin.command: systemctl --user stop docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
when: runner_mode | default('docker') == 'docker'
|
|
||||||
|
|
||||||
- name: Include deregistration
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
@@ -41,12 +55,30 @@
|
|||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not skip_runner_registration | default(false)
|
||||||
|
|
||||||
- name: Remove systemd unit file (last instance)
|
- name: Remove systemd user unit file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service"
|
||||||
state: absent
|
state: absent
|
||||||
when: remove_systemd_template | default(false)
|
when: remove_systemd_template | default(false)
|
||||||
|
|
||||||
|
- name: Kill remaining processes of runner user
|
||||||
|
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
failed_when: false
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Wait for processes to terminate
|
||||||
|
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Remove runner user and home directory
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
state: absent
|
||||||
|
remove: true
|
||||||
|
when: remove_runner_user | default(true)
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove runner data directory
|
- name: Remove runner data directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}"
|
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}"
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
runner_mode: "docker"
|
|
||||||
gitea_runner_version: "1.0.8"
|
gitea_runner_version: "1.0.8"
|
||||||
gitea_runner_docker_image: "gitea/runner"
|
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-22.04"
|
||||||
runner_labels: "ubuntu-latest:docker://runner-images:ubuntu-22.04"
|
|
||||||
skip_runner_registration: false
|
skip_runner_registration: false
|
||||||
|
|
||||||
|
# Per-runner user (rootless isolation)
|
||||||
|
gitea_runner_user_prefix: "grm-"
|
||||||
|
gitea_runner_base_home: "/home"
|
||||||
|
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ runner_name }}"
|
||||||
|
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
|
||||||
|
|
||||||
# Base paths (instance-scoped via runner_name)
|
# Base paths (instance-scoped via runner_name)
|
||||||
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
||||||
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
||||||
@@ -19,12 +23,11 @@ gitea_runner_prune_label: "gitea-runner=true"
|
|||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
gitea_runner_service_restart_sec: "5"
|
gitea_runner_service_restart_sec: "5"
|
||||||
gitea_runner_service_user: "{{ ansible_user | default('root') }}"
|
|
||||||
|
|
||||||
# Runner configuration
|
# Runner configuration
|
||||||
gitea_runner_log_level: "info"
|
gitea_runner_log_level: "info"
|
||||||
gitea_runner_container_label: "gitea-runner=true"
|
gitea_runner_container_label: "gitea-runner=true"
|
||||||
gitea_runner_file: ".runner"
|
gitea_runner_file: ".runner"
|
||||||
|
|
||||||
# Docker installation
|
# Docker installation (for rootless dependencies)
|
||||||
docker_gpg_key_path: "/etc/apt/keyrings/docker.asc"
|
docker_gpg_key_path: "/etc/apt/keyrings/docker.asc"
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd'
|
when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd'
|
||||||
|
|
||||||
- name: Restart gitea-runner
|
- name: Restart gitea-runner
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user restart gitea-runner
|
||||||
name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}"
|
become: true
|
||||||
state: restarted
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd'
|
when: ansible_facts is defined and ansible_facts['service_mgr'] | default('') == 'systemd'
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "molecule-test-runner"
|
|
||||||
runner_mode: "binary"
|
|
||||||
skip_runner_registration: true
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
driver:
|
|
||||||
name: docker
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
|
||||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
|
||||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
|
||||||
volumes:
|
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
|
||||||
cgroupns_mode: host
|
|
||||||
privileged: true
|
|
||||||
pre_build_image: false
|
|
||||||
|
|
||||||
provisioner:
|
|
||||||
name: ansible
|
|
||||||
playbooks:
|
|
||||||
converge: converge.yml
|
|
||||||
prepare: ../common/prepare.yml
|
|
||||||
env:
|
|
||||||
ANSIBLE_ROLES_PATH: "../../.."
|
|
||||||
|
|
||||||
scenario:
|
|
||||||
test_sequence:
|
|
||||||
- dependency
|
|
||||||
- cleanup
|
|
||||||
- destroy
|
|
||||||
- syntax
|
|
||||||
- create
|
|
||||||
- prepare
|
|
||||||
- converge
|
|
||||||
- idempotence
|
|
||||||
- side_effect
|
|
||||||
- verify
|
|
||||||
- cleanup
|
|
||||||
- destroy
|
|
||||||
|
|
||||||
verifier:
|
|
||||||
name: ansible
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Verify
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
runner_name: "molecule-test-runner"
|
|
||||||
pre_tasks:
|
|
||||||
- name: Load role defaults
|
|
||||||
ansible.builtin.include_vars:
|
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
|
||||||
- name: Override runner mode for binary scenario
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_mode: "binary"
|
|
||||||
tasks:
|
|
||||||
- name: Check gitea_runner binary exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_binary_path }}"
|
|
||||||
register: gitea_runner_stat
|
|
||||||
|
|
||||||
- name: Assert gitea_runner binary exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- gitea_runner_stat.stat.exists
|
|
||||||
fail_msg: "gitea_runner binary is missing at {{ gitea_runner_binary_path }}"
|
|
||||||
|
|
||||||
- name: Check Docker is installed
|
|
||||||
ansible.builtin.command: docker --version
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Check systemd template unit exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
|
||||||
register: service_stat
|
|
||||||
|
|
||||||
- name: Assert template unit exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- service_stat.stat.exists
|
|
||||||
fail_msg: "Systemd template unit is missing"
|
|
||||||
|
|
||||||
- name: Check instance data directory exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}"
|
|
||||||
register: data_dir_stat
|
|
||||||
|
|
||||||
- name: Assert instance data directory exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- data_dir_stat.stat.exists
|
|
||||||
fail_msg: "Instance data directory is missing"
|
|
||||||
|
|
||||||
- name: Check prune timer exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /etc/systemd/system/docker-prune.timer
|
|
||||||
register: timer_stat
|
|
||||||
|
|
||||||
- name: Assert prune timer exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- timer_stat.stat.exists
|
|
||||||
fail_msg: "Docker prune timer is missing"
|
|
||||||
|
|
||||||
- name: Check config file exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_config_dir }}/config.yaml"
|
|
||||||
register: config_stat
|
|
||||||
|
|
||||||
- name: Assert config file exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- config_stat.stat.exists
|
|
||||||
fail_msg: "Config file is missing"
|
|
||||||
@@ -19,16 +19,3 @@
|
|||||||
community.general.pacman:
|
community.general.pacman:
|
||||||
update_cache: true
|
update_cache: true
|
||||||
when: ansible_facts['os_family'] == 'Archlinux'
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
|
||||||
- name: Ensure /etc/docker directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/docker
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Configure Docker daemon for vfs storage driver (DinD)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/docker/daemon.json
|
|
||||||
content: |
|
|
||||||
{"storage-driver": "vfs"}
|
|
||||||
mode: "0644"
|
|
||||||
|
|||||||
@@ -9,42 +9,39 @@
|
|||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check Docker is installed
|
- name: Check runner user exists
|
||||||
ansible.builtin.command: docker --version
|
ansible.builtin.user:
|
||||||
changed_when: false
|
name: "{{ gitea_runner_service_user }}"
|
||||||
|
register: user_info
|
||||||
|
check_mode: true
|
||||||
|
|
||||||
- name: Check runner image was pulled
|
- name: Assert runner user exists
|
||||||
ansible.builtin.command: docker images gitea/runner --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }} .Tag {{ '}}' }}'
|
|
||||||
register: image_list
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Assert runner image exists locally
|
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'gitea/runner' in image_list.stdout"
|
- user_info.state == "present"
|
||||||
fail_msg: "Runner Docker image was not pulled"
|
fail_msg: "Runner system user was not created"
|
||||||
|
|
||||||
- name: Check prune timer exists
|
- name: Check runner binary exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: /etc/systemd/system/docker-prune.timer
|
path: "{{ gitea_runner_binary_path }}"
|
||||||
register: timer_stat
|
register: binary_stat
|
||||||
|
|
||||||
- name: Assert prune timer exists
|
- name: Assert runner binary exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- timer_stat.stat.exists
|
- binary_stat.stat.exists
|
||||||
fail_msg: "Docker prune timer is missing"
|
fail_msg: "Gitea runner binary is missing"
|
||||||
|
|
||||||
- name: Check systemd template unit exists
|
- name: Check systemd user service exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: service_stat
|
register: service_stat
|
||||||
|
|
||||||
- name: Assert template unit exists
|
- name: Assert user service exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- service_stat.stat.exists
|
- service_stat.stat.exists
|
||||||
fail_msg: "Systemd template unit is missing"
|
fail_msg: "Systemd user service is missing"
|
||||||
|
|
||||||
- name: Check instance data directory exists
|
- name: Check instance data directory exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
@@ -57,9 +54,9 @@
|
|||||||
- data_dir_stat.stat.exists
|
- data_dir_stat.stat.exists
|
||||||
fail_msg: "Instance data directory is missing"
|
fail_msg: "Instance data directory is missing"
|
||||||
|
|
||||||
- name: Check config file exists in data directory
|
- name: Check config file exists in config directory
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ gitea_runner_data_dir }}/config.yaml"
|
path: "{{ gitea_runner_config_dir }}/config.yaml"
|
||||||
register: config_stat
|
register: config_stat
|
||||||
|
|
||||||
- name: Assert config file exists
|
- name: Assert config file exists
|
||||||
@@ -67,3 +64,14 @@
|
|||||||
that:
|
that:
|
||||||
- config_stat.stat.exists
|
- config_stat.stat.exists
|
||||||
fail_msg: "Config file is missing"
|
fail_msg: "Config file is missing"
|
||||||
|
|
||||||
|
- name: Check prune timer exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||||
|
register: timer_stat
|
||||||
|
|
||||||
|
- name: Assert prune timer exists
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- timer_stat.stat.exists
|
||||||
|
fail_msg: "Docker prune timer is missing"
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "deregister-test-runner"
|
runner_name: "deregister-test-runner"
|
||||||
|
pre_tasks:
|
||||||
|
- name: Load role defaults
|
||||||
|
ansible.builtin.include_vars:
|
||||||
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Ensure fake .runner file exists
|
- name: Ensure fake .runner file exists
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: "/var/lib/gitea-runner/{{ runner_name }}/.runner"
|
dest: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
content: |
|
content: |
|
||||||
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"}
|
{"id": 1, "uuid": "test-uuid-1234", "name": "{{ runner_name }}", "address": "http://localhost:3000"}
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
- name: Deregister runner
|
- name: Deregister runner
|
||||||
@@ -19,7 +25,6 @@
|
|||||||
runner_name: "deregister-test-runner"
|
runner_name: "deregister-test-runner"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
runner_mode: "docker"
|
|
||||||
skip_runner_registration: false
|
skip_runner_registration: false
|
||||||
tasks:
|
tasks:
|
||||||
- name: Include deregistration tasks
|
- name: Include deregistration tasks
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Check registration file was removed
|
- name: Check registration file was removed
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/var/lib/gitea-runner/deregister-test-runner/.runner"
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
register: runner_file_stat
|
register: runner_file_stat
|
||||||
|
|
||||||
- name: Assert registration file no longer exists
|
- name: Assert registration file no longer exists
|
||||||
@@ -18,13 +18,13 @@
|
|||||||
- not runner_file_stat.stat.exists
|
- not runner_file_stat.stat.exists
|
||||||
fail_msg: "Registration file (.runner) was not removed by deregistration"
|
fail_msg: "Registration file (.runner) was not removed by deregistration"
|
||||||
|
|
||||||
- name: Check systemd template still exists
|
- name: Check systemd user service still exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: service_stat
|
register: service_stat
|
||||||
|
|
||||||
- name: Assert systemd template still exists after deregister
|
- name: Assert user service still exists after deregister
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- service_stat.stat.exists
|
- service_stat.stat.exists
|
||||||
fail_msg: "Systemd template unit was incorrectly removed"
|
fail_msg: "Systemd user service was incorrectly removed"
|
||||||
|
|||||||
@@ -5,24 +5,17 @@
|
|||||||
vars:
|
vars:
|
||||||
runner_name: "lifecycle-test-runner"
|
runner_name: "lifecycle-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Check if systemd is available
|
- name: Load role defaults
|
||||||
ansible.builtin.stat:
|
ansible.builtin.include_vars:
|
||||||
path: /run/systemd/system
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
register: systemd_available
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Stop gitea-runner systemd instance
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user stop gitea-runner"
|
||||||
name: "gitea-runner-docker@{{ runner_name }}"
|
changed_when: true
|
||||||
state: stopped
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Disable gitea-runner systemd instance
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user disable gitea-runner"
|
||||||
name: "gitea-runner-docker@{{ runner_name }}"
|
changed_when: true
|
||||||
enabled: false
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Re-enable and start runner
|
- name: Re-enable and start runner
|
||||||
hosts: all
|
hosts: all
|
||||||
@@ -30,21 +23,14 @@
|
|||||||
vars:
|
vars:
|
||||||
runner_name: "lifecycle-test-runner"
|
runner_name: "lifecycle-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Check if systemd is available
|
- name: Load role defaults
|
||||||
ansible.builtin.stat:
|
ansible.builtin.include_vars:
|
||||||
path: /run/systemd/system
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
register: systemd_available
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Enable gitea-runner systemd instance
|
- name: Enable gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user enable gitea-runner"
|
||||||
name: "gitea-runner-docker@{{ runner_name }}"
|
changed_when: true
|
||||||
enabled: true
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Start gitea-runner systemd instance
|
- name: Start gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: "sudo -u {{ gitea_runner_service_user }} systemctl --user start gitea-runner"
|
||||||
name: "gitea-runner-docker@{{ runner_name }}"
|
changed_when: true
|
||||||
state: started
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|||||||
@@ -9,38 +9,20 @@
|
|||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check systemd template unit exists
|
- name: Check systemd user service exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: template_stat
|
register: service_stat
|
||||||
|
|
||||||
- name: Assert template unit exists
|
- name: Assert user service exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- template_stat.stat.exists
|
- service_stat.stat.exists
|
||||||
fail_msg: "Systemd template unit is missing"
|
fail_msg: "Systemd user service is missing"
|
||||||
|
|
||||||
- name: Check if systemd is available
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: /run/systemd/system
|
|
||||||
register: systemd_available
|
|
||||||
|
|
||||||
- name: Check instance is enabled
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-{{ runner_mode | default('docker') }}@lifecycle-test-runner"
|
|
||||||
register: service_status
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Assert instance is enabled
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- service_status.status.LoadState == 'loaded'
|
|
||||||
fail_msg: "Systemd instance is not loaded"
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Check instance data directory exists after lifecycle
|
- name: Check instance data directory exists after lifecycle
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
|
path: "{{ gitea_runner_data_dir }}"
|
||||||
register: data_dir_stat
|
register: data_dir_stat
|
||||||
|
|
||||||
- name: Assert instance data directory exists
|
- name: Assert instance data directory exists
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "molecule-runner-a"
|
runner_name: "molecule-runner-a"
|
||||||
runner_mode: "binary"
|
|
||||||
skip_runner_registration: true
|
skip_runner_registration: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea-runner
|
||||||
@@ -18,7 +17,6 @@
|
|||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "molecule-runner-b"
|
runner_name: "molecule-runner-b"
|
||||||
runner_mode: "binary"
|
|
||||||
skip_runner_registration: true
|
skip_runner_registration: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea-runner
|
||||||
|
|||||||
@@ -6,24 +6,32 @@
|
|||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
- name: Override runner mode for multi-instance scenario
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_mode: "binary"
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check systemd template unit exists
|
- name: Check first runner user exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-{{ runner_mode | default('docker') }}@.service"
|
path: "{{ gitea_runner_base_home }}/grm-molecule-runner-a"
|
||||||
register: template_stat
|
register: home_a_stat
|
||||||
|
|
||||||
- name: Assert template unit exists
|
- name: Assert first runner user home exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- template_stat.stat.exists
|
- home_a_stat.stat.exists
|
||||||
fail_msg: "Systemd template unit is missing"
|
fail_msg: "First runner user home is missing"
|
||||||
|
|
||||||
|
- name: Check second runner user exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_base_home }}/grm-molecule-runner-b"
|
||||||
|
register: home_b_stat
|
||||||
|
|
||||||
|
- name: Assert second runner user home exists
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- home_b_stat.stat.exists
|
||||||
|
fail_msg: "Second runner user home is missing"
|
||||||
|
|
||||||
- name: Check first instance data directory exists
|
- name: Check first instance data directory exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/var/lib/gitea-runner/molecule-runner-a"
|
path: "{{ gitea_runner_base_data_dir }}/molecule-runner-a"
|
||||||
register: data_a_stat
|
register: data_a_stat
|
||||||
|
|
||||||
- name: Assert first instance data directory exists
|
- name: Assert first instance data directory exists
|
||||||
@@ -34,7 +42,7 @@
|
|||||||
|
|
||||||
- name: Check second instance data directory exists
|
- name: Check second instance data directory exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/var/lib/gitea-runner/molecule-runner-b"
|
path: "{{ gitea_runner_base_data_dir }}/molecule-runner-b"
|
||||||
register: data_b_stat
|
register: data_b_stat
|
||||||
|
|
||||||
- name: Assert second instance data directory exists
|
- name: Assert second instance data directory exists
|
||||||
@@ -45,7 +53,7 @@
|
|||||||
|
|
||||||
- name: Check first instance config exists
|
- name: Check first instance config exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/gitea-runner/molecule-runner-a/config.yaml"
|
path: "{{ gitea_runner_base_config_dir }}/molecule-runner-a/config.yaml"
|
||||||
register: config_a_stat
|
register: config_a_stat
|
||||||
|
|
||||||
- name: Assert first instance config exists
|
- name: Assert first instance config exists
|
||||||
@@ -56,7 +64,7 @@
|
|||||||
|
|
||||||
- name: Check second instance config exists
|
- name: Check second instance config exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/gitea-runner/molecule-runner-b/config.yaml"
|
path: "{{ gitea_runner_base_config_dir }}/molecule-runner-b/config.yaml"
|
||||||
register: config_b_stat
|
register: config_b_stat
|
||||||
|
|
||||||
- name: Assert second instance config exists
|
- name: Assert second instance config exists
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
---
|
---
|
||||||
- name: Converge Docker mode
|
- name: Converge
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "template-docker-runner"
|
runner_name: "template-test-runner"
|
||||||
skip_runner_registration: true
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
|
|
||||||
- name: Converge Binary mode
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "template-binary-runner"
|
|
||||||
runner_mode: "binary"
|
|
||||||
skip_runner_registration: true
|
skip_runner_registration: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea-runner
|
||||||
|
|||||||
@@ -2,65 +2,42 @@
|
|||||||
- name: Verify
|
- name: Verify
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
runner_name: "template-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check Docker mode systemd template exists
|
- name: Check systemd user service exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-docker@.service"
|
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: docker_template_stat
|
register: service_stat
|
||||||
|
|
||||||
- name: Assert Docker mode systemd template exists
|
- name: Assert user service exists
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- docker_template_stat.stat.exists
|
- service_stat.stat.exists
|
||||||
fail_msg: "Docker systemd template is missing"
|
fail_msg: "Systemd user service is missing"
|
||||||
|
|
||||||
- name: Read rendered Docker systemd template
|
- name: Read rendered user service template
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: "/etc/systemd/system/gitea-runner-docker@.service"
|
src: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: docker_template
|
register: service_template
|
||||||
|
|
||||||
- name: Assert Docker mode template contains expected directives
|
- name: Assert user service template contains expected directives
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'Type=oneshot' in docker_template.content | b64decode"
|
- "'Type=simple' in service_template.content | b64decode"
|
||||||
- "'RemainAfterExit=yes' in docker_template.content | b64decode"
|
- "'ExecStart={{ gitea_runner_binary_path }}' in service_template.content | b64decode"
|
||||||
- "'Requires=docker.service' in docker_template.content | b64decode"
|
- "'Restart=on-failure' in service_template.content | b64decode"
|
||||||
- "'docker run -d' in docker_template.content | b64decode"
|
- "'DOCKER_HOST=unix:///run/user' in service_template.content | b64decode"
|
||||||
- "'ExecStop=/usr/bin/docker stop' in docker_template.content | b64decode"
|
- "'XDG_RUNTIME_DIR=/run/user' in service_template.content | b64decode"
|
||||||
- "'Restart=on-failure' in docker_template.content | b64decode"
|
fail_msg: "User service template is missing expected directives"
|
||||||
fail_msg: "Docker systemd template is missing expected directives"
|
|
||||||
|
|
||||||
- name: Check Binary mode systemd template exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/etc/systemd/system/gitea-runner-binary@.service"
|
|
||||||
register: binary_template_stat
|
|
||||||
|
|
||||||
- name: Assert Binary mode systemd template exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- binary_template_stat.stat.exists
|
|
||||||
fail_msg: "Binary systemd template is missing"
|
|
||||||
|
|
||||||
- name: Read rendered Binary systemd template
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "/etc/systemd/system/gitea-runner-binary@.service"
|
|
||||||
register: binary_template
|
|
||||||
|
|
||||||
- name: Assert Binary mode template contains expected directives
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- "'Type=simple' in binary_template.content | b64decode"
|
|
||||||
- "'ExecStart={{ gitea_runner_binary_path }}' in binary_template.content | b64decode"
|
|
||||||
- "'Restart=on-failure' in binary_template.content | b64decode"
|
|
||||||
fail_msg: "Binary systemd template is missing expected directives"
|
|
||||||
|
|
||||||
- name: Read rendered prune service template
|
- name: Read rendered prune service template
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: "/etc/systemd/system/docker-prune.service"
|
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
||||||
register: prune_service
|
register: prune_service
|
||||||
|
|
||||||
- name: Assert prune service contains expected directives
|
- name: Assert prune service contains expected directives
|
||||||
@@ -69,17 +46,16 @@
|
|||||||
- "'Type=oneshot' in prune_service.content | b64decode"
|
- "'Type=oneshot' in prune_service.content | b64decode"
|
||||||
- "'docker system prune' in prune_service.content | b64decode"
|
- "'docker system prune' in prune_service.content | b64decode"
|
||||||
- "'docker volume prune' in prune_service.content | b64decode"
|
- "'docker volume prune' in prune_service.content | b64decode"
|
||||||
- "'label=gitea-runner=true' in prune_service.content | b64decode"
|
|
||||||
fail_msg: "Prune service template is missing expected directives"
|
fail_msg: "Prune service template is missing expected directives"
|
||||||
|
|
||||||
- name: Read rendered prune timer template
|
- name: Read rendered prune timer template
|
||||||
ansible.builtin.slurp:
|
ansible.builtin.slurp:
|
||||||
src: "/etc/systemd/system/docker-prune.timer"
|
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||||
register: prune_timer
|
register: prune_timer
|
||||||
|
|
||||||
- name: Assert prune timer contains expected directives
|
- name: Assert prune timer contains expected directives
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- "'OnCalendar=daily' in prune_timer.content | b64decode"
|
- "'OnCalendar={{ gitea_runner_prune_schedule }}' in prune_timer.content | b64decode"
|
||||||
- "'Persistent=true' in prune_timer.content | b64decode"
|
- "'Persistent=true' in prune_timer.content | b64decode"
|
||||||
fail_msg: "Prune timer template is missing expected directives"
|
fail_msg: "Prune timer template is missing expected directives"
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
---
|
---
|
||||||
- name: Converge Docker mode
|
- name: Converge
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
gitea_url: "http://localhost:3000"
|
gitea_url: "http://localhost:3000"
|
||||||
registration_token: "fake-token-for-testing"
|
registration_token: "fake-token-for-testing"
|
||||||
runner_name: "update-docker-runner"
|
runner_name: "update-test-runner"
|
||||||
skip_runner_registration: true
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
|
|
||||||
- name: Converge Binary mode
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "update-binary-runner"
|
|
||||||
runner_mode: "binary"
|
|
||||||
skip_runner_registration: true
|
skip_runner_registration: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea-runner
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
---
|
---
|
||||||
- name: Update Docker mode runner
|
- name: Update runner
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
vars:
|
vars:
|
||||||
runner_name: "update-docker-runner"
|
runner_name: "update-test-runner"
|
||||||
runner_mode: "docker"
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Include Docker update tasks
|
- name: Include update tasks
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: docker_update.yml
|
tasks_from: update_runner.yml
|
||||||
|
|
||||||
- name: Update Binary mode runner
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
runner_name: "update-binary-runner"
|
|
||||||
runner_mode: "binary"
|
|
||||||
tasks:
|
|
||||||
- name: Include binary update tasks
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: gitea-runner
|
|
||||||
tasks_from: binary_update.yml
|
|
||||||
|
|||||||
@@ -2,23 +2,14 @@
|
|||||||
- name: Verify
|
- name: Verify
|
||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
runner_name: "update-test-runner"
|
||||||
pre_tasks:
|
pre_tasks:
|
||||||
- name: Load role defaults
|
- name: Load role defaults
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check Docker runner image still exists after update
|
- name: Check runner binary still exists after update
|
||||||
ansible.builtin.command: docker images gitea/runner --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }} .Tag {{ '}}' }}'
|
|
||||||
register: image_list
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Assert runner image exists after update
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- "'gitea/runner' in image_list.stdout"
|
|
||||||
fail_msg: "Runner Docker image missing after update"
|
|
||||||
|
|
||||||
- name: Check binary runner executable still exists after update
|
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "{{ gitea_runner_binary_path }}"
|
path: "{{ gitea_runner_binary_path }}"
|
||||||
register: binary_stat
|
register: binary_stat
|
||||||
@@ -27,48 +18,26 @@
|
|||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- binary_stat.stat.exists
|
- binary_stat.stat.exists
|
||||||
fail_msg: "Binary runner executable missing after update"
|
fail_msg: "Runner binary missing after update"
|
||||||
|
|
||||||
- name: Check Docker mode systemd template still exists
|
- name: Check systemd user service still exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-docker@.service"
|
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
register: docker_service_stat
|
register: service_stat
|
||||||
|
|
||||||
- name: Assert Docker mode systemd template still exists after update
|
- name: Assert user service exists after update
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- docker_service_stat.stat.exists
|
- service_stat.stat.exists
|
||||||
fail_msg: "Docker mode systemd template missing after update"
|
fail_msg: "Systemd user service missing after update"
|
||||||
|
|
||||||
- name: Check Binary mode systemd template still exists
|
- name: Check instance data directory still exists
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
path: "/etc/systemd/system/gitea-runner-binary@.service"
|
path: "{{ gitea_runner_data_dir }}"
|
||||||
register: binary_service_stat
|
register: data_stat
|
||||||
|
|
||||||
- name: Assert Binary mode systemd template still exists after update
|
- name: Assert data directory exists after update
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- binary_service_stat.stat.exists
|
- data_stat.stat.exists
|
||||||
fail_msg: "Binary mode systemd template missing after update"
|
fail_msg: "Runner data directory missing after update"
|
||||||
|
|
||||||
- name: Check Docker mode data directory still exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/var/lib/gitea-runner/update-docker-runner"
|
|
||||||
register: docker_data_stat
|
|
||||||
|
|
||||||
- name: Assert Docker mode data directory exists after update
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- docker_data_stat.stat.exists
|
|
||||||
fail_msg: "Docker runner data directory missing after update"
|
|
||||||
|
|
||||||
- name: Check Binary mode data directory still exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "/var/lib/gitea-runner/update-binary-runner"
|
|
||||||
register: binary_data_stat
|
|
||||||
|
|
||||||
- name: Assert Binary mode data directory exists after update
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- binary_data_stat.stat.exists
|
|
||||||
fail_msg: "Binary runner data directory missing after update"
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Include gitea_runner download
|
|
||||||
ansible.builtin.include_tasks: download_gitea_runner.yml
|
|
||||||
|
|
||||||
- name: Restart gitea-runner systemd instance
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}"
|
|
||||||
state: restarted
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available is defined and systemd_available.stat is defined and systemd_available.stat.exists
|
|
||||||
@@ -27,34 +27,18 @@
|
|||||||
--no-interactive
|
--no-interactive
|
||||||
args:
|
args:
|
||||||
chdir: "{{ gitea_runner_data_dir }}"
|
chdir: "{{ gitea_runner_data_dir }}"
|
||||||
|
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:
|
when:
|
||||||
- runner_file_stat.stat.exists | default(false) | bool
|
- runner_file_stat.stat.exists | default(false) | bool
|
||||||
- not skip_runner_registration
|
- not skip_runner_registration
|
||||||
- runner_mode == 'binary'
|
|
||||||
register: deregister_output
|
register: deregister_output
|
||||||
changed_when: deregister_output.rc == 0
|
changed_when: deregister_output.rc == 0
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Deregister runner with Gitea via Docker
|
|
||||||
ansible.builtin.command: >
|
|
||||||
docker run --rm
|
|
||||||
--entrypoint /usr/local/bin/gitea-runner
|
|
||||||
-v {{ gitea_runner_data_dir }}:/data
|
|
||||||
-w /data
|
|
||||||
{{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }}
|
|
||||||
delete
|
|
||||||
--token {{ registration_token }}
|
|
||||||
--name {{ runner_name }}
|
|
||||||
--instance {{ gitea_url }}
|
|
||||||
--no-interactive
|
|
||||||
when:
|
|
||||||
- runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
- not skip_runner_registration
|
|
||||||
- runner_mode == 'docker'
|
|
||||||
register: deregister_docker_output
|
|
||||||
changed_when: deregister_docker_output.rc == 0
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Remove runner registration file
|
- name: Remove runner registration file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
path: "{{ gitea_runner_data_dir }}/.runner"
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if Docker is already installed
|
|
||||||
ansible.builtin.command: docker --version
|
|
||||||
register: docker_check
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Install Docker (Debian/Ubuntu)
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == 'Debian'
|
|
||||||
- docker_check.rc != 0
|
|
||||||
block:
|
|
||||||
- name: Remove old Docker apt repository files
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: absent
|
|
||||||
loop:
|
|
||||||
- /etc/apt/sources.list.d/docker.list
|
|
||||||
- /etc/apt/keyrings/docker.gpg
|
|
||||||
|
|
||||||
- name: Install prerequisite packages
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- apt-transport-https
|
|
||||||
- ca-certificates
|
|
||||||
- curl
|
|
||||||
- gnupg
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
cache_valid_time: 0
|
|
||||||
|
|
||||||
- name: Add Docker GPG key
|
|
||||||
ansible.builtin.get_url:
|
|
||||||
url: https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg
|
|
||||||
dest: "{{ docker_gpg_key_path }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Add Docker repository
|
|
||||||
ansible.builtin.deb822_repository:
|
|
||||||
name: docker
|
|
||||||
types: deb
|
|
||||||
uris: https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
|
|
||||||
suites: "{{ ansible_facts['distribution_release'] }}"
|
|
||||||
components: stable
|
|
||||||
signed_by: "{{ docker_gpg_key_path }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Install Docker packages
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- docker-ce
|
|
||||||
- docker-ce-cli
|
|
||||||
- containerd.io
|
|
||||||
- docker-compose-plugin
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
cache_valid_time: 0
|
|
||||||
|
|
||||||
- name: Install Docker (Arch Linux)
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == 'Archlinux'
|
|
||||||
- docker_check.rc != 0
|
|
||||||
block:
|
|
||||||
- name: Install Docker packages
|
|
||||||
community.general.pacman:
|
|
||||||
name:
|
|
||||||
- docker
|
|
||||||
- docker-compose
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Ensure Docker service is running (systemd)
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: docker
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
when: systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Start Docker daemon manually (no systemd)
|
|
||||||
ansible.builtin.shell: nohup dockerd > /var/log/dockerd.log 2>&1 &
|
|
||||||
when:
|
|
||||||
- not systemd_available.stat.exists
|
|
||||||
- docker_check.rc != 0
|
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
- name: Wait for Docker daemon to be ready
|
|
||||||
ansible.builtin.command: docker version
|
|
||||||
register: docker_ready
|
|
||||||
until: docker_ready.rc == 0
|
|
||||||
retries: 10
|
|
||||||
delay: 2
|
|
||||||
changed_when: false
|
|
||||||
when: not systemd_available.stat.exists
|
|
||||||
|
|
||||||
- name: Add user to docker group
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ ansible_user | default(ansible_facts['user_id']) }}"
|
|
||||||
groups: docker
|
|
||||||
append: true
|
|
||||||
when: ansible_user is defined or ansible_facts['user_id'] is defined
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Include Docker installation
|
|
||||||
ansible.builtin.include_tasks: docker.yml
|
|
||||||
|
|
||||||
- name: Ensure runner data directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_data_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Create gitea_runner config file in data directory
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: gitea-runner-config.yaml.j2
|
|
||||||
dest: "{{ gitea_runner_data_dir }}/config.yaml"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Pull gitea_runner image
|
|
||||||
ansible.builtin.command: docker pull {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }}
|
|
||||||
changed_when: "'Downloaded newer image' in pull_result.stdout or 'Status: Downloaded' in pull_result.stdout"
|
|
||||||
register: pull_result
|
|
||||||
|
|
||||||
- name: Check if runner is already registered
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_registered
|
|
||||||
|
|
||||||
- name: Register runner with Gitea via Docker
|
|
||||||
ansible.builtin.command: >
|
|
||||||
docker run --rm
|
|
||||||
--entrypoint /usr/local/bin/gitea-runner
|
|
||||||
-v {{ gitea_runner_data_dir }}:/data
|
|
||||||
-w /data
|
|
||||||
{{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }}
|
|
||||||
register
|
|
||||||
--token {{ registration_token }}
|
|
||||||
--name {{ runner_name }}
|
|
||||||
--instance {{ gitea_url }}
|
|
||||||
--labels {{ runner_labels }}
|
|
||||||
--no-interactive
|
|
||||||
when:
|
|
||||||
- not runner_registered.stat.exists
|
|
||||||
- not skip_runner_registration
|
|
||||||
register: register_output
|
|
||||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
|
||||||
timeout: 60
|
|
||||||
|
|
||||||
- name: Restart gitea-runner service after registration
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-docker@{{ runner_name }}"
|
|
||||||
state: restarted
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Include service setup
|
|
||||||
ansible.builtin.include_tasks: service.yml
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Pull gitea_runner image
|
|
||||||
ansible.builtin.command: docker pull {{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }}
|
|
||||||
changed_when: "'Downloaded newer image' in pull_result.stdout or 'Status: Downloaded' in pull_result.stdout"
|
|
||||||
register: pull_result
|
|
||||||
|
|
||||||
- name: Restart gitea-runner systemd instance
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}"
|
|
||||||
state: restarted
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available is defined and systemd_available.stat is defined and systemd_available.stat.exists
|
|
||||||
+7
-11
@@ -1,18 +1,14 @@
|
|||||||
---
|
---
|
||||||
- name: Include Docker installation
|
|
||||||
ansible.builtin.include_tasks: docker.yml
|
|
||||||
|
|
||||||
- name: Include gitea_runner download
|
- name: Include gitea_runner download
|
||||||
ansible.builtin.include_tasks: download_gitea_runner.yml
|
ansible.builtin.include_tasks: download_gitea_runner.yml
|
||||||
|
|
||||||
- name: Ensure runner data directory exists
|
- name: Create gitea_runner config file
|
||||||
ansible.builtin.file:
|
ansible.builtin.template:
|
||||||
path: "{{ gitea_runner_data_dir }}"
|
src: gitea-runner-config.yaml.j2
|
||||||
state: directory
|
dest: "{{ gitea_runner_config_dir }}/config.yaml"
|
||||||
mode: "0755"
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
- name: Include config creation
|
mode: "0644"
|
||||||
ansible.builtin.include_tasks: config.yml
|
|
||||||
|
|
||||||
- name: Include validation
|
- name: Include validation
|
||||||
ansible.builtin.include_tasks: validate.yml
|
ansible.builtin.include_tasks: validate.yml
|
||||||
@@ -18,19 +18,14 @@
|
|||||||
else {} }}
|
else {} }}
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
when: runner_file_stat.stat.exists | default(false) | bool
|
||||||
|
|
||||||
- name: Verify runner container running (Docker mode)
|
- name: Verify runner user service active
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.command: systemctl --user is-active gitea-runner
|
||||||
docker ps --filter "name=gitea-runner-{{ runner_name }}" --format "{{ '{{.Status}}' }}"
|
become: true
|
||||||
register: docker_check
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
changed_when: false
|
environment:
|
||||||
when: runner_mode == 'docker'
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
|
||||||
- name: Verify runner service active (Binary mode)
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}"
|
|
||||||
state: started
|
|
||||||
register: service_check
|
register: service_check
|
||||||
when: runner_mode == 'binary'
|
changed_when: false
|
||||||
|
|
||||||
- name: Validate runner installation
|
- name: Validate runner installation
|
||||||
ansible.builtin.fail:
|
ansible.builtin.fail:
|
||||||
@@ -39,16 +34,12 @@
|
|||||||
{% if not (runner_file_stat.stat.exists | default(false)) %}
|
{% if not (runner_file_stat.stat.exists | default(false)) %}
|
||||||
- Registration file (.runner) is missing. Registration may have failed.
|
- Registration file (.runner) is missing. Registration may have failed.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if runner_mode == 'docker' and not (docker_check.stdout | default('')) %}
|
{% if not (service_check.stdout | default('') | trim) == 'active' %}
|
||||||
- Docker container is not running.
|
- Systemd user service is not active.
|
||||||
{% endif %}
|
|
||||||
{% if runner_mode == 'binary' and not (service_check.status.ActiveState | default('')) == 'active' %}
|
|
||||||
- Systemd service is not active.
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
when: >
|
when: >
|
||||||
not (runner_file_stat.stat.exists | default(false))
|
not (runner_file_stat.stat.exists | default(false))
|
||||||
or (runner_mode == 'docker' and not (docker_check.stdout | default('')))
|
or not (service_check.stdout | default('') | trim) == 'active'
|
||||||
or (runner_mode == 'binary' and not (service_check.status.ActiveState | default('')) == 'active')
|
|
||||||
|
|
||||||
- name: Report runner status
|
- name: Report runner status
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
@@ -58,8 +49,7 @@
|
|||||||
{% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %}
|
{% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %}
|
||||||
{% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %}
|
{% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %}
|
||||||
{% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %}
|
{% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %}
|
||||||
{% if runner_mode == 'docker' %}Container: {{ docker_check.stdout | default('unknown') }}{% endif %}
|
Service: {{ service_check.stdout | default('unknown') | trim }}
|
||||||
{% if runner_mode == 'binary' %}Service: {{ service_check.status.ActiveState | default('unknown') }}{% endif %}
|
|
||||||
|
|
||||||
- name: Optional Gitea API verification
|
- name: Optional Gitea API verification
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_tasks: systemd_check.yml
|
ansible.builtin.include_tasks: systemd_check.yml
|
||||||
|
|
||||||
- name: Install runner (Docker mode)
|
- name: Include user setup
|
||||||
ansible.builtin.include_tasks: docker_mode.yml
|
ansible.builtin.include_tasks: user_setup.yml
|
||||||
when: runner_mode == 'docker'
|
|
||||||
|
|
||||||
- name: Install runner (Binary mode)
|
- name: Include rootless Docker setup
|
||||||
ansible.builtin.include_tasks: binary_mode.yml
|
ansible.builtin.include_tasks: rootless_docker.yml
|
||||||
when: runner_mode == 'binary'
|
|
||||||
|
- name: Include runner install
|
||||||
|
ansible.builtin.include_tasks: install_runner.yml
|
||||||
|
|
||||||
- name: Include prune setup
|
- name: Include prune setup
|
||||||
ansible.builtin.include_tasks: prune.yml
|
ansible.builtin.include_tasks: prune.yml
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
---
|
---
|
||||||
- name: Create docker-prune service file
|
- name: Create docker-prune user service file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: docker-prune.service.j2
|
src: docker-prune.service.j2
|
||||||
dest: /etc/systemd/system/docker-prune.service
|
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
- name: Create docker-prune timer file
|
- name: Create docker-prune user timer file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: docker-prune.timer.j2
|
src: docker-prune.timer.j2
|
||||||
dest: /etc/systemd/system/docker-prune.timer
|
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
|
|
||||||
- name: Enable and start docker-prune timer
|
- name: Reload systemd user daemon for prune timer
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user daemon-reload
|
||||||
name: docker-prune.timer
|
become: true
|
||||||
state: started
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
enabled: true
|
environment:
|
||||||
daemon_reload: true
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Enable and start docker-prune user timer
|
||||||
|
ansible.builtin.command: systemctl --user enable --now docker-prune.timer
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_data_dir }}"
|
path: "{{ gitea_runner_data_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Check if runner is already registered
|
- name: Check if runner is already registered
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
|
|
||||||
- name: Register runner with Gitea
|
- name: Register runner with Gitea
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
"{{ gitea_runner_binary_path }}" register
|
{{ gitea_runner_binary_path }} register
|
||||||
--token {{ registration_token }}
|
--token {{ registration_token }}
|
||||||
--name {{ runner_name }}
|
--name {{ runner_name }}
|
||||||
--instance {{ gitea_url }}
|
--instance {{ gitea_url }}
|
||||||
@@ -20,14 +22,12 @@
|
|||||||
--no-interactive
|
--no-interactive
|
||||||
args:
|
args:
|
||||||
chdir: "{{ gitea_runner_data_dir }}"
|
chdir: "{{ gitea_runner_data_dir }}"
|
||||||
|
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: not runner_registered.stat.exists
|
when: not runner_registered.stat.exists
|
||||||
register: register_output
|
register: register_output
|
||||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
changed_when: "'already exists' not in register_output.stdout | default('')"
|
||||||
timeout: 60
|
timeout: 60
|
||||||
|
|
||||||
- name: Restart gitea-runner service after registration
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "gitea-runner-binary@{{ runner_name }}"
|
|
||||||
state: restarted
|
|
||||||
daemon_reload: true
|
|
||||||
when: systemd_available.stat.exists | default(false) | bool
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
- name: Install rootless Docker dependencies (Debian/Ubuntu)
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- uidmap
|
||||||
|
- slirp4netns
|
||||||
|
- fuse-overlayfs
|
||||||
|
- docker-ce
|
||||||
|
- docker-ce-cli
|
||||||
|
- docker-ce-rootless-extras
|
||||||
|
- containerd.io
|
||||||
|
- docker-compose-plugin
|
||||||
|
- rsync
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
cache_valid_time: 0
|
||||||
|
when: ansible_facts['os_family'] == 'Debian'
|
||||||
|
|
||||||
|
- name: Install rootless Docker dependencies (Arch Linux)
|
||||||
|
community.general.pacman:
|
||||||
|
name:
|
||||||
|
- docker
|
||||||
|
- docker-compose
|
||||||
|
- slirp4netns
|
||||||
|
- fuse-overlayfs
|
||||||
|
- rsync
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
when: ansible_facts['os_family'] == 'Archlinux'
|
||||||
|
|
||||||
|
- name: Check if rootless Docker is already set up
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
||||||
|
register: rootless_docker_check
|
||||||
|
|
||||||
|
- name: Set up rootless Docker for runner user
|
||||||
|
ansible.builtin.command: dockerd-rootless-setuptool.sh install
|
||||||
|
args:
|
||||||
|
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
when: not rootless_docker_check.stat.exists
|
||||||
|
|
||||||
|
- name: Start rootless Docker daemon (systemd user service)
|
||||||
|
ansible.builtin.command: systemctl --user start docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Enable rootless Docker daemon (systemd user service)
|
||||||
|
ansible.builtin.command: systemctl --user enable docker
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Wait for rootless Docker daemon to be ready
|
||||||
|
ansible.builtin.command: docker version
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
register: docker_ready
|
||||||
|
until: docker_ready.rc == 0
|
||||||
|
retries: 10
|
||||||
|
delay: 2
|
||||||
|
changed_when: false
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
---
|
---
|
||||||
- name: Create systemd template service file
|
- name: Create systemd user service file
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: gitea-runner@.service.j2
|
src: gitea-runner-user.service.j2
|
||||||
dest: "/etc/systemd/system/gitea-runner-{{ runner_mode }}@.service"
|
dest: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
notify:
|
|
||||||
- Reload systemd
|
|
||||||
- Restart gitea-runner
|
|
||||||
|
|
||||||
- name: Enable and start gitea-runner instance
|
- name: Reload systemd user daemon
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user daemon-reload
|
||||||
name: "gitea-runner-{{ runner_mode }}@{{ runner_name }}"
|
become: true
|
||||||
state: started
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
enabled: true
|
environment:
|
||||||
daemon_reload: true
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Enable and start gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user enable --now gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
changed_when: true
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Include gitea_runner download
|
||||||
|
ansible.builtin.include_tasks: download_gitea_runner.yml
|
||||||
|
|
||||||
|
- name: Restart gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user restart gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
when: systemd_available.stat.exists | default(false) | bool
|
||||||
|
changed_when: true
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
- name: Create per-runner system user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ gitea_runner_service_user }}"
|
||||||
|
home: "{{ gitea_runner_home }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
system: true
|
||||||
|
create_home: true
|
||||||
|
register: runner_user
|
||||||
|
|
||||||
|
- name: Set runner UID fact
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
gitea_runner_uid: "{{ runner_user.uid }}"
|
||||||
|
|
||||||
|
- name: Enable lingering for runner user
|
||||||
|
ansible.builtin.command: loginctl enable-linger {{ gitea_runner_service_user }}
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Ensure subuid entry for runner user
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/subuid
|
||||||
|
regexp: "^{{ gitea_runner_service_user }}:"
|
||||||
|
line: "{{ gitea_runner_service_user }}:100000:65536"
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Ensure subgid entry for runner user
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/subgid
|
||||||
|
regexp: "^{{ gitea_runner_service_user }}:"
|
||||||
|
line: "{{ gitea_runner_service_user }}:100000:65536"
|
||||||
|
create: true
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Ensure XDG_RUNTIME_DIR exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Ensure runner data directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_data_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Ensure runner config directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_config_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Ensure systemd user directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_home }}/.config/systemd/user"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ gitea_runner_service_user }}"
|
||||||
|
group: "{{ gitea_runner_service_user }}"
|
||||||
|
mode: "0755"
|
||||||
@@ -14,7 +14,12 @@
|
|||||||
register: gitea_runner_version_output
|
register: gitea_runner_version_output
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Verify Docker connectivity
|
- name: Verify rootless Docker connectivity
|
||||||
ansible.builtin.command: docker version
|
ansible.builtin.command: docker version
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
register: docker_version_output
|
register: docker_version_output
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Docker prune for Gitea runner resources
|
Description=Docker prune for Gitea runner resources
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||||
ExecStart=/usr/bin/docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
ExecStart=/usr/bin/docker system prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
||||||
ExecStart=/usr/bin/docker volume prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
ExecStart=/usr/bin/docker volume prune -f --filter "label={{ gitea_runner_prune_label }}" --filter "until={{ gitea_runner_prune_until }}"
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ runner:
|
|||||||
|
|
||||||
container:
|
container:
|
||||||
label: "{{ gitea_runner_container_label }}"
|
label: "{{ gitea_runner_container_label }}"
|
||||||
|
docker_host: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gitea Actions Runner (rootless)
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart={{ gitea_runner_binary_path }} daemon --config {{ gitea_runner_config_dir }}/config.yaml
|
||||||
|
WorkingDirectory={{ gitea_runner_data_dir }}
|
||||||
|
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||||
|
ExecStop=/bin/kill -TERM $MAINPID
|
||||||
|
TimeoutStopSec=30
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec={{ gitea_runner_service_restart_sec }}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Gitea Actions Runner %i ({% if runner_mode == 'docker' %}docker{% else %}binary{% endif %})
|
|
||||||
After=network.target{% if runner_mode == 'docker' %} docker.service
|
|
||||||
Requires=docker.service{% endif %}
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
{% if runner_mode == 'docker' %}
|
|
||||||
Type=oneshot
|
|
||||||
RemainAfterExit=yes
|
|
||||||
ExecStartPre=-/usr/bin/docker rm -f gitea-runner-%i
|
|
||||||
ExecStart=/usr/bin/docker run -d --name gitea-runner-%i \
|
|
||||||
--restart=no \
|
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
||||||
-v {{ gitea_runner_base_data_dir }}/%i:/data \
|
|
||||||
-w /data \
|
|
||||||
{{ gitea_runner_docker_image }}:{{ gitea_runner_version | regex_replace('^v', '') }} \
|
|
||||||
daemon --config /data/config.yaml
|
|
||||||
ExecStop=/usr/bin/docker stop -t 30 gitea-runner-%i
|
|
||||||
ExecStopPost=-/usr/bin/docker rm -f gitea-runner-%i
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec={{ gitea_runner_service_restart_sec }}
|
|
||||||
{% else %}
|
|
||||||
Type=simple
|
|
||||||
ExecStart={{ gitea_runner_binary_path }} daemon --config {{ gitea_runner_base_config_dir }}/%i/config.yaml
|
|
||||||
WorkingDirectory={{ gitea_runner_base_data_dir }}/%i
|
|
||||||
ExecStop=/bin/kill -TERM $MAINPID
|
|
||||||
TimeoutStopSec=30
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec={{ gitea_runner_service_restart_sec }}
|
|
||||||
User={{ gitea_runner_service_user }}
|
|
||||||
Group=docker
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -22,9 +22,11 @@
|
|||||||
- not runner_registered.stat.exists
|
- not runner_registered.stat.exists
|
||||||
- not skip_runner_registration | default(false)
|
- not skip_runner_registration | default(false)
|
||||||
|
|
||||||
- name: Start gitea-runner systemd instance
|
- name: Start gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
state: started
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
daemon_reload: true
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|||||||
@@ -9,19 +9,19 @@
|
|||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Check systemd status
|
- name: Check systemd user service status
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user is-active gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
register: service_status
|
register: service_status
|
||||||
|
changed_when: false
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|
||||||
- name: Report systemd status
|
- name: Report service status
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: >
|
msg: "Service gitea-runner: {{ service_status.stdout | default('unknown') | trim }}"
|
||||||
Service gitea-runner@{{ runner_name }}:
|
|
||||||
ActiveState={{ service_status.status.ActiveState | default('unknown') }},
|
|
||||||
SubState={{ service_status.status.SubState | default('unknown') }},
|
|
||||||
LoadState={{ service_status.status.LoadState | default('unknown') }}
|
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|
||||||
- name: Check runner registration file
|
- name: Check runner registration file
|
||||||
@@ -32,15 +32,3 @@
|
|||||||
- name: Report runner registration
|
- name: Report runner registration
|
||||||
ansible.builtin.debug:
|
ansible.builtin.debug:
|
||||||
msg: "Runner registration file exists: {{ runner_file_stat.stat.exists | default(false) }}"
|
msg: "Runner registration file exists: {{ runner_file_stat.stat.exists | default(false) }}"
|
||||||
|
|
||||||
- name: Check container status (Docker mode)
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
docker ps --filter "name=gitea-runner-{{ runner_name }}" --format "{{ '{{.Status}}' }}"
|
|
||||||
register: docker_check
|
|
||||||
changed_when: false
|
|
||||||
when: runner_mode | default('docker') == 'docker'
|
|
||||||
|
|
||||||
- name: Report container status
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "Container status: {{ docker_check.stdout | default('not running') }}"
|
|
||||||
when: runner_mode | default('docker') == 'docker'
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner systemd instance
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
name: "gitea-runner@{{ runner_name }}"
|
become: true
|
||||||
state: stopped
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||||
daemon_reload: true
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|||||||
@@ -4,14 +4,7 @@
|
|||||||
become: true
|
become: true
|
||||||
vars: {}
|
vars: {}
|
||||||
tasks:
|
tasks:
|
||||||
- name: Update runner (Docker mode)
|
- name: Update runner
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea-runner
|
||||||
tasks_from: docker_update.yml
|
tasks_from: update_runner.yml
|
||||||
when: runner_mode | default('docker') == 'docker'
|
|
||||||
|
|
||||||
- name: Update runner (Binary mode)
|
|
||||||
ansible.builtin.include_role:
|
|
||||||
name: gitea-runner
|
|
||||||
tasks_from: binary_update.yml
|
|
||||||
when: runner_mode | default('docker') == 'binary'
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Auto-merge PR by extracting task ID from branch and validating PR title.
|
"""Auto-merge PR by extracting task ID from branch and validating PR title.
|
||||||
|
|
||||||
|
Waits for CI checks to complete before attempting the merge.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
REPO_TOKEN=<token> python3 scripts/auto_merge.py <branch> <pr_title> <repo> <pr_number> [label_name]
|
REPO_TOKEN=<token> python3 scripts/auto_merge.py <branch> <pr_title> <repo> <pr_number> [label_name]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||||
@@ -16,6 +19,8 @@ from gitea_runner_manager.exceptions import APIError
|
|||||||
from gitea_runner_manager.i18n import _
|
from gitea_runner_manager.i18n import _
|
||||||
|
|
||||||
READY_TO_MERGE = "ready-to-merge"
|
READY_TO_MERGE = "ready-to-merge"
|
||||||
|
MAX_WAIT_SECONDS = 900 # 15 minutes
|
||||||
|
POLL_INTERVAL_SECONDS = 30
|
||||||
|
|
||||||
load_dotenv(override=True)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -45,6 +50,66 @@ def has_ready_to_merge_label(client: GiteaClient, pr_number: str) -> bool:
|
|||||||
return any(label.get("name") == READY_TO_MERGE for label in labels)
|
return any(label.get("name") == READY_TO_MERGE for label in labels)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_ci(
|
||||||
|
client: GiteaClient, sha: str, max_wait: int = MAX_WAIT_SECONDS, poll_interval: int = POLL_INTERVAL_SECONDS
|
||||||
|
) -> bool:
|
||||||
|
"""Poll commit statuses until all CI checks are complete (not pending).
|
||||||
|
|
||||||
|
Returns True if all checks are successful, False if any failed or timed out.
|
||||||
|
"""
|
||||||
|
elapsed = 0
|
||||||
|
while elapsed < max_wait:
|
||||||
|
statuses = client.get_commit_status(sha)
|
||||||
|
if not statuses:
|
||||||
|
click.echo(_("No CI checks reported yet, waiting..."))
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
elapsed += poll_interval
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Deduplicate by context — keep the latest status per context.
|
||||||
|
latest: dict[str, dict[str, object]] = {}
|
||||||
|
for s in statuses:
|
||||||
|
ctx = s.get("context", "")
|
||||||
|
if ctx not in latest or s.get("updated_at", "") > latest[ctx].get("updated_at", ""):
|
||||||
|
latest[ctx] = s
|
||||||
|
|
||||||
|
ci_statuses = {ctx: s for ctx, s in latest.items() if ctx.startswith("CI /")}
|
||||||
|
if not ci_statuses:
|
||||||
|
click.echo(_("No CI checks found yet, waiting..."))
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
elapsed += poll_interval
|
||||||
|
continue
|
||||||
|
|
||||||
|
pending = [ctx for ctx, s in ci_statuses.items() if s.get("status") in ("pending", "waiting")]
|
||||||
|
if not pending:
|
||||||
|
# All CI checks are complete — check if they all succeeded.
|
||||||
|
failed = [
|
||||||
|
ctx
|
||||||
|
for ctx, s in ci_statuses.items()
|
||||||
|
if s.get("status") not in ("success", "ok")
|
||||||
|
]
|
||||||
|
if failed:
|
||||||
|
click.echo(
|
||||||
|
_("CI checks failed: {failed}", failed=", ".join(sorted(failed)))
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
click.echo(_("All CI checks passed."))
|
||||||
|
return True
|
||||||
|
|
||||||
|
click.echo(
|
||||||
|
_(
|
||||||
|
"Waiting for CI checks: {pending} ({elapsed}s elapsed)",
|
||||||
|
pending=", ".join(sorted(pending)),
|
||||||
|
elapsed=elapsed,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
elapsed += poll_interval
|
||||||
|
|
||||||
|
click.echo(_("Timed out waiting for CI checks after {max_wait}s.", max_wait=max_wait))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("branch")
|
@click.argument("branch")
|
||||||
@click.argument("pr_title")
|
@click.argument("pr_title")
|
||||||
@@ -75,6 +140,18 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str, label_name: str)
|
|||||||
|
|
||||||
validate_pr_title(pr_title)
|
validate_pr_title(pr_title)
|
||||||
|
|
||||||
|
# Wait for CI checks to complete before attempting merge.
|
||||||
|
pr = client.get_pr(pr_number)
|
||||||
|
sha = pr.get("head", {}).get("sha", "")
|
||||||
|
if sha:
|
||||||
|
click.echo(_("Waiting for CI checks on commit {sha}...", sha=sha[:8]))
|
||||||
|
if not wait_for_ci(client, sha):
|
||||||
|
raise click.ClickException(
|
||||||
|
_("Cannot merge: CI checks did not pass. Please fix failing checks and re-label.")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
click.echo(_("Warning: could not determine PR head SHA, proceeding without CI wait."))
|
||||||
|
|
||||||
merge_title = f"{task_id}: {pr_title}"
|
merge_title = f"{task_id}: {pr_title}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ Usage:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
import re
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
import sys
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Distribute molecule scenarios across N parallel runners.
|
"""Distribute molecule (scenario, platform) pairs across N parallel runners.
|
||||||
|
|
||||||
Discovers all molecule scenarios under ansible/roles/*/molecule/ and
|
Discovers all molecule scenarios under ansible/roles/*/molecule/ and
|
||||||
splits them evenly across the requested number of runners.
|
crosses them with the supported OS platform matrix, then splits the
|
||||||
|
resulting test pairs evenly across the requested number of runners.
|
||||||
|
|
||||||
|
Each pair is printed as ``scenario|platform_name|platform_image|platform_command``
|
||||||
|
so the CI workflow can set the appropriate environment variables.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 scripts/distribute_molecule.py --runner-index 0 --max-runners 3
|
python3 scripts/distribute_molecule.py --runner-index 0 --max-runners 3
|
||||||
# prints: default deregister
|
# prints: default|ubuntu-2204|ubuntu:22.04| lifecycle|ubuntu-2204|ubuntu:22.04| ...
|
||||||
python3 scripts/distribute_molecule.py --list
|
python3 scripts/distribute_molecule.py --list
|
||||||
# prints all scenarios, one per line
|
# prints all scenarios, one per line
|
||||||
|
python3 scripts/distribute_molecule.py --list-platforms
|
||||||
|
# prints all platforms, one per line
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -23,6 +29,36 @@ from gitea_runner_manager.i18n import _
|
|||||||
DEFAULT_MAX_RUNNERS = 3
|
DEFAULT_MAX_RUNNERS = 3
|
||||||
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
|
MOLECULE_ROOT = Path("ansible/roles/gitea-runner/molecule")
|
||||||
|
|
||||||
|
#: Supported OS platform matrix.
|
||||||
|
#: Each entry maps a short name to (image, command).
|
||||||
|
PLATFORMS: list[dict[str, str]] = [
|
||||||
|
{"name": "ubuntu-2204", "image": "geerlingguy/docker-ubuntu2204-ansible:latest", "command": ""},
|
||||||
|
{"name": "ubuntu-2404", "image": "geerlingguy/docker-ubuntu2404-ansible:latest", "command": ""},
|
||||||
|
{"name": "debian-12", "image": "geerlingguy/docker-debian12-ansible:latest", "command": ""},
|
||||||
|
{"name": "archlinux", "image": "marcstraube/archlinux-ansible:latest", "command": "/usr/lib/systemd/systemd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestPair:
|
||||||
|
"""A (scenario, platform) combination to test."""
|
||||||
|
|
||||||
|
scenario: str
|
||||||
|
platform: dict[str, str]
|
||||||
|
|
||||||
|
def encode(self) -> str:
|
||||||
|
"""Serialize to a pipe-delimited string for CI consumption."""
|
||||||
|
return f"{self.scenario}|{self.platform['name']}|{self.platform['image']}|{self.platform['command']}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(encoded: str) -> TestPair:
|
||||||
|
"""Deserialize from a pipe-delimited string."""
|
||||||
|
parts = encoded.split("|")
|
||||||
|
return TestPair(
|
||||||
|
scenario=parts[0],
|
||||||
|
platform={"name": parts[1], "image": parts[2], "command": parts[3]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def discover_scenarios(root: Path | None = None) -> list[str]:
|
def discover_scenarios(root: Path | None = None) -> list[str]:
|
||||||
"""Return sorted list of molecule scenario directory names."""
|
"""Return sorted list of molecule scenario directory names."""
|
||||||
@@ -40,19 +76,26 @@ def discover_scenarios(root: Path | None = None) -> list[str]:
|
|||||||
return sorted(scenarios)
|
return sorted(scenarios)
|
||||||
|
|
||||||
|
|
||||||
def distribute(scenarios: list[str], max_runners: int) -> list[list[str]]:
|
def build_pairs(scenarios: list[str], platforms: list[dict[str, str]] | None = None) -> list[TestPair]:
|
||||||
"""Split *scenarios* into *max_runners* balanced groups (round-robin)."""
|
"""Build the full cross-product of scenarios and platforms."""
|
||||||
groups: list[list[str]] = [[] for _ in range(max_runners)]
|
if platforms is None:
|
||||||
for i, scenario in enumerate(scenarios):
|
platforms = PLATFORMS
|
||||||
groups[i % max_runners].append(scenario)
|
return [TestPair(s, p) for s in scenarios for p in platforms]
|
||||||
|
|
||||||
|
|
||||||
|
def distribute(pairs: list[TestPair], max_runners: int) -> list[list[TestPair]]:
|
||||||
|
"""Split *pairs* into *max_runners* balanced groups (round-robin)."""
|
||||||
|
groups: list[list[TestPair]] = [[] for _ in range(max_runners)]
|
||||||
|
for i, pair in enumerate(pairs):
|
||||||
|
groups[i % max_runners].append(pair)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
|
||||||
def scenarios_for_runner(
|
def pairs_for_runner(
|
||||||
scenarios: list[str], runner_index: int, max_runners: int
|
pairs: list[TestPair], runner_index: int, max_runners: int
|
||||||
) -> list[str]:
|
) -> list[TestPair]:
|
||||||
"""Return the subset of scenarios assigned to *runner_index*."""
|
"""Return the subset of pairs assigned to *runner_index*."""
|
||||||
groups = distribute(scenarios, max_runners)
|
groups = distribute(pairs, max_runners)
|
||||||
if runner_index < 0 or runner_index >= len(groups):
|
if runner_index < 0 or runner_index >= len(groups):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
_(
|
_(
|
||||||
@@ -84,19 +127,31 @@ def scenarios_for_runner(
|
|||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="List all discovered scenarios, one per line.",
|
help="List all discovered scenarios, one per line.",
|
||||||
)
|
)
|
||||||
def cli(runner_index: int | None, max_runners: int, list_all: bool) -> None:
|
@click.option(
|
||||||
|
"--list-platforms",
|
||||||
|
"list_platforms",
|
||||||
|
is_flag=True,
|
||||||
|
help="List all supported platforms, one per line.",
|
||||||
|
)
|
||||||
|
def cli(runner_index: int | None, max_runners: int, list_all: bool, list_platforms: bool) -> None:
|
||||||
scenarios = discover_scenarios()
|
scenarios = discover_scenarios()
|
||||||
if list_all:
|
if list_all:
|
||||||
for s in scenarios:
|
for s in scenarios:
|
||||||
click.echo(s)
|
click.echo(s)
|
||||||
return
|
return
|
||||||
if runner_index is None:
|
if list_platforms:
|
||||||
groups = distribute(scenarios, max_runners)
|
for p in PLATFORMS:
|
||||||
for i, group in enumerate(groups):
|
click.echo(f"{p['name']}|{p['image']}|{p['command']}")
|
||||||
click.echo(f"Runner {i}: {' '.join(group) if group else '(none)'}")
|
|
||||||
return
|
return
|
||||||
assigned = scenarios_for_runner(scenarios, runner_index, max_runners)
|
pairs = build_pairs(scenarios)
|
||||||
click.echo(" ".join(assigned))
|
if runner_index is None:
|
||||||
|
groups = distribute(pairs, max_runners)
|
||||||
|
for i, group in enumerate(groups):
|
||||||
|
labels = " ".join(p.encode() for p in group) if group else "(none)"
|
||||||
|
click.echo(f"Runner {i}: {labels}")
|
||||||
|
return
|
||||||
|
assigned = pairs_for_runner(pairs, runner_index, max_runners)
|
||||||
|
click.echo(" ".join(p.encode() for p in assigned))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -10,10 +10,8 @@ from __future__ import annotations
|
|||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess # nosec B404
|
import subprocess # nosec B404
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run all molecule scenarios on all supported OS platforms.
|
||||||
|
# Used by `make molecule-all`. Sequential — CI uses parallel matrix instead.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MOLECULE_BIN="$(realpath "${BIN:-.venv/bin}/molecule")"
|
||||||
|
ROLE_DIR="$(cd "$(dirname "$0")/.." && pwd)/ansible/roles/gitea-runner"
|
||||||
|
|
||||||
|
for p in \
|
||||||
|
ubuntu-2204:geerlingguy/docker-ubuntu2204-ansible:latest: \
|
||||||
|
ubuntu-2404:geerlingguy/docker-ubuntu2404-ansible:latest: \
|
||||||
|
debian-12:geerlingguy/docker-debian12-ansible:latest: \
|
||||||
|
archlinux:marcstraube/archlinux-ansible:latest:/usr/lib/systemd/systemd
|
||||||
|
do
|
||||||
|
IFS=":" read -r name image command <<< "$p"
|
||||||
|
export MOLECULE_PLATFORM_NAME="$name" MOLECULE_PLATFORM_IMAGE="$image"
|
||||||
|
if [ -n "$command" ]; then
|
||||||
|
export MOLECULE_PLATFORM_COMMAND="$command"
|
||||||
|
else
|
||||||
|
unset MOLECULE_PLATFORM_COMMAND
|
||||||
|
fi
|
||||||
|
echo "=== Platform: $name ==="
|
||||||
|
for s in default multi-instance lifecycle template-content deregister update; do
|
||||||
|
echo "--- Scenario: $s on $name ---"
|
||||||
|
(
|
||||||
|
cd "$ROLE_DIR"
|
||||||
|
if [ "$s" = "default" ]; then
|
||||||
|
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 "$MOLECULE_BIN" test
|
||||||
|
else
|
||||||
|
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 "$MOLECULE_BIN" test -s "$s"
|
||||||
|
fi
|
||||||
|
)
|
||||||
|
done
|
||||||
|
done
|
||||||
@@ -116,6 +116,16 @@ class GiteaClient:
|
|||||||
payload = {"Do": "squash", "MergeTitleField": merge_title}
|
payload = {"Do": "squash", "MergeTitleField": merge_title}
|
||||||
self._request("POST", f"/pulls/{pr_number}/merge", json=payload)
|
self._request("POST", f"/pulls/{pr_number}/merge", json=payload)
|
||||||
|
|
||||||
|
def get_commit_status(self, sha: str) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch all status check contexts reported for a commit."""
|
||||||
|
r = self._request("GET", f"/commits/{sha}/statuses")
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_pr(self, pr_number: str | int) -> dict[str, Any]:
|
||||||
|
"""Fetch pull request details including mergeable state."""
|
||||||
|
r = self._request("GET", f"/pulls/{pr_number}")
|
||||||
|
return r.json()
|
||||||
|
|
||||||
def create_release(
|
def create_release(
|
||||||
self,
|
self,
|
||||||
tag: str,
|
tag: str,
|
||||||
|
|||||||
@@ -69,13 +69,6 @@ def cli() -> None:
|
|||||||
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
||||||
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
default="docker",
|
|
||||||
help=_("Gitea Runner deployment mode (default: docker)"),
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--url",
|
"--url",
|
||||||
default=lambda: os.getenv("GITEA_URL", ""),
|
default=lambda: os.getenv("GITEA_URL", ""),
|
||||||
@@ -111,7 +104,6 @@ def install(
|
|||||||
key: str | None,
|
key: str | None,
|
||||||
name: str | None,
|
name: str | None,
|
||||||
token: str | None,
|
token: str | None,
|
||||||
mode: str,
|
|
||||||
url: str,
|
url: str,
|
||||||
admin_token: str | None,
|
admin_token: str | None,
|
||||||
integration_retries: int,
|
integration_retries: int,
|
||||||
@@ -127,7 +119,6 @@ def install(
|
|||||||
name=name,
|
name=name,
|
||||||
token=token,
|
token=token,
|
||||||
gitea_url=url,
|
gitea_url=url,
|
||||||
mode=mode,
|
|
||||||
admin_token=admin_token,
|
admin_token=admin_token,
|
||||||
integration_retries=integration_retries,
|
integration_retries=integration_retries,
|
||||||
labels=labels or None,
|
labels=labels or None,
|
||||||
@@ -147,13 +138,6 @@ def install(
|
|||||||
)
|
)
|
||||||
@click.option("--key", "-k", default=lambda: os.getenv("GITEA_RUNNER_KEY"), help=_("Path to SSH private key"))
|
@click.option("--key", "-k", default=lambda: os.getenv("GITEA_RUNNER_KEY"), help=_("Path to SSH private key"))
|
||||||
@click.option("--version", "-v", help=_("Specific Gitea Runner version"))
|
@click.option("--version", "-v", help=_("Specific Gitea Runner version"))
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
default="docker",
|
|
||||||
help=_("Gitea Runner deployment mode (default: docker)"),
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--ask-become-pass/--no-ask-become-pass",
|
"--ask-become-pass/--no-ask-become-pass",
|
||||||
default=True,
|
default=True,
|
||||||
@@ -165,7 +149,6 @@ def update(
|
|||||||
user: str,
|
user: str,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
version: str | None,
|
version: str | None,
|
||||||
mode: str,
|
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -174,7 +157,6 @@ def update(
|
|||||||
user=user,
|
user=user,
|
||||||
key=key,
|
key=key,
|
||||||
version=version,
|
version=version,
|
||||||
mode=mode,
|
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -182,19 +164,12 @@ def update(
|
|||||||
@cli.command(help=_("Start a registered Gitea Runner."))
|
@cli.command(help=_("Start a registered Gitea Runner."))
|
||||||
@click.argument("runner_name")
|
@click.argument("runner_name")
|
||||||
@_runner_options
|
@_runner_options
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
help=_("Override deployment mode from registry"),
|
|
||||||
)
|
|
||||||
@_handle_errors("Start failed: {error}")
|
@_handle_errors("Start failed: {error}")
|
||||||
def start(
|
def start(
|
||||||
runner_name: str,
|
runner_name: str,
|
||||||
host: str | None,
|
host: str | None,
|
||||||
user: str | None,
|
user: str | None,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
mode: str | None,
|
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -203,7 +178,6 @@ def start(
|
|||||||
host=host,
|
host=host,
|
||||||
user=user,
|
user=user,
|
||||||
key=key,
|
key=key,
|
||||||
mode=mode,
|
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -259,12 +233,6 @@ def enable(
|
|||||||
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
||||||
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
help=_("Override deployment mode from registry"),
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--url",
|
"--url",
|
||||||
default=lambda: os.getenv("GITEA_URL", ""),
|
default=lambda: os.getenv("GITEA_URL", ""),
|
||||||
@@ -277,7 +245,6 @@ def disable(
|
|||||||
user: str | None,
|
user: str | None,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
token: str | None,
|
token: str | None,
|
||||||
mode: str | None,
|
|
||||||
url: str,
|
url: str,
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -289,7 +256,6 @@ def disable(
|
|||||||
key=key,
|
key=key,
|
||||||
token=token,
|
token=token,
|
||||||
gitea_url=url,
|
gitea_url=url,
|
||||||
mode=mode,
|
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -297,19 +263,12 @@ def disable(
|
|||||||
@cli.command(help=_("Check the status of a registered Gitea Runner."))
|
@cli.command(help=_("Check the status of a registered Gitea Runner."))
|
||||||
@click.argument("runner_name")
|
@click.argument("runner_name")
|
||||||
@_runner_options
|
@_runner_options
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
help=_("Override deployment mode from registry"),
|
|
||||||
)
|
|
||||||
@_handle_errors("Status check failed: {error}")
|
@_handle_errors("Status check failed: {error}")
|
||||||
def status(
|
def status(
|
||||||
runner_name: str,
|
runner_name: str,
|
||||||
host: str | None,
|
host: str | None,
|
||||||
user: str | None,
|
user: str | None,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
mode: str | None,
|
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -318,7 +277,6 @@ def status(
|
|||||||
host=host,
|
host=host,
|
||||||
user=user,
|
user=user,
|
||||||
key=key,
|
key=key,
|
||||||
mode=mode,
|
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -332,12 +290,6 @@ def status(
|
|||||||
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
||||||
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
"--mode",
|
|
||||||
"-m",
|
|
||||||
type=click.Choice(["docker", "binary"]),
|
|
||||||
help=_("Override deployment mode from registry"),
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--url",
|
"--url",
|
||||||
default=lambda: os.getenv("GITEA_URL", ""),
|
default=lambda: os.getenv("GITEA_URL", ""),
|
||||||
@@ -356,7 +308,6 @@ def remove(
|
|||||||
user: str | None,
|
user: str | None,
|
||||||
key: str | None,
|
key: str | None,
|
||||||
token: str | None,
|
token: str | None,
|
||||||
mode: str | None,
|
|
||||||
url: str,
|
url: str,
|
||||||
force: bool,
|
force: bool,
|
||||||
ask_become_pass: bool,
|
ask_become_pass: bool,
|
||||||
@@ -369,7 +320,6 @@ def remove(
|
|||||||
key=key,
|
key=key,
|
||||||
token=token,
|
token=token,
|
||||||
gitea_url=url,
|
gitea_url=url,
|
||||||
mode=mode,
|
|
||||||
ask_become_pass=ask_become_pass,
|
ask_become_pass=ask_become_pass,
|
||||||
force=force,
|
force=force,
|
||||||
)
|
)
|
||||||
@@ -385,7 +335,7 @@ def list_runners() -> None:
|
|||||||
click.echo(_("No runners registered. Use 'grm install' to add one."))
|
click.echo(_("No runners registered. Use 'grm install' to add one."))
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('USER'):<10} {_('MODE'):<8} {_('LABELS'):<30} {_('STATUS')}")
|
click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('USER'):<10} {_('LABELS'):<30} {_('STATUS')}")
|
||||||
click.echo("-" * 100)
|
click.echo("-" * 90)
|
||||||
for r in runners:
|
for r in runners:
|
||||||
click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['mode']:<8} {r['labels']:<30} {r['status']}")
|
click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['labels']:<30} {r['status']}")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ BRANCH_PROTECTION_CONFIG: dict[str, object] = {
|
|||||||
"branch_name": "master",
|
"branch_name": "master",
|
||||||
"enable_push": False,
|
"enable_push": False,
|
||||||
"enable_status_check": True,
|
"enable_status_check": True,
|
||||||
"status_check_contexts": ["lint", "unit-tests", "molecule-tests"],
|
"status_check_contexts": ["CI / quality", "CI / molecule-tests*"],
|
||||||
"required_approvals": 1,
|
"required_approvals": 1,
|
||||||
"dismiss_stale_approvals": True,
|
"dismiss_stale_approvals": True,
|
||||||
"block_on_outdated_branch": True,
|
"block_on_outdated_branch": True,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Local JSON registry for installed Gitea Runners.
|
"""Local JSON registry for installed Gitea Runners.
|
||||||
|
|
||||||
Stores connection metadata (host, user, key, mode) on the local machine
|
Stores connection metadata (host, user, key, labels) on the local machine
|
||||||
so that subsequent lifecycle commands only need the runner name.
|
so that subsequent lifecycle commands only need the runner name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ class RunnerRegistry:
|
|||||||
host: str,
|
host: str,
|
||||||
user: str,
|
user: str,
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
mode: str = "docker",
|
|
||||||
gitea_url: str = "",
|
gitea_url: str = "",
|
||||||
labels: str | None = None,
|
labels: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -53,7 +52,6 @@ class RunnerRegistry:
|
|||||||
"host": host,
|
"host": host,
|
||||||
"user": user,
|
"user": user,
|
||||||
"key": key,
|
"key": key,
|
||||||
"mode": mode,
|
|
||||||
"gitea_url": gitea_url,
|
"gitea_url": gitea_url,
|
||||||
"labels": labels,
|
"labels": labels,
|
||||||
"created_at": datetime.now(UTC).isoformat(),
|
"created_at": datetime.now(UTC).isoformat(),
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class RunnerManager:
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
gitea_url: str = "",
|
gitea_url: str = "",
|
||||||
mode: str = "docker",
|
|
||||||
admin_token: str | None = None,
|
admin_token: str | None = None,
|
||||||
integration_retries: int = 3,
|
integration_retries: int = 3,
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
@@ -47,7 +46,6 @@ class RunnerManager:
|
|||||||
|
|
||||||
extra_vars = (
|
extra_vars = (
|
||||||
f"registration_token={token} runner_name={name} gitea_url={gitea_url}"
|
f"registration_token={token} runner_name={name} gitea_url={gitea_url}"
|
||||||
f" runner_mode={mode}"
|
|
||||||
f" gitea_runner_integration_retries={integration_retries}"
|
f" gitea_runner_integration_retries={integration_retries}"
|
||||||
)
|
)
|
||||||
if admin_token:
|
if admin_token:
|
||||||
@@ -67,7 +65,6 @@ class RunnerManager:
|
|||||||
host=host,
|
host=host,
|
||||||
user=user,
|
user=user,
|
||||||
key=key,
|
key=key,
|
||||||
mode=mode,
|
|
||||||
gitea_url=gitea_url,
|
gitea_url=gitea_url,
|
||||||
labels=labels,
|
labels=labels,
|
||||||
)
|
)
|
||||||
@@ -79,11 +76,10 @@ class RunnerManager:
|
|||||||
user: str,
|
user: str,
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
version: str | None = None,
|
version: str | None = None,
|
||||||
mode: str = "docker",
|
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the gitea_runner binary on a remote host."""
|
"""Update the gitea_runner binary on a remote host."""
|
||||||
extra_vars = f"runner_mode={mode}"
|
extra_vars = ""
|
||||||
if version:
|
if version:
|
||||||
extra_vars += f" gitea_runner_version={version}"
|
extra_vars += f" gitea_runner_version={version}"
|
||||||
|
|
||||||
@@ -99,11 +95,10 @@ class RunnerManager:
|
|||||||
host: str | None = None,
|
host: str | None = None,
|
||||||
user: str | None = None,
|
user: str | None = None,
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
mode: str | None = None,
|
) -> tuple[str, str, str | None, str]:
|
||||||
) -> tuple[str, str, str | None, str, str]:
|
|
||||||
"""Look up runner metadata from registry, applying CLI overrides.
|
"""Look up runner metadata from registry, applying CLI overrides.
|
||||||
|
|
||||||
Returns ``(host, user, key, mode, gitea_url)`` where *gitea_url* is
|
Returns ``(host, user, key, gitea_url)`` where *gitea_url* is
|
||||||
taken from the registry when available, allowing ``disable`` and
|
taken from the registry when available, allowing ``disable`` and
|
||||||
``remove`` to reuse the value stored at install time.
|
``remove`` to reuse the value stored at install time.
|
||||||
"""
|
"""
|
||||||
@@ -114,12 +109,10 @@ class RunnerManager:
|
|||||||
actual_host = host
|
actual_host = host
|
||||||
actual_user = user
|
actual_user = user
|
||||||
actual_key = key
|
actual_key = key
|
||||||
actual_mode = mode or "docker"
|
|
||||||
elif info:
|
elif info:
|
||||||
actual_host = host or info["host"]
|
actual_host = host or info["host"]
|
||||||
actual_user = user or info["user"]
|
actual_user = user or info["user"]
|
||||||
actual_key = key if key is not None else info.get("key")
|
actual_key = key if key is not None else info.get("key")
|
||||||
actual_mode = mode or info.get("mode", "docker")
|
|
||||||
else:
|
else:
|
||||||
raise AnsibleError(
|
raise AnsibleError(
|
||||||
_(
|
_(
|
||||||
@@ -127,7 +120,7 @@ class RunnerManager:
|
|||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return actual_host, actual_user, actual_key, actual_mode, actual_gitea_url
|
return actual_host, actual_user, actual_key, actual_gitea_url
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
@@ -135,16 +128,13 @@ class RunnerManager:
|
|||||||
host: str | None = None,
|
host: str | None = None,
|
||||||
user: str | None = None,
|
user: str | None = None,
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
mode: str | None = None,
|
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Start a runner instance on a remote host."""
|
"""Start a runner instance on a remote host."""
|
||||||
actual_host, actual_user, actual_key, actual_mode, _gitea_url = self._resolve_runner(
|
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
name, host, user, key, mode
|
|
||||||
)
|
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host))
|
tracker.begin(_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host))
|
||||||
extra_vars = f"runner_name={name} runner_mode={actual_mode}"
|
extra_vars = f"runner_name={name}"
|
||||||
cmd = self._build_cmd("start-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass)
|
cmd = self._build_cmd("start-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass)
|
||||||
self._executor.run(
|
self._executor.run(
|
||||||
cmd, description=_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host)
|
cmd, description=_("Starting Gitea Runner {name} on {host}", name=name, host=actual_host)
|
||||||
@@ -160,7 +150,7 @@ class RunnerManager:
|
|||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stop a runner instance on a remote host."""
|
"""Stop a runner instance on a remote host."""
|
||||||
actual_host, actual_user, actual_key, _mode, _gitea_url = self._resolve_runner(name, host, user, key)
|
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Stopping Gitea Runner {name} on {host}", name=name, host=actual_host))
|
tracker.begin(_("Stopping Gitea Runner {name} on {host}", name=name, host=actual_host))
|
||||||
extra_vars = f"runner_name={name}"
|
extra_vars = f"runner_name={name}"
|
||||||
@@ -179,7 +169,7 @@ class RunnerManager:
|
|||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Enable a runner instance to start on boot."""
|
"""Enable a runner instance to start on boot."""
|
||||||
actual_host, actual_user, actual_key, _mode, _gitea_url = self._resolve_runner(name, host, user, key)
|
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Enabling Gitea Runner {name} on {host}", name=name, host=actual_host))
|
tracker.begin(_("Enabling Gitea Runner {name} on {host}", name=name, host=actual_host))
|
||||||
extra_vars = f"runner_name={name}"
|
extra_vars = f"runner_name={name}"
|
||||||
@@ -199,13 +189,10 @@ class RunnerManager:
|
|||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
gitea_url: str = "",
|
gitea_url: str = "",
|
||||||
mode: str | None = None,
|
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Disable and deregister a runner instance."""
|
"""Disable and deregister a runner instance."""
|
||||||
actual_host, actual_user, actual_key, actual_mode, registry_gitea_url = self._resolve_runner(
|
actual_host, actual_user, actual_key, registry_gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
name, host, user, key, mode
|
|
||||||
)
|
|
||||||
resolved_gitea_url = gitea_url or registry_gitea_url
|
resolved_gitea_url = gitea_url or registry_gitea_url
|
||||||
if not resolved_gitea_url:
|
if not resolved_gitea_url:
|
||||||
raise AnsibleError(_("GITEA_URL must be set (or pass --url)"))
|
raise AnsibleError(_("GITEA_URL must be set (or pass --url)"))
|
||||||
@@ -213,10 +200,7 @@ class RunnerManager:
|
|||||||
raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)"))
|
raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)"))
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Disabling Gitea Runner {name} on {host}", name=name, host=actual_host))
|
tracker.begin(_("Disabling Gitea Runner {name} on {host}", name=name, host=actual_host))
|
||||||
extra_vars = (
|
extra_vars = f"runner_name={name} registration_token={token} gitea_url={resolved_gitea_url}"
|
||||||
f"runner_name={name} registration_token={token}"
|
|
||||||
f" gitea_url={resolved_gitea_url} runner_mode={actual_mode}"
|
|
||||||
)
|
|
||||||
cmd = self._build_cmd(
|
cmd = self._build_cmd(
|
||||||
"disable-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
"disable-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
||||||
)
|
)
|
||||||
@@ -231,16 +215,13 @@ class RunnerManager:
|
|||||||
host: str | None = None,
|
host: str | None = None,
|
||||||
user: str | None = None,
|
user: str | None = None,
|
||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
mode: str | None = None,
|
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check the status of a runner instance."""
|
"""Check the status of a runner instance."""
|
||||||
actual_host, actual_user, actual_key, actual_mode, _gitea_url = self._resolve_runner(
|
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
name, host, user, key, mode
|
|
||||||
)
|
|
||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Checking status of Gitea Runner {name} on {host}", name=name, host=actual_host))
|
tracker.begin(_("Checking status of Gitea Runner {name} on {host}", name=name, host=actual_host))
|
||||||
extra_vars = f"runner_name={name} runner_mode={actual_mode}"
|
extra_vars = f"runner_name={name}"
|
||||||
cmd = self._build_cmd(
|
cmd = self._build_cmd(
|
||||||
"status-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
"status-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
||||||
)
|
)
|
||||||
@@ -257,7 +238,6 @@ class RunnerManager:
|
|||||||
key: str | None = None,
|
key: str | None = None,
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
gitea_url: str = "",
|
gitea_url: str = "",
|
||||||
mode: str | None = None,
|
|
||||||
ask_become_pass: bool = False,
|
ask_become_pass: bool = False,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -267,9 +247,7 @@ class RunnerManager:
|
|||||||
only remove the local registry entry. Use this when the remote
|
only remove the local registry entry. Use this when the remote
|
||||||
host is already gone or unreachable.
|
host is already gone or unreachable.
|
||||||
"""
|
"""
|
||||||
actual_host, actual_user, actual_key, actual_mode, registry_gitea_url = self._resolve_runner(
|
actual_host, actual_user, actual_key, registry_gitea_url = self._resolve_runner(name, host, user, key)
|
||||||
name, host, user, key, mode
|
|
||||||
)
|
|
||||||
resolved_gitea_url = gitea_url or registry_gitea_url
|
resolved_gitea_url = gitea_url or registry_gitea_url
|
||||||
resolved_token = token
|
resolved_token = token
|
||||||
if not force:
|
if not force:
|
||||||
@@ -280,10 +258,7 @@ class RunnerManager:
|
|||||||
with track_steps() as tracker:
|
with track_steps() as tracker:
|
||||||
tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host))
|
tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host))
|
||||||
if not force:
|
if not force:
|
||||||
extra_vars = (
|
extra_vars = f"runner_name={name} registration_token={resolved_token} gitea_url={resolved_gitea_url}"
|
||||||
f"runner_name={name} registration_token={resolved_token}"
|
|
||||||
f" gitea_url={resolved_gitea_url} runner_mode={actual_mode}"
|
|
||||||
)
|
|
||||||
cmd = self._build_cmd(
|
cmd = self._build_cmd(
|
||||||
"remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
"remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass
|
||||||
)
|
)
|
||||||
@@ -304,7 +279,6 @@ class RunnerManager:
|
|||||||
host = info["host"]
|
host = info["host"]
|
||||||
user = info["user"]
|
user = info["user"]
|
||||||
key = info.get("key")
|
key = info.get("key")
|
||||||
mode = info.get("mode", "docker")
|
|
||||||
say(
|
say(
|
||||||
_(
|
_(
|
||||||
"Checking status of Gitea Runner {name} on {host} as {user} (sudo required)",
|
"Checking status of Gitea Runner {name} on {host} as {user} (sudo required)",
|
||||||
@@ -315,41 +289,13 @@ class RunnerManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
service_status = "unknown"
|
service_status = "unknown"
|
||||||
if mode == "docker":
|
|
||||||
# Try expected container name first, then host-based fallback for legacy installs.
|
|
||||||
# Use python3 JSON parsing to avoid Jinja2 brace conflicts in Ansible shell args.
|
|
||||||
for container_name in (f"gitea-runner-{name}", f"gitea-runner-{host}"):
|
|
||||||
try:
|
try:
|
||||||
stdout = self._executor.run_ad_hoc(
|
stdout = self._executor.run_ad_hoc(
|
||||||
host,
|
host,
|
||||||
user,
|
user,
|
||||||
key,
|
key,
|
||||||
"shell",
|
"shell",
|
||||||
(
|
f"sudo -u grm-{name} systemctl --user is-active gitea-runner 2>/dev/null",
|
||||||
f"docker inspect {container_name} 2>/dev/null | "
|
|
||||||
f"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" "
|
|
||||||
f"2>/dev/null || echo unknown"
|
|
||||||
),
|
|
||||||
become=True,
|
|
||||||
ask_become_pass=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
except AnsibleError:
|
|
||||||
continue
|
|
||||||
status = self._parse_status(stdout)
|
|
||||||
if status != "unknown":
|
|
||||||
docker_map = {"running": "active", "exited": "inactive", "dead": "failed"}
|
|
||||||
service_status = docker_map.get(status, "unknown")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Final fallback: check systemd service (for installs using the fixed template)
|
|
||||||
try:
|
|
||||||
stdout = self._executor.run_ad_hoc(
|
|
||||||
host,
|
|
||||||
user,
|
|
||||||
key,
|
|
||||||
"shell",
|
|
||||||
f"systemctl is-active gitea-runner@{name} 2>/dev/null",
|
|
||||||
become=True,
|
become=True,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
check=False,
|
check=False,
|
||||||
@@ -357,22 +303,6 @@ class RunnerManager:
|
|||||||
service_status = self._parse_status(stdout)
|
service_status = self._parse_status(stdout)
|
||||||
except AnsibleError:
|
except AnsibleError:
|
||||||
service_status = "unknown"
|
service_status = "unknown"
|
||||||
else:
|
|
||||||
try:
|
|
||||||
stdout = self._executor.run_ad_hoc(
|
|
||||||
host,
|
|
||||||
user,
|
|
||||||
key,
|
|
||||||
"shell",
|
|
||||||
f"systemctl is-active gitea-runner@{name} 2>/dev/null",
|
|
||||||
become=True,
|
|
||||||
ask_become_pass=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
service_status = self._parse_status(stdout)
|
|
||||||
except AnsibleError:
|
|
||||||
service_status = "unknown"
|
|
||||||
# Translate known status values
|
|
||||||
translated_status = _(
|
translated_status = _(
|
||||||
service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown"
|
service_status if service_status in {"active", "inactive", "failed", "unknown"} else "unknown"
|
||||||
)
|
)
|
||||||
@@ -381,7 +311,6 @@ class RunnerManager:
|
|||||||
"name": name,
|
"name": name,
|
||||||
"host": host,
|
"host": host,
|
||||||
"user": user,
|
"user": user,
|
||||||
"mode": mode,
|
|
||||||
"labels": info.get("labels", ""),
|
"labels": info.get("labels", ""),
|
||||||
"status": translated_status,
|
"status": translated_status,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,46 +27,55 @@ class TestLifecycleCLI:
|
|||||||
mock_manager.install.assert_called_once()
|
mock_manager.install.assert_called_once()
|
||||||
|
|
||||||
# Start
|
# Start
|
||||||
result = runner.invoke(cli, ["start", "host1", "--user", "ubuntu", "--name", "r1"])
|
result = runner.invoke(cli, ["start", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.start.assert_called_once_with(
|
mock_manager.start.assert_called_once_with(
|
||||||
host="host1", user="ubuntu", name="r1", mode="docker", ask_become_pass=False
|
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
result = runner.invoke(cli, ["status", "host1", "--user", "ubuntu", "--name", "r1"])
|
result = runner.invoke(cli, ["status", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.status.assert_called_once_with(
|
mock_manager.status.assert_called_once_with(
|
||||||
host="host1", user="ubuntu", name="r1", mode="docker", ask_become_pass=False
|
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stop
|
# Stop
|
||||||
result = runner.invoke(cli, ["stop", "host1", "--user", "ubuntu", "--name", "r1"])
|
result = runner.invoke(cli, ["stop", "r1", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.stop.assert_called_once_with(host="host1", user="ubuntu", name="r1", ask_become_pass=False)
|
mock_manager.stop.assert_called_once_with(
|
||||||
|
host="host1", user="ubuntu", name="r1", key=None, ask_become_pass=False
|
||||||
|
)
|
||||||
|
|
||||||
# Disable
|
# Disable
|
||||||
result = runner.invoke(cli, ["disable", "host1", "--user", "ubuntu", "--name", "r1", "--token", "tok"])
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
["disable", "r1", "--host", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"],
|
||||||
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.disable.assert_called_once_with(
|
mock_manager.disable.assert_called_once_with(
|
||||||
host="host1",
|
host="host1",
|
||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
name="r1",
|
name="r1",
|
||||||
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
ask_become_pass=False,
|
ask_become_pass=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove
|
# Remove
|
||||||
result = runner.invoke(cli, ["remove", "host1", "--user", "ubuntu", "--name", "r1", "--token", "tok"])
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
["remove", "r1", "--host", "host1", "--user", "ubuntu", "--token", "tok", "--no-ask-become-pass"],
|
||||||
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.remove.assert_called_once_with(
|
mock_manager.remove.assert_called_once_with(
|
||||||
host="host1",
|
host="host1",
|
||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
name="r1",
|
name="r1",
|
||||||
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
ask_become_pass=False,
|
ask_become_pass=False,
|
||||||
|
force=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,15 +26,13 @@ class TestMultiInstanceCLI:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
# Install instance B
|
# Install instance B
|
||||||
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--name", "runner-b", "--mode", "binary"])
|
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--name", "runner-b"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
assert mock_manager.install.call_count == 2
|
assert mock_manager.install.call_count == 2
|
||||||
calls = mock_manager.install.call_args_list
|
calls = mock_manager.install.call_args_list
|
||||||
assert calls[0].kwargs["name"] == "runner-a"
|
assert calls[0].kwargs["name"] == "runner-a"
|
||||||
assert calls[0].kwargs["mode"] == "docker"
|
|
||||||
assert calls[1].kwargs["name"] == "runner-b"
|
assert calls[1].kwargs["name"] == "runner-b"
|
||||||
assert calls[1].kwargs["mode"] == "binary"
|
|
||||||
|
|
||||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||||
def test_start_stop_one_instance(self, mock_manager_class: MagicMock) -> None:
|
def test_start_stop_one_instance(self, mock_manager_class: MagicMock) -> None:
|
||||||
@@ -44,12 +42,16 @@ class TestMultiInstanceCLI:
|
|||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
result = runner.invoke(cli, ["start", "host1", "--user", "ubuntu", "--name", "runner-a"])
|
result = runner.invoke(
|
||||||
|
cli, ["start", "runner-a", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"]
|
||||||
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.start.assert_called_once_with(
|
mock_manager.start.assert_called_once_with(
|
||||||
host="host1", user="ubuntu", name="runner-a", mode="docker", ask_become_pass=False
|
host="host1", user="ubuntu", name="runner-a", key=None, ask_become_pass=False
|
||||||
)
|
)
|
||||||
|
|
||||||
result = runner.invoke(cli, ["stop", "host1", "--user", "ubuntu", "--name", "runner-b"])
|
result = runner.invoke(cli, ["stop", "runner-b", "--host", "host1", "--user", "ubuntu", "--no-ask-become-pass"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.stop.assert_called_once_with(host="host1", user="ubuntu", name="runner-b", ask_become_pass=False)
|
mock_manager.stop.assert_called_once_with(
|
||||||
|
host="host1", user="ubuntu", name="runner-b", key=None, ask_become_pass=False
|
||||||
|
)
|
||||||
|
|||||||
@@ -201,6 +201,33 @@ class TestGiteaClient:
|
|||||||
timeout=DEFAULT_TIMEOUT,
|
timeout=DEFAULT_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_commit_status(self) -> None:
|
||||||
|
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||||
|
client._session.request = MagicMock(
|
||||||
|
return_value=_mock_response([{"context": "CI / quality", "status": "success"}])
|
||||||
|
)
|
||||||
|
|
||||||
|
result = client.get_commit_status("abc123")
|
||||||
|
assert result == [{"context": "CI / quality", "status": "success"}]
|
||||||
|
client._session.request.assert_called_once_with(
|
||||||
|
"GET",
|
||||||
|
"https://git.example.com/repos/owner/repo/commits/abc123/statuses",
|
||||||
|
timeout=DEFAULT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_pr(self) -> None:
|
||||||
|
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||||
|
client._session.request = MagicMock(return_value=_mock_response({"number": 7, "head": {"sha": "abc123"}}))
|
||||||
|
|
||||||
|
result = client.get_pr(7)
|
||||||
|
assert result["number"] == 7
|
||||||
|
assert result["head"]["sha"] == "abc123"
|
||||||
|
client._session.request.assert_called_once_with(
|
||||||
|
"GET",
|
||||||
|
"https://git.example.com/repos/owner/repo/pulls/7",
|
||||||
|
timeout=DEFAULT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
def test_update_repo_settings(self) -> None:
|
def test_update_repo_settings(self) -> None:
|
||||||
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
|
||||||
client._session.request = MagicMock(return_value=_mock_response({"default_delete_branch_after_merge": True}))
|
client._session.request = MagicMock(return_value=_mock_response({"default_delete_branch_after_merge": True}))
|
||||||
|
|||||||
@@ -9,7 +9,21 @@ from click.testing import CliRunner
|
|||||||
|
|
||||||
from gitea_runner_manager.config import CONVENTIONAL_RE, TASK_ID_RE
|
from gitea_runner_manager.config import CONVENTIONAL_RE, TASK_ID_RE
|
||||||
from gitea_runner_manager.exceptions import APIError
|
from gitea_runner_manager.exceptions import APIError
|
||||||
from scripts.auto_merge import extract_task_id, has_ready_to_merge_label, main, validate_pr_title
|
from scripts.auto_merge import (
|
||||||
|
extract_task_id,
|
||||||
|
has_ready_to_merge_label,
|
||||||
|
main,
|
||||||
|
validate_pr_title,
|
||||||
|
wait_for_ci,
|
||||||
|
)
|
||||||
|
|
||||||
|
CI_SUCCESS = "success"
|
||||||
|
CI_PENDING = "pending"
|
||||||
|
CI_FAILURE = "failure"
|
||||||
|
|
||||||
|
|
||||||
|
def _status(context: str, status: str, updated_at: str = "2026-01-01T00:00:00Z") -> dict[str, str]:
|
||||||
|
return {"context": context, "status": status, "updated_at": updated_at}
|
||||||
|
|
||||||
|
|
||||||
class TestRegexes:
|
class TestRegexes:
|
||||||
@@ -70,12 +84,91 @@ class TestHasReadyToMergeLabel:
|
|||||||
assert has_ready_to_merge_label(client, "5") is False
|
assert has_ready_to_merge_label(client, "5") is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestWaitForCi:
|
||||||
|
def test_all_pass_immediately(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.return_value = [
|
||||||
|
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||||
|
_status("CI / molecule-tests (0) (pull_request)", CI_SUCCESS),
|
||||||
|
]
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||||
|
|
||||||
|
def test_waits_then_passes(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.side_effect = [
|
||||||
|
[_status("CI / quality (pull_request)", CI_PENDING)],
|
||||||
|
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||||
|
]
|
||||||
|
with patch("scripts.auto_merge.time.sleep"):
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||||
|
|
||||||
|
def test_fails_on_failed_check(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.return_value = [
|
||||||
|
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||||
|
_status("CI / molecule-tests (0) (pull_request)", CI_FAILURE),
|
||||||
|
]
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10) is False
|
||||||
|
|
||||||
|
def test_times_out(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.return_value = [
|
||||||
|
_status("CI / quality (pull_request)", CI_PENDING),
|
||||||
|
]
|
||||||
|
with patch("scripts.auto_merge.time.sleep"):
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=5) is False
|
||||||
|
|
||||||
|
def test_no_statuses_waits(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.side_effect = [
|
||||||
|
[],
|
||||||
|
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||||
|
]
|
||||||
|
with patch("scripts.auto_merge.time.sleep"):
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||||
|
|
||||||
|
def test_ignores_non_ci_contexts(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.return_value = [
|
||||||
|
_status("Auto-merge / merge (pull_request)", CI_PENDING),
|
||||||
|
_status("CI / quality (pull_request)", CI_SUCCESS),
|
||||||
|
]
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||||
|
|
||||||
|
def test_deduplicates_by_latest(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.return_value = [
|
||||||
|
_status("CI / quality (pull_request)", CI_PENDING, "2026-01-01T00:00:00Z"),
|
||||||
|
_status("CI / quality (pull_request)", CI_SUCCESS, "2026-01-01T00:01:00Z"),
|
||||||
|
]
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10) is True
|
||||||
|
|
||||||
|
def test_only_non_ci_contexts_waits_then_ci_appears(self) -> None:
|
||||||
|
client = MagicMock()
|
||||||
|
client.get_commit_status.side_effect = [
|
||||||
|
[_status("Auto-merge / merge (pull_request)", CI_PENDING)],
|
||||||
|
[_status("CI / quality (pull_request)", CI_SUCCESS)],
|
||||||
|
]
|
||||||
|
with patch("scripts.auto_merge.time.sleep"):
|
||||||
|
assert wait_for_ci(client, "abc123", max_wait=10, poll_interval=5) is True
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_pr(sha: str = "abc123def456") -> dict[str, object]:
|
||||||
|
return {"head": {"sha": sha}}
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_ci_passing() -> list[dict[str, str]]:
|
||||||
|
return [_status("CI / quality (pull_request)", CI_SUCCESS)]
|
||||||
|
|
||||||
|
|
||||||
class TestMain:
|
class TestMain:
|
||||||
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||||
@patch("scripts.auto_merge.GiteaClient")
|
@patch("scripts.auto_merge.GiteaClient")
|
||||||
def test_successful_flow_with_label_arg(self, mock_client_cls: MagicMock) -> None:
|
def test_successful_flow_with_label_arg(self, mock_client_cls: MagicMock) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_cls.return_value = mock_client
|
mock_client_cls.return_value = mock_client
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
main,
|
main,
|
||||||
@@ -91,6 +184,8 @@ class TestMain:
|
|||||||
"""Label not passed via arg, but PR has ready-to-merge via API."""
|
"""Label not passed via arg, but PR has ready-to-merge via API."""
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||||
mock_client_cls.return_value = mock_client
|
mock_client_cls.return_value = mock_client
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -123,6 +218,8 @@ class TestMain:
|
|||||||
"""Gitea Actions doesn't populate label name, but API shows ready-to-merge."""
|
"""Gitea Actions doesn't populate label name, but API shows ready-to-merge."""
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||||
mock_client_cls.return_value = mock_client
|
mock_client_cls.return_value = mock_client
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -166,6 +263,8 @@ class TestMain:
|
|||||||
def test_merge_pr_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
|
def test_merge_pr_failure_raises_click(self, mock_client_cls: MagicMock) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||||
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
|
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.INTERNAL_SERVER_ERROR, "server error")
|
||||||
mock_client_cls.return_value = mock_client
|
mock_client_cls.return_value = mock_client
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@@ -178,9 +277,41 @@ class TestMain:
|
|||||||
def test_merge_pr_json_parse_failure(self, mock_client_cls: MagicMock) -> None:
|
def test_merge_pr_json_parse_failure(self, mock_client_cls: MagicMock) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = _mock_ci_passing()
|
||||||
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.BAD_GATEWAY, "bad gateway")
|
mock_client.merge_pr.side_effect = APIError(http.HTTPStatus.BAD_GATEWAY, "bad gateway")
|
||||||
mock_client_cls.return_value = mock_client
|
mock_client_cls.return_value = mock_client
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert str(http.HTTPStatus.BAD_GATEWAY) in result.output
|
assert str(http.HTTPStatus.BAD_GATEWAY) in result.output
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||||
|
@patch("scripts.auto_merge.GiteaClient")
|
||||||
|
def test_ci_failure_blocks_merge(self, mock_client_cls: MagicMock) -> None:
|
||||||
|
"""CI checks fail — merge should not be attempted."""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = _mock_pr()
|
||||||
|
mock_client.get_commit_status.return_value = [
|
||||||
|
_status("CI / quality (pull_request)", CI_FAILURE),
|
||||||
|
]
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "CI checks did not pass" in result.output
|
||||||
|
mock_client.merge_pr.assert_not_called()
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
|
||||||
|
@patch("scripts.auto_merge.GiteaClient")
|
||||||
|
def test_no_sha_proceeds_without_wait(self, mock_client_cls: MagicMock) -> None:
|
||||||
|
"""PR head SHA missing — should proceed without waiting."""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.get_pr_labels.return_value = [{"name": "ready-to-merge"}]
|
||||||
|
mock_client.get_pr.return_value = {"head": {}}
|
||||||
|
mock_client_cls.return_value = mock_client
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["GRM-19-fix", "fix: bug", "owner/repo", "1"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "squash-merged" in result.output
|
||||||
|
|||||||
+1
-56
@@ -29,7 +29,6 @@ class TestCLI:
|
|||||||
name=None,
|
name=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
admin_token="",
|
admin_token="",
|
||||||
integration_retries=3,
|
integration_retries=3,
|
||||||
labels=None,
|
labels=None,
|
||||||
@@ -51,7 +50,6 @@ class TestCLI:
|
|||||||
name=None,
|
name=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
admin_token="",
|
admin_token="",
|
||||||
integration_retries=3,
|
integration_retries=3,
|
||||||
labels=None,
|
labels=None,
|
||||||
@@ -91,7 +89,6 @@ class TestCLI:
|
|||||||
name=None,
|
name=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
admin_token=None,
|
admin_token=None,
|
||||||
integration_retries=3,
|
integration_retries=3,
|
||||||
labels=None,
|
labels=None,
|
||||||
@@ -141,7 +138,6 @@ class TestCLI:
|
|||||||
name="r1",
|
name="r1",
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
admin_token="",
|
admin_token="",
|
||||||
integration_retries=3,
|
integration_retries=3,
|
||||||
labels=None,
|
labels=None,
|
||||||
@@ -163,29 +159,6 @@ class TestCLI:
|
|||||||
name=None,
|
name=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode="docker",
|
|
||||||
admin_token="",
|
|
||||||
integration_retries=3,
|
|
||||||
labels=None,
|
|
||||||
ask_become_pass=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
|
||||||
def test_install_binary_mode(self, mock_manager_class: MagicMock) -> None:
|
|
||||||
mock_manager = MagicMock()
|
|
||||||
mock_manager_class.return_value = mock_manager
|
|
||||||
|
|
||||||
runner = CliRunner(env={"GITEA_URL": "https://git.example.com", "REPO_TOKEN": "", "GITEA_RUNNER_LABELS": ""})
|
|
||||||
result = runner.invoke(cli, ["install", "host1", "--user", "ubuntu", "--token", "tok", "--mode", "binary"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
mock_manager.install.assert_called_once_with(
|
|
||||||
host="host1",
|
|
||||||
user="ubuntu",
|
|
||||||
key=None,
|
|
||||||
name=None,
|
|
||||||
token="tok",
|
|
||||||
gitea_url="https://git.example.com",
|
|
||||||
mode="binary",
|
|
||||||
admin_token="",
|
admin_token="",
|
||||||
integration_retries=3,
|
integration_retries=3,
|
||||||
labels=None,
|
labels=None,
|
||||||
@@ -230,7 +203,6 @@ class TestCLI:
|
|||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
key="/key",
|
key="/key",
|
||||||
version="v0.2.0",
|
version="v0.2.0",
|
||||||
mode="docker",
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -247,24 +219,6 @@ class TestCLI:
|
|||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
key=None,
|
key=None,
|
||||||
version=None,
|
version=None,
|
||||||
mode="docker",
|
|
||||||
ask_become_pass=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
|
||||||
def test_update_binary_mode(self, mock_manager_class: MagicMock) -> None:
|
|
||||||
mock_manager = MagicMock()
|
|
||||||
mock_manager_class.return_value = mock_manager
|
|
||||||
|
|
||||||
runner = CliRunner()
|
|
||||||
result = runner.invoke(cli, ["update", "host1", "--user", "ubuntu", "--mode", "binary"])
|
|
||||||
assert result.exit_code == 0
|
|
||||||
mock_manager.update.assert_called_once_with(
|
|
||||||
host="host1",
|
|
||||||
user="ubuntu",
|
|
||||||
key=None,
|
|
||||||
version=None,
|
|
||||||
mode="binary",
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -294,7 +248,6 @@ class TestCLI:
|
|||||||
host=None,
|
host=None,
|
||||||
user=None,
|
user=None,
|
||||||
key=None,
|
key=None,
|
||||||
mode=None,
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -304,14 +257,13 @@ class TestCLI:
|
|||||||
mock_manager_class.return_value = mock_manager
|
mock_manager_class.return_value = mock_manager
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser", "--mode", "binary"])
|
result = runner.invoke(cli, ["start", "r1", "--host", "newhost", "--user", "newuser"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_manager.start.assert_called_once_with(
|
mock_manager.start.assert_called_once_with(
|
||||||
name="r1",
|
name="r1",
|
||||||
host="newhost",
|
host="newhost",
|
||||||
user="newuser",
|
user="newuser",
|
||||||
key=None,
|
key=None,
|
||||||
mode="binary",
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,7 +314,6 @@ class TestCLI:
|
|||||||
key=None,
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -396,7 +347,6 @@ class TestCLI:
|
|||||||
key=None,
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -426,7 +376,6 @@ class TestCLI:
|
|||||||
host=None,
|
host=None,
|
||||||
user=None,
|
user=None,
|
||||||
key=None,
|
key=None,
|
||||||
mode=None,
|
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -445,7 +394,6 @@ class TestCLI:
|
|||||||
key=None,
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
|
||||||
force=False,
|
force=False,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
@@ -465,7 +413,6 @@ class TestCLI:
|
|||||||
key=None,
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
|
||||||
force=True,
|
force=True,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
@@ -552,7 +499,6 @@ class TestCLI:
|
|||||||
key=None,
|
key=None,
|
||||||
token="tok",
|
token="tok",
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
mode=None,
|
|
||||||
force=False,
|
force=False,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
)
|
)
|
||||||
@@ -578,7 +524,6 @@ class TestCLI:
|
|||||||
"name": "r1",
|
"name": "r1",
|
||||||
"host": "10.0.0.1",
|
"host": "10.0.0.1",
|
||||||
"user": "ubuntu",
|
"user": "ubuntu",
|
||||||
"mode": "docker",
|
|
||||||
"labels": "docker:docker://alpine:latest",
|
"labels": "docker:docker://alpine:latest",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class TestConfigConstants:
|
|||||||
assert BRANCH_PROTECTION_CONFIG["branch_name"] == "master"
|
assert BRANCH_PROTECTION_CONFIG["branch_name"] == "master"
|
||||||
assert BRANCH_PROTECTION_CONFIG["enable_push"] is False
|
assert BRANCH_PROTECTION_CONFIG["enable_push"] is False
|
||||||
assert BRANCH_PROTECTION_CONFIG["required_approvals"] == 1
|
assert BRANCH_PROTECTION_CONFIG["required_approvals"] == 1
|
||||||
assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["lint", "unit-tests", "molecule-tests"]
|
assert BRANCH_PROTECTION_CONFIG["status_check_contexts"] == ["CI / quality", "CI / molecule-tests*"]
|
||||||
|
|
||||||
def test_label_config(self) -> None:
|
def test_label_config(self) -> None:
|
||||||
assert LABEL_CONFIG["name"] == "ready-to-merge"
|
assert LABEL_CONFIG["name"] == "ready-to-merge"
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import pytest
|
|||||||
|
|
||||||
from scripts.distribute_molecule import (
|
from scripts.distribute_molecule import (
|
||||||
MOLECULE_ROOT,
|
MOLECULE_ROOT,
|
||||||
|
PLATFORMS,
|
||||||
|
TestPair,
|
||||||
|
build_pairs,
|
||||||
discover_scenarios,
|
discover_scenarios,
|
||||||
distribute,
|
distribute,
|
||||||
scenarios_for_runner,
|
pairs_for_runner,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -33,47 +36,97 @@ class TestDiscoverScenarios:
|
|||||||
assert Path("ansible/roles/gitea-runner/molecule") == MOLECULE_ROOT
|
assert Path("ansible/roles/gitea-runner/molecule") == MOLECULE_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
class TestPairEncoding:
|
||||||
|
def test_encode_roundtrip(self) -> None:
|
||||||
|
pair = TestPair("default", {"name": "ubuntu-2204", "image": "ubuntu:22.04", "command": ""})
|
||||||
|
encoded = pair.encode()
|
||||||
|
assert encoded == "default|ubuntu-2204|ubuntu:22.04|"
|
||||||
|
decoded = TestPair.decode(encoded)
|
||||||
|
assert decoded.scenario == "default"
|
||||||
|
assert decoded.platform["name"] == "ubuntu-2204"
|
||||||
|
assert decoded.platform["image"] == "ubuntu:22.04"
|
||||||
|
assert decoded.platform["command"] == ""
|
||||||
|
|
||||||
|
def test_encode_with_command(self) -> None:
|
||||||
|
pair = TestPair(
|
||||||
|
"default",
|
||||||
|
{"name": "archlinux", "image": "archlinux:latest", "command": "/usr/lib/systemd/systemd"},
|
||||||
|
)
|
||||||
|
encoded = pair.encode()
|
||||||
|
assert encoded == "default|archlinux|archlinux:latest|/usr/lib/systemd/systemd"
|
||||||
|
decoded = TestPair.decode(encoded)
|
||||||
|
assert decoded.platform["command"] == "/usr/lib/systemd/systemd"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildPairs:
|
||||||
|
def test_cross_product(self) -> None:
|
||||||
|
scenarios = ["a", "b"]
|
||||||
|
platforms = [
|
||||||
|
{"name": "p1", "image": "img1", "command": ""},
|
||||||
|
{"name": "p2", "image": "img2", "command": ""},
|
||||||
|
]
|
||||||
|
pairs = build_pairs(scenarios, platforms)
|
||||||
|
assert len(pairs) == 4
|
||||||
|
assert pairs[0].scenario == "a"
|
||||||
|
assert pairs[0].platform["name"] == "p1"
|
||||||
|
assert pairs[1].scenario == "a"
|
||||||
|
assert pairs[1].platform["name"] == "p2"
|
||||||
|
assert pairs[2].scenario == "b"
|
||||||
|
assert pairs[2].platform["name"] == "p1"
|
||||||
|
assert pairs[3].scenario == "b"
|
||||||
|
assert pairs[3].platform["name"] == "p2"
|
||||||
|
|
||||||
|
def test_default_platforms(self) -> None:
|
||||||
|
pairs = build_pairs(["default"])
|
||||||
|
assert len(pairs) == len(PLATFORMS)
|
||||||
|
assert all(p.scenario == "default" for p in pairs)
|
||||||
|
|
||||||
|
|
||||||
class TestDistribute:
|
class TestDistribute:
|
||||||
def test_even_split(self) -> None:
|
def test_even_split(self) -> None:
|
||||||
scenarios = ["a", "b", "c", "d", "e", "f"]
|
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)]
|
||||||
groups = distribute(scenarios, 3)
|
groups = distribute(pairs, 3)
|
||||||
assert groups == [["a", "d"], ["b", "e"], ["c", "f"]]
|
assert len(groups) == 3
|
||||||
|
assert len(groups[0]) == 2
|
||||||
|
assert len(groups[1]) == 2
|
||||||
|
assert len(groups[2]) == 2
|
||||||
|
|
||||||
def test_uneven_split(self) -> None:
|
def test_uneven_split(self) -> None:
|
||||||
scenarios = ["a", "b", "c", "d", "e"]
|
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(5)]
|
||||||
groups = distribute(scenarios, 3)
|
groups = distribute(pairs, 3)
|
||||||
assert groups == [["a", "d"], ["b", "e"], ["c"]]
|
assert len(groups[0]) == 2
|
||||||
|
assert len(groups[1]) == 2
|
||||||
|
assert len(groups[2]) == 1
|
||||||
|
|
||||||
def test_more_runners_than_scenarios(self) -> None:
|
def test_more_runners_than_pairs(self) -> None:
|
||||||
scenarios = ["a", "b"]
|
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||||
groups = distribute(scenarios, 5)
|
groups = distribute(pairs, 5)
|
||||||
assert groups == [["a"], ["b"], [], [], []]
|
assert len(groups) == 5
|
||||||
|
assert len(groups[0]) == 1
|
||||||
|
assert all(len(g) == 0 for g in groups[1:])
|
||||||
|
|
||||||
def test_single_runner(self) -> None:
|
def test_empty_pairs(self) -> None:
|
||||||
scenarios = ["a", "b", "c"]
|
|
||||||
groups = distribute(scenarios, 1)
|
|
||||||
assert groups == [["a", "b", "c"]]
|
|
||||||
|
|
||||||
def test_empty_scenarios(self) -> None:
|
|
||||||
groups = distribute([], 3)
|
groups = distribute([], 3)
|
||||||
assert groups == [[], [], []]
|
assert groups == [[], [], []]
|
||||||
|
|
||||||
|
|
||||||
class TestScenariosForRunner:
|
class TestPairsForRunner:
|
||||||
def test_returns_correct_subset(self) -> None:
|
def test_returns_correct_subset(self) -> None:
|
||||||
scenarios = ["a", "b", "c", "d", "e", "f"]
|
pairs = [TestPair(f"s{i}", {"name": "p", "image": "i", "command": ""}) for i in range(6)]
|
||||||
assert scenarios_for_runner(scenarios, 0, 3) == ["a", "d"]
|
assert len(pairs_for_runner(pairs, 0, 3)) == 2
|
||||||
assert scenarios_for_runner(scenarios, 1, 3) == ["b", "e"]
|
assert len(pairs_for_runner(pairs, 1, 3)) == 2
|
||||||
assert scenarios_for_runner(scenarios, 2, 3) == ["c", "f"]
|
assert len(pairs_for_runner(pairs, 2, 3)) == 2
|
||||||
|
|
||||||
def test_out_of_range_raises(self) -> None:
|
def test_out_of_range_raises(self) -> None:
|
||||||
|
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||||
with pytest.raises(click.ClickException) as exc:
|
with pytest.raises(click.ClickException) as exc:
|
||||||
scenarios_for_runner(["a"], 5, 3)
|
pairs_for_runner(pairs, 5, 3)
|
||||||
assert "out of range" in str(exc.value)
|
assert "out of range" in str(exc.value)
|
||||||
|
|
||||||
def test_negative_index_raises(self) -> None:
|
def test_negative_index_raises(self) -> None:
|
||||||
|
pairs = [TestPair("a", {"name": "p", "image": "i", "command": ""})]
|
||||||
with pytest.raises(click.ClickException) as exc:
|
with pytest.raises(click.ClickException) as exc:
|
||||||
scenarios_for_runner(["a"], -1, 3)
|
pairs_for_runner(pairs, -1, 3)
|
||||||
assert "out of range" in str(exc.value)
|
assert "out of range" in str(exc.value)
|
||||||
|
|
||||||
|
|
||||||
@@ -93,6 +146,19 @@ class TestCli:
|
|||||||
assert "alpha" in result.output
|
assert "alpha" in result.output
|
||||||
assert "beta" in result.output
|
assert "beta" in result.output
|
||||||
|
|
||||||
|
def test_list_platforms_flag(self) -> None:
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from scripts.distribute_molecule import cli
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(cli, ["--list-platforms"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "ubuntu-2204" in result.output
|
||||||
|
assert "ubuntu-2404" in result.output
|
||||||
|
assert "debian-12" in result.output
|
||||||
|
assert "archlinux" in result.output
|
||||||
|
|
||||||
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
|
def test_no_runner_index_prints_all_groups(self, tmp_path: Path) -> None:
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
@@ -115,13 +181,14 @@ class TestCli:
|
|||||||
from scripts.distribute_molecule import cli
|
from scripts.distribute_molecule import cli
|
||||||
|
|
||||||
root = tmp_path / "molecule"
|
root = tmp_path / "molecule"
|
||||||
for s in ["a", "b", "c"]:
|
(root / "alpha").mkdir(parents=True)
|
||||||
(root / s).mkdir(parents=True)
|
|
||||||
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
with patch("scripts.distribute_molecule.MOLECULE_ROOT", root):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["--runner-index", "1", "--max-runners", "3"])
|
result = runner.invoke(cli, ["--runner-index", "0", "--max-runners", "3"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert result.output.strip() == "b"
|
# Output should contain encoded pairs with platform info
|
||||||
|
assert "alpha|" in result.output
|
||||||
|
assert "ubuntu-2204" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_main_module_block() -> None:
|
def test_main_module_block() -> None:
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class TestRunnerRegistry:
|
|||||||
host="10.0.0.1",
|
host="10.0.0.1",
|
||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
key="/key",
|
key="/key",
|
||||||
mode="docker",
|
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
labels="docker:docker://alpine:latest",
|
labels="docker:docker://alpine:latest",
|
||||||
)
|
)
|
||||||
@@ -31,7 +30,6 @@ class TestRunnerRegistry:
|
|||||||
assert info["host"] == "10.0.0.1"
|
assert info["host"] == "10.0.0.1"
|
||||||
assert info["user"] == "ubuntu"
|
assert info["user"] == "ubuntu"
|
||||||
assert info["key"] == "/key"
|
assert info["key"] == "/key"
|
||||||
assert info["mode"] == "docker"
|
|
||||||
assert info["gitea_url"] == "https://git.example.com"
|
assert info["gitea_url"] == "https://git.example.com"
|
||||||
assert info["labels"] == "docker:docker://alpine:latest"
|
assert info["labels"] == "docker:docker://alpine:latest"
|
||||||
assert "created_at" in info
|
assert "created_at" in info
|
||||||
|
|||||||
@@ -33,14 +33,12 @@ class TestRunnerManager:
|
|||||||
assert "registration_token=tok" in cmd_str
|
assert "registration_token=tok" in cmd_str
|
||||||
assert "runner_name=192.168.1.10" in cmd_str
|
assert "runner_name=192.168.1.10" in cmd_str
|
||||||
assert "gitea_url=https://git.example.com" in cmd_str
|
assert "gitea_url=https://git.example.com" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "Installing Gitea Runner on 192.168.1.10" in mock_executor.run.call_args.kwargs["description"]
|
assert "Installing Gitea Runner on 192.168.1.10" in mock_executor.run.call_args.kwargs["description"]
|
||||||
mock_registry.add.assert_called_once_with(
|
mock_registry.add.assert_called_once_with(
|
||||||
name="192.168.1.10",
|
name="192.168.1.10",
|
||||||
host="192.168.1.10",
|
host="192.168.1.10",
|
||||||
user="ubuntu",
|
user="ubuntu",
|
||||||
key=None,
|
key=None,
|
||||||
mode="docker",
|
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
labels=None,
|
labels=None,
|
||||||
)
|
)
|
||||||
@@ -60,14 +58,12 @@ class TestRunnerManager:
|
|||||||
assert "/key" in cmd_str
|
assert "/key" in cmd_str
|
||||||
assert "registration_token=preset" in cmd_str
|
assert "registration_token=preset" in cmd_str
|
||||||
assert "runner_name=my-runner" in cmd_str
|
assert "runner_name=my-runner" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "--ask-become-pass" not in cmd_str
|
assert "--ask-become-pass" not in cmd_str
|
||||||
mock_registry.add.assert_called_once_with(
|
mock_registry.add.assert_called_once_with(
|
||||||
name="my-runner",
|
name="my-runner",
|
||||||
host="host1",
|
host="host1",
|
||||||
user="root",
|
user="root",
|
||||||
key="/key",
|
key="/key",
|
||||||
mode="docker",
|
|
||||||
gitea_url="https://git.example.com",
|
gitea_url="https://git.example.com",
|
||||||
labels=None,
|
labels=None,
|
||||||
)
|
)
|
||||||
@@ -82,27 +78,6 @@ class TestRunnerManager:
|
|||||||
cmd = mock_executor.run.call_args.args[0]
|
cmd = mock_executor.run.call_args.args[0]
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "--ask-become-pass" in cmd_str
|
assert "--ask-become-pass" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
|
|
||||||
def test_install_binary_mode(self) -> None:
|
|
||||||
mock_registry = MagicMock()
|
|
||||||
manager = RunnerManager(registry=mock_registry)
|
|
||||||
mock_executor = MagicMock()
|
|
||||||
manager._executor = mock_executor
|
|
||||||
|
|
||||||
manager.install("host1", "root", token="tok", gitea_url="https://git.example.com", mode="binary")
|
|
||||||
cmd = mock_executor.run.call_args.args[0]
|
|
||||||
cmd_str = " ".join(cmd)
|
|
||||||
assert "runner_mode=binary" in cmd_str
|
|
||||||
mock_registry.add.assert_called_once_with(
|
|
||||||
name="host1",
|
|
||||||
host="host1",
|
|
||||||
user="root",
|
|
||||||
key=None,
|
|
||||||
mode="binary",
|
|
||||||
gitea_url="https://git.example.com",
|
|
||||||
labels=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_install_missing_gitea_url(self) -> None:
|
def test_install_missing_gitea_url(self) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -181,7 +156,6 @@ class TestRunnerManager:
|
|||||||
assert "--private-key" in cmd_str
|
assert "--private-key" in cmd_str
|
||||||
assert "/key" in cmd_str
|
assert "/key" in cmd_str
|
||||||
assert "gitea_runner_version=v0.2.0" in cmd_str
|
assert "gitea_runner_version=v0.2.0" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "--ask-become-pass" not in cmd_str
|
assert "--ask-become-pass" not in cmd_str
|
||||||
assert "Updating Gitea Runner on host" in mock_executor.run.call_args.kwargs["description"]
|
assert "Updating Gitea Runner on host" in mock_executor.run.call_args.kwargs["description"]
|
||||||
|
|
||||||
@@ -194,17 +168,6 @@ class TestRunnerManager:
|
|||||||
cmd = mock_executor.run.call_args.args[0]
|
cmd = mock_executor.run.call_args.args[0]
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "--ask-become-pass" in cmd_str
|
assert "--ask-become-pass" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
|
|
||||||
def test_update_binary_mode(self) -> None:
|
|
||||||
manager = RunnerManager()
|
|
||||||
mock_executor = MagicMock()
|
|
||||||
manager._executor = mock_executor
|
|
||||||
|
|
||||||
manager.update("host", "user", mode="binary")
|
|
||||||
cmd = mock_executor.run.call_args.args[0]
|
|
||||||
cmd_str = " ".join(cmd)
|
|
||||||
assert "runner_mode=binary" in cmd_str
|
|
||||||
|
|
||||||
def test_update_playbook_not_found(self) -> None:
|
def test_update_playbook_not_found(self) -> None:
|
||||||
manager = RunnerManager()
|
manager = RunnerManager()
|
||||||
@@ -226,15 +189,13 @@ class TestRunnerManager:
|
|||||||
"host": "10.0.0.1",
|
"host": "10.0.0.1",
|
||||||
"user": "ubuntu",
|
"user": "ubuntu",
|
||||||
"key": "/key",
|
"key": "/key",
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "https://git.example.com",
|
"gitea_url": "https://git.example.com",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
host, user, key, mode, gitea_url = manager._resolve_runner("r1")
|
host, user, key, gitea_url = manager._resolve_runner("r1")
|
||||||
assert host == "10.0.0.1"
|
assert host == "10.0.0.1"
|
||||||
assert user == "ubuntu"
|
assert user == "ubuntu"
|
||||||
assert key == "/key"
|
assert key == "/key"
|
||||||
assert mode == "docker"
|
|
||||||
assert gitea_url == "https://git.example.com"
|
assert gitea_url == "https://git.example.com"
|
||||||
mock_registry.get.assert_called_once_with("r1")
|
mock_registry.get.assert_called_once_with("r1")
|
||||||
|
|
||||||
@@ -242,11 +203,10 @@ class TestRunnerManager:
|
|||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = None
|
mock_registry.get.return_value = None
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
host, user, key, mode, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root")
|
host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root")
|
||||||
assert host == "10.0.0.2"
|
assert host == "10.0.0.2"
|
||||||
assert user == "root"
|
assert user == "root"
|
||||||
assert key is None
|
assert key is None
|
||||||
assert mode == "docker"
|
|
||||||
assert gitea_url == ""
|
assert gitea_url == ""
|
||||||
|
|
||||||
def test_resolve_runner_override(self) -> None:
|
def test_resolve_runner_override(self) -> None:
|
||||||
@@ -255,17 +215,13 @@ class TestRunnerManager:
|
|||||||
"host": "10.0.0.1",
|
"host": "10.0.0.1",
|
||||||
"user": "ubuntu",
|
"user": "ubuntu",
|
||||||
"key": "/key",
|
"key": "/key",
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "https://git.example.com",
|
"gitea_url": "https://git.example.com",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
host, user, key, mode, gitea_url = manager._resolve_runner(
|
host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root", key="/new")
|
||||||
"r1", host="10.0.0.2", user="root", key="/new", mode="binary"
|
|
||||||
)
|
|
||||||
assert host == "10.0.0.2"
|
assert host == "10.0.0.2"
|
||||||
assert user == "root"
|
assert user == "root"
|
||||||
assert key == "/new"
|
assert key == "/new"
|
||||||
assert mode == "binary"
|
|
||||||
assert gitea_url == "https://git.example.com"
|
assert gitea_url == "https://git.example.com"
|
||||||
|
|
||||||
def test_resolve_runner_not_found(self) -> None:
|
def test_resolve_runner_not_found(self) -> None:
|
||||||
@@ -277,7 +233,7 @@ class TestRunnerManager:
|
|||||||
|
|
||||||
def test_start(self) -> None:
|
def test_start(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
@@ -287,22 +243,20 @@ class TestRunnerManager:
|
|||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "start-runner.yml" in cmd_str
|
assert "start-runner.yml" in cmd_str
|
||||||
assert "runner_name=r1" in cmd_str
|
assert "runner_name=r1" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "Starting Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
assert "Starting Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||||
|
|
||||||
def test_start_with_override(self) -> None:
|
def test_start_with_override(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
|
|
||||||
manager.start("r1", host="newhost", user="newuser", mode="binary")
|
manager.start("r1", host="newhost", user="newuser")
|
||||||
cmd = mock_executor.run.call_args.args[0]
|
cmd = mock_executor.run.call_args.args[0]
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "newhost," in cmd_str
|
assert "newhost," in cmd_str
|
||||||
assert "newuser" in cmd_str
|
assert "newuser" in cmd_str
|
||||||
assert "runner_mode=binary" in cmd_str
|
|
||||||
|
|
||||||
def test_start_no_registry_with_explicit(self) -> None:
|
def test_start_no_registry_with_explicit(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
@@ -311,14 +265,14 @@ class TestRunnerManager:
|
|||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
|
|
||||||
manager.start("r1", host="host", user="user", mode="docker")
|
manager.start("r1", host="host", user="user")
|
||||||
cmd = mock_executor.run.call_args.args[0]
|
cmd = mock_executor.run.call_args.args[0]
|
||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "start-runner.yml" in cmd_str
|
assert "start-runner.yml" in cmd_str
|
||||||
|
|
||||||
def test_stop(self) -> None:
|
def test_stop(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
@@ -332,7 +286,7 @@ class TestRunnerManager:
|
|||||||
|
|
||||||
def test_enable(self) -> None:
|
def test_enable(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"}
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
@@ -350,7 +304,6 @@ class TestRunnerManager:
|
|||||||
"host": "host",
|
"host": "host",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"key": None,
|
"key": None,
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "",
|
"gitea_url": "",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
@@ -364,7 +317,6 @@ class TestRunnerManager:
|
|||||||
assert "runner_name=r1" in cmd_str
|
assert "runner_name=r1" in cmd_str
|
||||||
assert "registration_token=tok" in cmd_str
|
assert "registration_token=tok" in cmd_str
|
||||||
assert "gitea_url=https://git.example.com" in cmd_str
|
assert "gitea_url=https://git.example.com" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "Disabling Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
assert "Disabling Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||||
|
|
||||||
def test_disable_uses_registry_gitea_url(self) -> None:
|
def test_disable_uses_registry_gitea_url(self) -> None:
|
||||||
@@ -373,7 +325,6 @@ class TestRunnerManager:
|
|||||||
"host": "host",
|
"host": "host",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"key": None,
|
"key": None,
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "https://registry.example.com",
|
"gitea_url": "https://registry.example.com",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
@@ -401,7 +352,7 @@ class TestRunnerManager:
|
|||||||
|
|
||||||
def test_status(self) -> None:
|
def test_status(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "binary"}
|
mock_registry.get.return_value = {"host": "host", "user": "user", "key": None}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
@@ -411,7 +362,6 @@ class TestRunnerManager:
|
|||||||
cmd_str = " ".join(cmd)
|
cmd_str = " ".join(cmd)
|
||||||
assert "status-runner.yml" in cmd_str
|
assert "status-runner.yml" in cmd_str
|
||||||
assert "runner_name=r1" in cmd_str
|
assert "runner_name=r1" in cmd_str
|
||||||
assert "runner_mode=binary" in cmd_str
|
|
||||||
assert "Checking status of Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
assert "Checking status of Gitea Runner r1 on host" in mock_executor.run.call_args.kwargs["description"]
|
||||||
|
|
||||||
def test_remove(self) -> None:
|
def test_remove(self) -> None:
|
||||||
@@ -420,7 +370,6 @@ class TestRunnerManager:
|
|||||||
"host": "host",
|
"host": "host",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"key": None,
|
"key": None,
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "",
|
"gitea_url": "",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
@@ -434,7 +383,6 @@ class TestRunnerManager:
|
|||||||
assert "runner_name=r1" in cmd_str
|
assert "runner_name=r1" in cmd_str
|
||||||
assert "registration_token=tok" in cmd_str
|
assert "registration_token=tok" in cmd_str
|
||||||
assert "gitea_url=https://git.example.com" in cmd_str
|
assert "gitea_url=https://git.example.com" in cmd_str
|
||||||
assert "runner_mode=docker" in cmd_str
|
|
||||||
assert "Removing Gitea Runner r1 from host" in mock_executor.run.call_args.kwargs["description"]
|
assert "Removing Gitea Runner r1 from host" in mock_executor.run.call_args.kwargs["description"]
|
||||||
mock_registry.remove.assert_called_once_with("r1")
|
mock_registry.remove.assert_called_once_with("r1")
|
||||||
|
|
||||||
@@ -470,7 +418,6 @@ class TestRunnerManager:
|
|||||||
"host": "host",
|
"host": "host",
|
||||||
"user": "user",
|
"user": "user",
|
||||||
"key": None,
|
"key": None,
|
||||||
"mode": "docker",
|
|
||||||
"gitea_url": "",
|
"gitea_url": "",
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
@@ -493,14 +440,14 @@ class TestRunnerManager:
|
|||||||
mock_executor.run.assert_not_called()
|
mock_executor.run.assert_not_called()
|
||||||
mock_registry.remove.assert_called_once_with("r1")
|
mock_registry.remove.assert_called_once_with("r1")
|
||||||
|
|
||||||
def test_list_runners_docker(self) -> None:
|
def test_list_runners(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "docker"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key"},
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
mock_executor.run_ad_hoc.return_value = "running"
|
mock_executor.run_ad_hoc.return_value = "active"
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
|
|
||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
@@ -513,44 +460,16 @@ class TestRunnerManager:
|
|||||||
"ubuntu",
|
"ubuntu",
|
||||||
"/key",
|
"/key",
|
||||||
"shell",
|
"shell",
|
||||||
(
|
"sudo -u grm-r1 systemctl --user is-active gitea-runner 2>/dev/null",
|
||||||
"docker inspect gitea-runner-r1 2>/dev/null | "
|
|
||||||
"python3 -c \"import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])\" "
|
|
||||||
"2>/dev/null || echo unknown"
|
|
||||||
),
|
|
||||||
become=True,
|
become=True,
|
||||||
ask_become_pass=True,
|
ask_become_pass=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_runners_binary(self) -> None:
|
def test_list_runners_exception(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "binary"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||||
}
|
|
||||||
manager = RunnerManager(registry=mock_registry)
|
|
||||||
mock_executor = MagicMock()
|
|
||||||
mock_executor.run_ad_hoc.return_value = "active"
|
|
||||||
manager._executor = mock_executor
|
|
||||||
|
|
||||||
runners = manager.list_runners()
|
|
||||||
assert len(runners) == 1
|
|
||||||
assert runners[0]["status"] == "active"
|
|
||||||
mock_executor.run_ad_hoc.assert_called_once_with(
|
|
||||||
"10.0.0.1",
|
|
||||||
"ubuntu",
|
|
||||||
"/key",
|
|
||||||
"shell",
|
|
||||||
"systemctl is-active gitea-runner@r1 2>/dev/null",
|
|
||||||
become=True,
|
|
||||||
ask_become_pass=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_list_runners_binary_exception(self) -> None:
|
|
||||||
mock_registry = MagicMock()
|
|
||||||
mock_registry.list.return_value = {
|
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "binary"},
|
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
@@ -569,7 +488,7 @@ class TestRunnerManager:
|
|||||||
def test_list_runners_without_labels(self) -> None:
|
def test_list_runners_without_labels(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
@@ -579,38 +498,23 @@ class TestRunnerManager:
|
|||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
assert runners[0]["labels"] == ""
|
assert runners[0]["labels"] == ""
|
||||||
|
|
||||||
def test_list_runners_docker_exited(self) -> None:
|
def test_list_runners_inactive(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
mock_executor.run_ad_hoc.return_value = "exited"
|
mock_executor.run_ad_hoc.return_value = "inactive"
|
||||||
manager._executor = mock_executor
|
manager._executor = mock_executor
|
||||||
|
|
||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
assert runners[0]["status"] == "inactive"
|
assert runners[0]["status"] == "inactive"
|
||||||
|
|
||||||
def test_list_runners_docker_fallback_to_host_container(self) -> None:
|
|
||||||
mock_registry = MagicMock()
|
|
||||||
mock_registry.list.return_value = {
|
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
|
||||||
}
|
|
||||||
manager = RunnerManager(registry=mock_registry)
|
|
||||||
mock_executor = MagicMock()
|
|
||||||
# First call (name-based) returns unknown, second (host-based) returns running
|
|
||||||
mock_executor.run_ad_hoc.side_effect = ["unknown", "running"]
|
|
||||||
manager._executor = mock_executor
|
|
||||||
|
|
||||||
runners = manager.list_runners()
|
|
||||||
assert runners[0]["status"] == "active"
|
|
||||||
assert mock_executor.run_ad_hoc.call_count == 2
|
|
||||||
|
|
||||||
def test_list_runners_unknown_status(self) -> None:
|
def test_list_runners_unknown_status(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
@@ -620,25 +524,10 @@ class TestRunnerManager:
|
|||||||
runners = manager.list_runners()
|
runners = manager.list_runners()
|
||||||
assert runners[0]["status"] == "unknown"
|
assert runners[0]["status"] == "unknown"
|
||||||
|
|
||||||
def test_list_runners_docker_first_command_falls_back(self) -> None:
|
|
||||||
"""When the first docker inspect raises, the loop continues to the next fallback."""
|
|
||||||
mock_registry = MagicMock()
|
|
||||||
mock_registry.list.return_value = {
|
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
|
||||||
}
|
|
||||||
manager = RunnerManager(registry=mock_registry)
|
|
||||||
mock_executor = MagicMock()
|
|
||||||
mock_executor.run_ad_hoc.side_effect = [AnsibleError("ssh fail"), "running"]
|
|
||||||
manager._executor = mock_executor
|
|
||||||
|
|
||||||
runners = manager.list_runners()
|
|
||||||
assert runners[0]["status"] == "active"
|
|
||||||
assert mock_executor.run_ad_hoc.call_count == 2
|
|
||||||
|
|
||||||
def test_list_runners_ad_hoc_error(self) -> None:
|
def test_list_runners_ad_hoc_error(self) -> None:
|
||||||
mock_registry = MagicMock()
|
mock_registry = MagicMock()
|
||||||
mock_registry.list.return_value = {
|
mock_registry.list.return_value = {
|
||||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None, "mode": "docker"},
|
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||||
}
|
}
|
||||||
manager = RunnerManager(registry=mock_registry)
|
manager = RunnerManager(registry=mock_registry)
|
||||||
mock_executor = MagicMock()
|
mock_executor = MagicMock()
|
||||||
|
|||||||
Reference in New Issue
Block a user