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
@@ -33,14 +33,12 @@ class TestRunnerManager:
|
||||
assert "registration_token=tok" in cmd_str
|
||||
assert "runner_name=192.168.1.10" 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"]
|
||||
mock_registry.add.assert_called_once_with(
|
||||
name="192.168.1.10",
|
||||
host="192.168.1.10",
|
||||
user="ubuntu",
|
||||
key=None,
|
||||
mode="docker",
|
||||
gitea_url="https://git.example.com",
|
||||
labels=None,
|
||||
)
|
||||
@@ -60,14 +58,12 @@ class TestRunnerManager:
|
||||
assert "/key" in cmd_str
|
||||
assert "registration_token=preset" 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
|
||||
mock_registry.add.assert_called_once_with(
|
||||
name="my-runner",
|
||||
host="host1",
|
||||
user="root",
|
||||
key="/key",
|
||||
mode="docker",
|
||||
gitea_url="https://git.example.com",
|
||||
labels=None,
|
||||
)
|
||||
@@ -82,27 +78,6 @@ class TestRunnerManager:
|
||||
cmd = mock_executor.run.call_args.args[0]
|
||||
cmd_str = " ".join(cmd)
|
||||
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:
|
||||
manager = RunnerManager()
|
||||
@@ -181,7 +156,6 @@ class TestRunnerManager:
|
||||
assert "--private-key" in cmd_str
|
||||
assert "/key" 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 "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_str = " ".join(cmd)
|
||||
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:
|
||||
manager = RunnerManager()
|
||||
@@ -226,15 +189,13 @@ class TestRunnerManager:
|
||||
"host": "10.0.0.1",
|
||||
"user": "ubuntu",
|
||||
"key": "/key",
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://git.example.com",
|
||||
}
|
||||
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 user == "ubuntu"
|
||||
assert key == "/key"
|
||||
assert mode == "docker"
|
||||
assert gitea_url == "https://git.example.com"
|
||||
mock_registry.get.assert_called_once_with("r1")
|
||||
|
||||
@@ -242,11 +203,10 @@ class TestRunnerManager:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = None
|
||||
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 user == "root"
|
||||
assert key is None
|
||||
assert mode == "docker"
|
||||
assert gitea_url == ""
|
||||
|
||||
def test_resolve_runner_override(self) -> None:
|
||||
@@ -255,17 +215,13 @@ class TestRunnerManager:
|
||||
"host": "10.0.0.1",
|
||||
"user": "ubuntu",
|
||||
"key": "/key",
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://git.example.com",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
host, user, key, mode, gitea_url = manager._resolve_runner(
|
||||
"r1", host="10.0.0.2", user="root", key="/new", mode="binary"
|
||||
)
|
||||
host, user, key, gitea_url = manager._resolve_runner("r1", host="10.0.0.2", user="root", key="/new")
|
||||
assert host == "10.0.0.2"
|
||||
assert user == "root"
|
||||
assert key == "/new"
|
||||
assert mode == "binary"
|
||||
assert gitea_url == "https://git.example.com"
|
||||
|
||||
def test_resolve_runner_not_found(self) -> None:
|
||||
@@ -277,7 +233,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_start(self) -> None:
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -287,22 +243,20 @@ class TestRunnerManager:
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "start-runner.yml" 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"]
|
||||
|
||||
def test_start_with_override(self) -> None:
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
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_str = " ".join(cmd)
|
||||
assert "newhost," in cmd_str
|
||||
assert "newuser" in cmd_str
|
||||
assert "runner_mode=binary" in cmd_str
|
||||
|
||||
def test_start_no_registry_with_explicit(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
@@ -311,14 +265,14 @@ class TestRunnerManager:
|
||||
mock_executor = MagicMock()
|
||||
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_str = " ".join(cmd)
|
||||
assert "start-runner.yml" in cmd_str
|
||||
|
||||
def test_stop(self) -> None:
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -332,7 +286,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_enable(self) -> None:
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -350,7 +304,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -364,7 +317,6 @@ class TestRunnerManager:
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "registration_token=tok" 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"]
|
||||
|
||||
def test_disable_uses_registry_gitea_url(self) -> None:
|
||||
@@ -373,7 +325,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "https://registry.example.com",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -401,7 +352,7 @@ class TestRunnerManager:
|
||||
|
||||
def test_status(self) -> None:
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
manager._executor = mock_executor
|
||||
@@ -411,7 +362,6 @@ class TestRunnerManager:
|
||||
cmd_str = " ".join(cmd)
|
||||
assert "status-runner.yml" 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"]
|
||||
|
||||
def test_remove(self) -> None:
|
||||
@@ -420,7 +370,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -434,7 +383,6 @@ class TestRunnerManager:
|
||||
assert "runner_name=r1" in cmd_str
|
||||
assert "registration_token=tok" 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"]
|
||||
mock_registry.remove.assert_called_once_with("r1")
|
||||
|
||||
@@ -470,7 +418,6 @@ class TestRunnerManager:
|
||||
"host": "host",
|
||||
"user": "user",
|
||||
"key": None,
|
||||
"mode": "docker",
|
||||
"gitea_url": "",
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
@@ -493,14 +440,14 @@ class TestRunnerManager:
|
||||
mock_executor.run.assert_not_called()
|
||||
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.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)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "running"
|
||||
mock_executor.run_ad_hoc.return_value = "active"
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
@@ -513,44 +460,16 @@ class TestRunnerManager:
|
||||
"ubuntu",
|
||||
"/key",
|
||||
"shell",
|
||||
(
|
||||
"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"
|
||||
),
|
||||
"sudo -u grm-r1 systemctl --user is-active gitea-runner 2>/dev/null",
|
||||
become=True,
|
||||
ask_become_pass=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_list_runners_binary(self) -> None:
|
||||
def test_list_runners_exception(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": "/key", "mode": "binary"},
|
||||
}
|
||||
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"},
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
@@ -569,7 +488,7 @@ class TestRunnerManager:
|
||||
def test_list_runners_without_labels(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
@@ -579,38 +498,23 @@ class TestRunnerManager:
|
||||
runners = manager.list_runners()
|
||||
assert runners[0]["labels"] == ""
|
||||
|
||||
def test_list_runners_docker_exited(self) -> None:
|
||||
def test_list_runners_inactive(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "exited"
|
||||
mock_executor.run_ad_hoc.return_value = "inactive"
|
||||
manager._executor = mock_executor
|
||||
|
||||
runners = manager.list_runners()
|
||||
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:
|
||||
mock_registry = MagicMock()
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
@@ -620,25 +524,10 @@ class TestRunnerManager:
|
||||
runners = manager.list_runners()
|
||||
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:
|
||||
mock_registry = MagicMock()
|
||||
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)
|
||||
mock_executor = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user