"""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']}")