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>
342 lines
8.9 KiB
Python
342 lines
8.9 KiB
Python
"""Click CLI for Gitea Runner Manager."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import os
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
import click
|
|
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
|
|
|
from . import __version__
|
|
from .exceptions import GRMError
|
|
from .i18n import _
|
|
from .runner_manager import RunnerManager
|
|
|
|
load_dotenv(override=True)
|
|
|
|
|
|
def _runner_options(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
"""Apply common override options for registry-based lifecycle commands."""
|
|
func = click.option(
|
|
"--ask-become-pass/--no-ask-become-pass",
|
|
default=True,
|
|
help=_("Prompt for sudo password (default)"),
|
|
)(func)
|
|
func = click.option("--key", "-k", help=_("Override SSH key from registry"))(func)
|
|
func = click.option("--user", "-u", help=_("Override user from registry"))(func)
|
|
func = click.option("--host", help=_("Override host from registry"))(func)
|
|
return func
|
|
|
|
|
|
def _handle_errors(msg_key: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
"""Convert GRMError into a Click exception with a translated message."""
|
|
|
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@functools.wraps(func)
|
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except GRMError as e:
|
|
raise click.ClickException(_(msg_key, error=e)) from e
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
@click.group(help=_("Gitea Runner Manager — manage Gitea Actions runners."))
|
|
@click.version_option(version=__version__)
|
|
def cli() -> None:
|
|
pass
|
|
|
|
|
|
@cli.command(help=_("Install and configure a Gitea Runner on a remote host."))
|
|
@click.argument("host")
|
|
@click.option(
|
|
"--user",
|
|
"-u",
|
|
default=lambda: os.getenv("GITEA_RUNNER_USER", os.getlogin()),
|
|
help=_("SSH user"),
|
|
)
|
|
@click.option("--key", "-k", default=lambda: os.getenv("GITEA_RUNNER_KEY"), help=_("Path to SSH private key"))
|
|
@click.option("--name", "-n", help=_("Gitea Runner name (default: host)"))
|
|
@click.option(
|
|
"--token",
|
|
"-t",
|
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
|
)
|
|
@click.option(
|
|
"--url",
|
|
default=lambda: os.getenv("GITEA_URL", ""),
|
|
help=_("Gitea URL (env: GITEA_URL)"),
|
|
)
|
|
@click.option(
|
|
"--admin-token",
|
|
"-a",
|
|
default=lambda: os.getenv("REPO_TOKEN"),
|
|
help=_("Gitea admin API token for integration test (env: REPO_TOKEN)"),
|
|
)
|
|
@click.option(
|
|
"--integration-retries",
|
|
"-r",
|
|
type=int,
|
|
default=lambda: int(os.getenv("GITEA_INTEGRATION_RETRIES", "3")),
|
|
help=_("Integration test API retries (default: 3, env: GITEA_INTEGRATION_RETRIES)"),
|
|
)
|
|
@click.option(
|
|
"--labels",
|
|
"-l",
|
|
default=lambda: os.getenv("GITEA_RUNNER_LABELS", ""),
|
|
help=_("Runner labels for Gitea Actions (env: GITEA_RUNNER_LABELS). Example: docker:docker://alpine:latest"),
|
|
)
|
|
@click.option(
|
|
"--ask-become-pass/--no-ask-become-pass",
|
|
default=True,
|
|
help=_("Prompt for sudo password (default)"),
|
|
)
|
|
def install(
|
|
host: str,
|
|
user: str,
|
|
key: str | None,
|
|
name: str | None,
|
|
token: str | None,
|
|
url: str,
|
|
admin_token: str | None,
|
|
integration_retries: int,
|
|
labels: str,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
try:
|
|
manager.install(
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
name=name,
|
|
token=token,
|
|
gitea_url=url,
|
|
admin_token=admin_token,
|
|
integration_retries=integration_retries,
|
|
labels=labels or None,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
except GRMError as e:
|
|
raise click.ClickException(_("Installation failed: {error}", error=e)) from e
|
|
|
|
|
|
@cli.command(help=_("Update the Gitea Runner binary on a remote host."))
|
|
@click.argument("host")
|
|
@click.option(
|
|
"--user",
|
|
"-u",
|
|
default=lambda: os.getenv("GITEA_RUNNER_USER", os.getlogin()),
|
|
help=_("SSH user"),
|
|
)
|
|
@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(
|
|
"--ask-become-pass/--no-ask-become-pass",
|
|
default=True,
|
|
help=_("Prompt for sudo password (default)"),
|
|
)
|
|
@_handle_errors("Update failed: {error}")
|
|
def update(
|
|
host: str,
|
|
user: str,
|
|
key: str | None,
|
|
version: str | None,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.update(
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
version=version,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Start a registered Gitea Runner."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@_handle_errors("Start failed: {error}")
|
|
def start(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.start(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Stop a registered Gitea Runner."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@_handle_errors("Stop failed: {error}")
|
|
def stop(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.stop(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Enable a registered Gitea Runner to start on boot."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@_handle_errors("Enable failed: {error}")
|
|
def enable(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.enable(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Disable a registered Gitea Runner and deregister it."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@click.option(
|
|
"--token",
|
|
"-t",
|
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
|
)
|
|
@click.option(
|
|
"--url",
|
|
default=lambda: os.getenv("GITEA_URL", ""),
|
|
help=_("Gitea URL (env: GITEA_URL)"),
|
|
)
|
|
@_handle_errors("Disable failed: {error}")
|
|
def disable(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
token: str | None,
|
|
url: str,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.disable(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
token=token,
|
|
gitea_url=url,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Check the status of a registered Gitea Runner."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@_handle_errors("Status check failed: {error}")
|
|
def status(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.status(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
ask_become_pass=ask_become_pass,
|
|
)
|
|
|
|
|
|
@cli.command(help=_("Remove a registered Gitea Runner completely."))
|
|
@click.argument("runner_name")
|
|
@_runner_options
|
|
@click.option(
|
|
"--token",
|
|
"-t",
|
|
default=lambda: os.getenv("GITEA_REGISTRATION_TOKEN"),
|
|
help=_("Registration token (env: GITEA_REGISTRATION_TOKEN)"),
|
|
)
|
|
@click.option(
|
|
"--url",
|
|
default=lambda: os.getenv("GITEA_URL", ""),
|
|
help=_("Gitea URL (env: GITEA_URL)"),
|
|
)
|
|
@click.option(
|
|
"--force",
|
|
"-f",
|
|
is_flag=True,
|
|
help=_("Skip remote cleanup and only remove the local registry entry"),
|
|
)
|
|
@_handle_errors("Remove failed: {error}")
|
|
def remove(
|
|
runner_name: str,
|
|
host: str | None,
|
|
user: str | None,
|
|
key: str | None,
|
|
token: str | None,
|
|
url: str,
|
|
force: bool,
|
|
ask_become_pass: bool,
|
|
) -> None:
|
|
manager = RunnerManager()
|
|
manager.remove(
|
|
name=runner_name,
|
|
host=host,
|
|
user=user,
|
|
key=key,
|
|
token=token,
|
|
gitea_url=url,
|
|
ask_become_pass=ask_become_pass,
|
|
force=force,
|
|
)
|
|
|
|
|
|
@cli.command(name="list", help=_("List all registered runners with live status."))
|
|
@_handle_errors("List failed: {error}")
|
|
def list_runners() -> None:
|
|
manager = RunnerManager()
|
|
runners = manager.list_runners()
|
|
|
|
if not runners:
|
|
click.echo(_("No runners registered. Use 'grm install' to add one."))
|
|
return
|
|
|
|
click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('USER'):<10} {_('LABELS'):<30} {_('STATUS')}")
|
|
click.echo("-" * 90)
|
|
for r in runners:
|
|
click.echo(f"{r['name']:<18} {r['host']:<16} {r['user']:<10} {r['labels']:<30} {r['status']}")
|