refactor(git-gate): make GitGate a service class in a git_gate package
test / integration-docker (pull_request) Successful in 15s
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / unit (pull_request) Successful in 47s
lint / lint (push) Failing after 58s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 20s
test / publish-infra (pull_request) Has been skipped

Turn the git_gate module into a package with GitGate as a concrete service class
(dropping the ABC), mirroring the Supervisor shape. The host-side git-gate
operations are now methods the backend drives:

  git_gate/
    __init__.py  — thin __getattr__ facade (keeps `from bot_bottle.git_gate
                   import …` working; render/hook names lazily forwarded to
                   gateway.git_gate_render)
    plan.py      — GitGatePlan (the launch DTO the backend contract references)
    service.py   — GitGate: prepare / provision_dynamic_keys /
                   revoke_provisioned_keys / preflight_host_keys
    provision.py — deploy-key lifecycle (was git_gate_provision.py)
    host_key.py  — host-key preflight (was git_gate_host_key.py)

The backend launch + base.py preflight now call GitGate() methods instead of the
free functions. The runtime rendering / in-gateway hook execution stay in
gateway/git_gate_render.py (data plane) — only the host-side service moved.

Because the facade forwards names lazily, the ~25 `from bot_bottle.git_gate
import GitGatePlan` call-sites are unchanged, and importing git_gate.plan (the
contract's dependency) no longer drags in the provisioning / forge-API code.

Full unit suite green (2243).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 16:27:56 -04:00
parent 14c28946a7
commit 450037b7e9
13 changed files with 295 additions and 225 deletions
+3 -3
View File
@@ -385,13 +385,13 @@ class TestDynamicKeyProvisioning(unittest.TestCase):
def test_resolve_identity_file_gitea_provisions_key(self):
entry = self._gitea_manifest().bottles["dev"].git[0]
with patch("bot_bottle.git_gate_provision._provision_dynamic_key", return_value="/tmp/provisioned-key") as mock_provision:
with patch("bot_bottle.git_gate.provision._provision_dynamic_key", return_value="/tmp/provisioned-key") as mock_provision:
self.assertEqual("/tmp/provisioned-key", _resolve_identity_file(entry, "demo", self.stage))
mock_provision.assert_called_once()
def test_prepare_defers_gitea_key_provisioning(self):
bottle = self._gitea_manifest().bottles["dev"]
with patch("bot_bottle.git_gate_provision._provision_dynamic_key") as mock_provision:
with patch("bot_bottle.git_gate.provision._provision_dynamic_key") as mock_provision:
plan = _StubGate().prepare(bottle, "demo", self.stage)
mock_provision.assert_not_called()
@@ -402,7 +402,7 @@ class TestDynamicKeyProvisioning(unittest.TestCase):
plan = _StubGate().prepare(bottle, "demo", self.stage)
with patch(
"bot_bottle.git_gate_provision._provision_dynamic_key",
"bot_bottle.git_gate.provision._provision_dynamic_key",
return_value="/tmp/provisioned-key",
) as mock_provision:
updated = provision_git_gate_dynamic_keys(bottle, plan, self.stage)
+19 -19
View File
@@ -7,7 +7,7 @@ import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from bot_bottle.git_gate_host_key import (
from bot_bottle.git_gate.host_key import (
add_host_key_to_frontmatter,
find_and_update_bottle_file,
find_repo_bottle_file,
@@ -358,7 +358,7 @@ git-gate:
with tempfile.TemporaryDirectory() as d:
path = self._write_bottle(d, "dev", content)
with patch(
"bot_bottle.git_gate_host_key.find_repo_bottle_file",
"bot_bottle.git_gate.host_key.find_repo_bottle_file",
return_value=path,
), patch.object(Path, "read_text", side_effect=OSError("Permission denied")):
result = find_and_update_bottle_file(Path(d), "myrepo", "ssh-ed25519 AAAA")
@@ -382,7 +382,7 @@ git-gate:
with tempfile.TemporaryDirectory() as d:
path = self._write_bottle(d, "dev", content)
with patch(
"bot_bottle.git_gate_host_key.add_host_key_to_frontmatter",
"bot_bottle.git_gate.host_key.add_host_key_to_frontmatter",
return_value=content, # no change
):
result = find_and_update_bottle_file(Path(d), "myrepo", "ssh-ed25519 AAAA")
@@ -467,10 +467,10 @@ git-gate:
mock_prompt = MagicMock(side_effect=["y", "n"]) # confirm key, skip save
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty", mock_prompt,
"bot_bottle.git_gate.host_key.prompt_tty", mock_prompt,
), patch("sys.stderr"):
preflight_host_keys(manifest, headless=False, home_md=Path(home))
@@ -520,10 +520,10 @@ class TestPreflightHostKeys(unittest.TestCase):
manifest = _manifest_with_git()
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
side_effect=["y", "n"], # confirm key, don't persist
), patch("sys.stderr"):
result = preflight_host_keys(manifest, headless=False, home_md=None)
@@ -534,10 +534,10 @@ class TestPreflightHostKeys(unittest.TestCase):
manifest = _manifest_with_git()
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
return_value="n",
), patch("sys.stderr"), self.assertRaises(SystemExit):
preflight_host_keys(manifest, headless=False, home_md=None)
@@ -557,10 +557,10 @@ class TestPreflightHostKeys(unittest.TestCase):
)
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
side_effect=["y", "y"], # confirm key, yes persist
), patch("sys.stderr"):
result = preflight_host_keys(
@@ -586,10 +586,10 @@ class TestPreflightHostKeys(unittest.TestCase):
)
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
side_effect=["y", "n"], # confirm key, no persist
), patch("sys.stderr"):
result = preflight_host_keys(
@@ -604,7 +604,7 @@ class TestPreflightHostKeys(unittest.TestCase):
manifest = _manifest_with_git()
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
side_effect=RuntimeError("connection refused"),
), patch("sys.stderr"), self.assertRaises(SystemExit):
preflight_host_keys(manifest, headless=False, home_md=None)
@@ -620,10 +620,10 @@ class TestPreflightHostKeys(unittest.TestCase):
import io
buf = io.StringIO()
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
side_effect=["y"], # confirm key only; no save prompt when no file found
), patch("sys.stderr", buf):
result = preflight_host_keys(
@@ -651,13 +651,13 @@ class TestPreflightHostKeys(unittest.TestCase):
import io
buf = io.StringIO()
with patch(
"bot_bottle.git_gate_host_key.fetch_host_key",
"bot_bottle.git_gate.host_key.fetch_host_key",
return_value="ssh-ed25519 FETCHED",
), patch(
"bot_bottle.git_gate_host_key.prompt_tty",
"bot_bottle.git_gate.host_key.prompt_tty",
side_effect=["y", "y"], # confirm key, yes persist
), patch(
"bot_bottle.git_gate_host_key.find_and_update_bottle_file",
"bot_bottle.git_gate.host_key.find_and_update_bottle_file",
return_value=False,
), patch("sys.stderr", buf):
result = preflight_host_keys(