diff --git a/src/gitea_runner_manager/cli.py b/src/gitea_runner_manager/cli.py index 5d8627b..2fe8231 100644 --- a/src/gitea_runner_manager/cli.py +++ b/src/gitea_runner_manager/cli.py @@ -306,6 +306,12 @@ def status( type=click.Choice(["docker", "binary"]), help=_("Override deployment mode from registry"), ) +@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, @@ -314,6 +320,7 @@ def remove( key: str | None, token: str | None, mode: str | None, + force: bool, ask_become_pass: bool, ) -> None: manager = RunnerManager() @@ -326,6 +333,7 @@ def remove( gitea_url=os.getenv("GITEA_URL", ""), mode=mode, ask_become_pass=ask_become_pass, + force=force, ) diff --git a/src/gitea_runner_manager/i18n.py b/src/gitea_runner_manager/i18n.py index 1275458..51b11df 100644 --- a/src/gitea_runner_manager/i18n.py +++ b/src/gitea_runner_manager/i18n.py @@ -467,6 +467,13 @@ TRANSLATIONS: dict[str, dict[str, str]] = { "ru": "Удалить runner '{name}' из локального реестра", "zh": "从本地注册表移除 runner '{name}'", }, + "Skip remote cleanup and only remove the local registry entry": { + "en": "Skip remote cleanup and only remove the local registry entry", + "bg": "Пропуснете отдалеченото почистване и премахнете само локалния запис", + "de": "Remote-Bereinigung überspringen und nur den lokalen Registrierungseintrag entfernen", + "ru": "Пропустить удаленную очистку и удалить только локальную запись реестра", + "zh": "跳过远程清理,仅删除本地注册表条目", + }, } diff --git a/src/gitea_runner_manager/runner_manager.py b/src/gitea_runner_manager/runner_manager.py index 4b71fbc..4873d8a 100644 --- a/src/gitea_runner_manager/runner_manager.py +++ b/src/gitea_runner_manager/runner_manager.py @@ -240,8 +240,14 @@ class RunnerManager: gitea_url: str = "", mode: str | None = None, ask_become_pass: bool = False, + force: bool = False, ) -> None: - """Remove a runner instance completely.""" + """Remove a runner instance completely. + + When *force* is ``True``, skip the remote Ansible playbook and + only remove the local registry entry. Use this when the remote + host is already gone or unreachable. + """ if not gitea_url: raise AnsibleError(_("GITEA_URL must be set (or pass --url)")) actual_host, actual_user, actual_key, actual_mode = self._resolve_runner(name, host, user, key, mode) @@ -249,15 +255,16 @@ class RunnerManager: raise AnsibleError(_("GITEA_REGISTRATION_TOKEN must be set (or pass --token)")) with track_steps() as tracker: tracker.begin(_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host)) - extra_vars = ( - f"runner_name={name} registration_token={token} gitea_url={gitea_url} runner_mode={actual_mode}" - ) - cmd = self._build_cmd( - "remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass - ) - self._executor.run( - cmd, description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host) - ) + if not force: + extra_vars = ( + f"runner_name={name} registration_token={token} gitea_url={gitea_url} runner_mode={actual_mode}" + ) + cmd = self._build_cmd( + "remove-runner.yml", actual_host, actual_user, extra_vars, actual_key, ask_become_pass + ) + self._executor.run( + cmd, description=_("Removing Gitea Runner {name} from {host}", name=name, host=actual_host) + ) tracker.done() tracker.begin(_("Remove runner '{name}' from local registry", name=name)) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 6930c0c..63f4cdb 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -375,6 +375,27 @@ class TestCLI: gitea_url="https://git.example.com", mode=None, ask_become_pass=False, + force=False, + ) + + @patch("gitea_runner_manager.cli.RunnerManager") + def test_remove_force_flag(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"}) + result = runner.invoke(cli, ["remove", "r1", "--token", "tok", "--force"]) + assert result.exit_code == 0 + mock_manager.remove.assert_called_once_with( + name="r1", + host=None, + user=None, + key=None, + token="tok", + gitea_url="https://git.example.com", + mode=None, + ask_become_pass=False, + force=True, ) @patch("gitea_runner_manager.cli.RunnerManager") diff --git a/tests/unit/test_runner_manager.py b/tests/unit/test_runner_manager.py index 2ac69c5..7743c5e 100644 --- a/tests/unit/test_runner_manager.py +++ b/tests/unit/test_runner_manager.py @@ -381,6 +381,17 @@ class TestRunnerManager: manager.remove("r1", token="tok") mock_registry.remove.assert_not_called() + def test_remove_force_skips_playbook(self) -> None: + mock_registry = MagicMock() + mock_registry.get.return_value = {"host": "host", "user": "user", "key": None, "mode": "docker"} + manager = RunnerManager(registry=mock_registry) + mock_executor = MagicMock() + manager._executor = mock_executor + + manager.remove("r1", token="tok", gitea_url="https://git.example.com", force=True) + mock_executor.run.assert_not_called() + mock_registry.remove.assert_called_once_with("r1") + def test_list_runners(self) -> None: mock_registry = MagicMock() mock_registry.list.return_value = {