refactor: unify identity/provisioned_key into key block
Replace the two mutually-exclusive repo keys (identity and provisioned_key) with a single required key block. key.provider is "static" (path to host SSH key) or "gitea" (deploy-key lifecycle via provisioner_token env var, replacing token_env). Internal fields: ManifestProvisionedKeyConfig → ManifestKeyConfig; ProvisionedKey field removed from ManifestGitEntry; Key field added. git_gate.py checks entry.Key.provider == "gitea" instead of entry.ProvisionedKey is not None.
This commit is contained in:
+2
-2
@@ -46,12 +46,12 @@ def fixture_with_git_dict() -> dict[str, Any]:
|
||||
"repos": {
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
"host_key": "ssh-ed25519 AAAA...",
|
||||
},
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/didericis/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
"host_key": "ssh-ed25519 BBBB...",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ def _manifest(*, supervise: bool, with_git: bool, with_egress: bool) -> Manifest
|
||||
bottle["git-gate"] = {"repos": {
|
||||
"upstream": {
|
||||
"url": "ssh://git@example.com:22/x/y.git",
|
||||
"identity": "/etc/hostname", # any existing file
|
||||
"key": {"provider": "static", "path": "/etc/hostname"},
|
||||
},
|
||||
}}
|
||||
if with_egress:
|
||||
|
||||
@@ -284,7 +284,7 @@ class TestPrepare(unittest.TestCase):
|
||||
"bottles": {"dev": {"git-gate": {"repos": {
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/didericis/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}}}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
|
||||
@@ -112,7 +112,7 @@ class TestAgentGitUserOverlay(unittest.TestCase):
|
||||
class TestAgentGitUserRejections(unittest.TestCase):
|
||||
def test_agent_repos_dies_bottle_only(self):
|
||||
msg = _error_message(_manifest, agent_git={
|
||||
"repos": {"r": {"url": "ssh://git@x/y.git", "identity": "/dev/null"}},
|
||||
"repos": {"r": {"url": "ssh://git@x/y.git", "key": {"provider": "static", "path": "/dev/null"}}},
|
||||
})
|
||||
self.assertIn("git-gate.repos", msg)
|
||||
self.assertIn("bottle-only", msg)
|
||||
|
||||
@@ -116,8 +116,8 @@ class TestExtendsGitMerge(unittest.TestCase):
|
||||
"""git-gate.user overlays by field; git-gate.repos merges by upstream
|
||||
host, with child entries replacing duplicate hosts."""
|
||||
|
||||
_GIT_ENTRY_A = {"url": "ssh://git@host-a/a.git", "identity": "/dev/null"}
|
||||
_GIT_ENTRY_B = {"url": "ssh://git@host-b/b.git", "identity": "/dev/null"}
|
||||
_GIT_ENTRY_A = {"url": "ssh://git@host-a/a.git", "key": {"provider": "static", "path": "/dev/null"}}
|
||||
_GIT_ENTRY_B = {"url": "ssh://git@host-b/b.git", "key": {"provider": "static", "path": "/dev/null"}}
|
||||
|
||||
def test_child_git_repos_merge_with_parent(self):
|
||||
m = _build(
|
||||
@@ -131,7 +131,7 @@ class TestExtendsGitMerge(unittest.TestCase):
|
||||
self.assertEqual(["a", "b"], names)
|
||||
|
||||
def test_child_git_repo_replaces_same_host(self):
|
||||
replacement = {"url": "ssh://git@host-a/replacement.git", "identity": "/dev/null"}
|
||||
replacement = {"url": "ssh://git@host-a/replacement.git", "key": {"provider": "static", "path": "/dev/null"}}
|
||||
m = _build(
|
||||
base={"git-gate": {"repos": {"a": self._GIT_ENTRY_A}}},
|
||||
child={
|
||||
|
||||
+109
-79
@@ -17,7 +17,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
entries = m.bottles["dev"].git
|
||||
@@ -33,7 +33,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/didericis/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
e = m.bottles["dev"].git[0]
|
||||
@@ -44,7 +44,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("", m.bottles["dev"].git[0].KnownHostKey)
|
||||
@@ -53,7 +53,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
"host_key": "ssh-ed25519 AAAA",
|
||||
},
|
||||
}))
|
||||
@@ -63,7 +63,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"my-repo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("my-repo", m.bottles["dev"].git[0].Name)
|
||||
@@ -71,10 +71,10 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
def test_missing_url_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {"identity": "/dev/null"},
|
||||
"foo": {"key": {"provider": "static", "path": "/dev/null"}},
|
||||
}))
|
||||
|
||||
def test_missing_identity_dies(self):
|
||||
def test_missing_key_block_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {"url": "ssh://git@github.com/foo.git"},
|
||||
@@ -85,7 +85,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
"IdentityFile": "/dev/null", # old PascalCase key
|
||||
},
|
||||
}))
|
||||
@@ -95,7 +95,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "https://github.com/didericis/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -104,7 +104,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "git@github.com:didericis/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -113,7 +113,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -122,7 +122,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -131,7 +131,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com:notaport/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -139,7 +139,7 @@ class TestGitEntryParsing(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@100.78.141.42:30009/didericis/bot-bottle.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
e = m.bottles["dev"].git[0]
|
||||
@@ -156,11 +156,11 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
"bottles": {"dev": {"git-gate": {"repos": {
|
||||
"foo": {
|
||||
"url": "ssh://git@a.example/x.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
"bar": {
|
||||
"url": "ssh://git@b.example/y.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}}}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
@@ -190,7 +190,7 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"o'reilly": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -199,7 +199,7 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"my repo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -208,7 +208,7 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo;bar": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -217,7 +217,7 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo$bar": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -225,7 +225,7 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"my.repo-name_1": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("my.repo-name_1", m.bottles["dev"].git[0].Name)
|
||||
@@ -243,111 +243,141 @@ class TestGitEntryCrossValidation(unittest.TestCase):
|
||||
self.assertIn("PRD 0047", msg)
|
||||
|
||||
|
||||
class TestProvisionedKey(unittest.TestCase):
|
||||
"""git-gate.repos entries that use provisioned_key (PRD 0048)."""
|
||||
class TestStaticKey(unittest.TestCase):
|
||||
"""git-gate.repos entries with key.provider = "static"."""
|
||||
|
||||
def test_provisioned_key_minimal(self):
|
||||
def test_static_key_minimal(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"provisioned_key": {
|
||||
"key": {"provider": "static", "path": "/home/user/.ssh/id_ed25519"},
|
||||
},
|
||||
}))
|
||||
e = m.bottles["dev"].git[0]
|
||||
self.assertEqual("bot-bottle", e.Name)
|
||||
self.assertEqual("static", e.Key.provider)
|
||||
self.assertEqual("/home/user/.ssh/id_ed25519", e.Key.path)
|
||||
self.assertEqual("/home/user/.ssh/id_ed25519", e.IdentityFile)
|
||||
|
||||
def test_static_key_sets_identity_file_at_parse_time(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("/dev/null", m.bottles["dev"].git[0].IdentityFile)
|
||||
|
||||
def test_static_key_missing_path_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"key": {"provider": "static"},
|
||||
},
|
||||
}))
|
||||
|
||||
def test_static_key_unknown_field_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"key": {"provider": "static", "path": "/dev/null", "api_url": "x"},
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
class TestGiteaKey(unittest.TestCase):
|
||||
"""git-gate.repos entries with key.provider = "gitea"."""
|
||||
|
||||
def test_gitea_key_minimal(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"key": {
|
||||
"provider": "gitea",
|
||||
"token_env": "GITEA_TOKEN",
|
||||
"provisioner_token": "GITEA_TOKEN",
|
||||
},
|
||||
},
|
||||
}))
|
||||
e = m.bottles["dev"].git[0]
|
||||
self.assertEqual("bot-bottle", e.Name)
|
||||
self.assertIsNotNone(e.ProvisionedKey)
|
||||
assert e.ProvisionedKey is not None
|
||||
self.assertEqual("gitea", e.ProvisionedKey.provider)
|
||||
self.assertEqual("GITEA_TOKEN", e.ProvisionedKey.token_env)
|
||||
self.assertEqual("", e.ProvisionedKey.api_url)
|
||||
self.assertEqual("gitea", e.Key.provider)
|
||||
self.assertEqual("GITEA_TOKEN", e.Key.provisioner_token)
|
||||
self.assertEqual("", e.Key.api_url)
|
||||
self.assertEqual("", e.IdentityFile)
|
||||
|
||||
def test_provisioned_key_with_api_url(self):
|
||||
def test_gitea_key_with_api_url(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"repo": {
|
||||
"url": "ssh://git@gitea.example.com/org/repo.git",
|
||||
"provisioned_key": {
|
||||
"key": {
|
||||
"provider": "gitea",
|
||||
"token_env": "MY_TOKEN",
|
||||
"provisioner_token": "MY_TOKEN",
|
||||
"api_url": "https://gitea.example.com",
|
||||
},
|
||||
},
|
||||
}))
|
||||
pk = m.bottles["dev"].git[0].ProvisionedKey
|
||||
assert pk is not None
|
||||
self.assertEqual("https://gitea.example.com", pk.api_url)
|
||||
self.assertEqual("https://gitea.example.com", m.bottles["dev"].git[0].Key.api_url)
|
||||
|
||||
def test_both_identity_and_provisioned_key_dies(self):
|
||||
with self.assertRaises(ManifestError) as ctx:
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
"provisioned_key": {"provider": "gitea", "token_env": "T"},
|
||||
},
|
||||
}))
|
||||
self.assertIn("exactly one of", str(ctx.exception))
|
||||
self.assertIn("got both", str(ctx.exception))
|
||||
def test_gitea_key_has_no_identity_file_at_parse_time(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/didericis/foo.git",
|
||||
"key": {"provider": "gitea", "provisioner_token": "T"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("", m.bottles["dev"].git[0].IdentityFile)
|
||||
|
||||
def test_neither_identity_nor_provisioned_key_dies(self):
|
||||
with self.assertRaises(ManifestError) as ctx:
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {"url": "ssh://git@github.com/foo.git"},
|
||||
}))
|
||||
self.assertIn("exactly one of", str(ctx.exception))
|
||||
self.assertIn("got neither", str(ctx.exception))
|
||||
|
||||
def test_unknown_key_in_provisioned_key_block_dies(self):
|
||||
def test_gitea_key_missing_provisioner_token_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"provisioned_key": {
|
||||
"key": {"provider": "gitea"},
|
||||
},
|
||||
}))
|
||||
|
||||
def test_gitea_key_unknown_field_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"key": {
|
||||
"provider": "gitea",
|
||||
"token_env": "T",
|
||||
"provisioner_token": "T",
|
||||
"key_type": "rsa", # not allowed
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
class TestKeyBlockValidation(unittest.TestCase):
|
||||
"""Validation rules on the key block shared across providers."""
|
||||
|
||||
def test_missing_provider_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"provisioned_key": {"token_env": "T"},
|
||||
"key": {"path": "/dev/null"},
|
||||
},
|
||||
}))
|
||||
|
||||
def test_missing_token_env_dies(self):
|
||||
def test_unknown_provider_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"provisioned_key": {"provider": "gitea"},
|
||||
"key": {"provider": "github"},
|
||||
},
|
||||
}))
|
||||
|
||||
def test_provisioned_key_entry_has_no_identity_file(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/didericis/foo.git",
|
||||
"provisioned_key": {"provider": "gitea", "token_env": "T"},
|
||||
},
|
||||
}))
|
||||
self.assertEqual("", m.bottles["dev"].git[0].IdentityFile)
|
||||
|
||||
def test_identity_entry_has_no_provisioned_key(self):
|
||||
m = Manifest.from_json_obj(_manifest({
|
||||
"foo": {
|
||||
"url": "ssh://git@github.com/foo.git",
|
||||
"identity": "/dev/null",
|
||||
},
|
||||
}))
|
||||
self.assertIsNone(m.bottles["dev"].git[0].ProvisionedKey)
|
||||
def test_missing_key_block_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
Manifest.from_json_obj(_manifest({
|
||||
"foo": {"url": "ssh://git@github.com/foo.git"},
|
||||
}))
|
||||
|
||||
|
||||
class TestEmptyGitGateField(unittest.TestCase):
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestGitGateGitconfigRender(unittest.TestCase):
|
||||
"bottles": {"dev": {"git-gate": {"repos": {
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@100.78.141.42:30009/didericis/bot-bottle.git",
|
||||
"identity": "/dev/null",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
}}}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
|
||||
@@ -33,7 +33,7 @@ from bot_bottle.backend.smolmachines.launch import _bundle_launch_spec
|
||||
from bot_bottle.backend.util import AGENT_CA_PATH
|
||||
from bot_bottle.egress import EgressPlan, EgressRoute
|
||||
from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
||||
from bot_bottle.manifest import ManifestGitEntry, Manifest
|
||||
from bot_bottle.manifest import ManifestGitEntry, ManifestKeyConfig, Manifest
|
||||
from bot_bottle.supervise import SupervisePlan
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ def _plan(
|
||||
git_gate_json["repos"] = {
|
||||
g.Name: {
|
||||
"url": g.Upstream,
|
||||
"identity": g.IdentityFile,
|
||||
"key": {"provider": g.Key.provider or "static", "path": g.Key.path or g.IdentityFile},
|
||||
}
|
||||
for g in git
|
||||
}
|
||||
@@ -360,6 +360,7 @@ class TestProvisionGit(unittest.TestCase):
|
||||
git=[ManifestGitEntry(
|
||||
Name="bot-bottle",
|
||||
Upstream="ssh://git@host/repo.git",
|
||||
Key=ManifestKeyConfig(provider="static", path="~/.ssh/id_ed25519"),
|
||||
IdentityFile="~/.ssh/id_ed25519",
|
||||
)],
|
||||
stage_dir=self.stage,
|
||||
|
||||
Reference in New Issue
Block a user