Compare commits

...

2 Commits

Author SHA1 Message Date
didericis-claude c44a010464 refactor(git-gate): centralize hostname qualification in globalize_slug
lint / lint (push) Successful in 2m18s
test / unit (pull_request) Successful in 1m21s
test / integration (pull_request) Successful in 31s
test / coverage (pull_request) Successful in 1m25s
Adds globalize_slug(slug) to bottle_state alongside bottle_identity.
git_gate_provision now calls globalize_slug(slug) instead of inlining
socket.gethostname(), so the hostname-qualification logic has a single,
named home. Assumes slug is a mint_slug output.

Title format changes from bot-bottle:{host}:{slug}:{name}
to bot-bottle:{host}-{slug}:{name} to match the globalize_slug contract.
2026-07-17 03:05:32 +00:00
didericis-claude 0060f5229e feat(git-gate): include hostname in deploy key title
lint / lint (push) Successful in 2m27s
test / unit (pull_request) Successful in 1m19s
test / integration (pull_request) Successful in 31s
test / coverage (pull_request) Successful in 1m26s
Closes #388 (part 1 of 3). Deploy key titles now carry the machine
hostname so keys provisioned on different hosts don't collide with
each other on the forge when a prior bottle was never torn down.

Title format: bot-bottle:<hostname>:<slug>:<repo-name>
2026-07-17 02:27:11 +00:00
3 changed files with 16 additions and 3 deletions
+10
View File
@@ -31,6 +31,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import json import json
import secrets import secrets
import socket
import string import string
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -87,6 +88,14 @@ def bottle_identity(agent_name: str) -> str:
return f"{slug}-{suffix}" return f"{slug}-{suffix}"
def globalize_slug(slug: str) -> str:
"""Return a globally-unique slug qualified with the current hostname.
Assumes slug is a value returned from mint_slug. Use wherever a slug
must be unique across hosts (e.g. deploy-key titles)."""
return f"{socket.gethostname()}-{slug}"
@dataclass(frozen=True) @dataclass(frozen=True)
class BottleMetadata: class BottleMetadata:
"""Persistent record of how a bottle was launched, written at """Persistent record of how a bottle was launched, written at
@@ -340,6 +349,7 @@ __all__ = [
"BottleMetadata", "BottleMetadata",
"agent_state_dir", "agent_state_dir",
"bottle_identity", "bottle_identity",
"globalize_slug",
"bottle_state_dir", "bottle_state_dir",
"cleanup_state", "cleanup_state",
"clear_preserve_marker", "clear_preserve_marker",
+2 -1
View File
@@ -13,6 +13,7 @@ import dataclasses
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .bottle_state import globalize_slug
from .errors import MissingEnvVarError from .errors import MissingEnvVarError
from .log import info from .log import info
from .manifest import ManifestBottle, ManifestGitEntry from .manifest import ManifestBottle, ManifestGitEntry
@@ -46,7 +47,7 @@ def _provision_dynamic_key(
owner_repo = entry.UpstreamPath owner_repo = entry.UpstreamPath
if owner_repo.endswith(".git"): if owner_repo.endswith(".git"):
owner_repo = owner_repo[:-4] owner_repo = owner_repo[:-4]
title = f"bot-bottle:{slug}:{entry.Name}" title = f"bot-bottle:{globalize_slug(slug)}:{entry.Name}"
info(f"provisioning deploy key for git-gate.repos[{entry.Name!r}]") info(f"provisioning deploy key for git-gate.repos[{entry.Name!r}]")
key_id, private_key_bytes = provisioner.create(owner_repo, title) key_id, private_key_bytes = provisioner.create(owner_repo, title)
+4 -2
View File
@@ -6,6 +6,7 @@ Covers the pure `git_gate_render_gitconfig` renderer and the dynamic
from __future__ import annotations from __future__ import annotations
import socket
import tempfile import tempfile
import types import types
import unittest import unittest
@@ -113,8 +114,9 @@ class TestProvisionDynamicKey(unittest.TestCase):
self.assertEqual(b"PRIVATE-KEY-BYTES", key_file.read_bytes()) self.assertEqual(b"PRIVATE-KEY-BYTES", key_file.read_bytes())
id_file = Path(d) / "repo-deploy-key-id" id_file = Path(d) / "repo-deploy-key-id"
self.assertEqual("kid123", id_file.read_text()) self.assertEqual("kid123", id_file.read_text())
# owner_repo had .git stripped; title carries slug + name # owner_repo had .git stripped; title carries globalize_slug(slug) + name
self.assertEqual([("o/r", "bot-bottle:myslug:repo")], fake.created) hostname = socket.gethostname()
self.assertEqual([("o/r", f"bot-bottle:{hostname}-myslug:repo")], fake.created)
def test_missing_token_raises(self) -> None: def test_missing_token_raises(self) -> None:
with tempfile.TemporaryDirectory() as d, \ with tempfile.TemporaryDirectory() as d, \