deploy: add git-pull source for host deployment

Host can now fetch code directly from GitHub instead of receiving a VM-built tarball, bypassing the VM->host scp/kex failures and stale-tar risk. Design: VM sends only two small scripts (host_git_fetch.sh + host_deploy.sh, sha-verified via the existing transfer chain, now with a chunk_key fallback that does not need SSH_PASSWORD); host clones via a read-only deploy key (SSH only, HTTPS refused, key must exist with mode 0600), checks out the exact 40-char commit, verifies HEAD, builds a tar in the same format as the VM release (DEPLOYED_COMMIT + RELEASE.json, member names without ./ prefix so host_deploy.sh tar -O works), then calls the unchanged host_deploy.sh for staging swap, backup, health and rollback. Git fetch tolerates the known intermittent host->GitHub degradation: 120s timeout, 3 attempts, 10s sleep, live untouched until host_deploy runs. VM gates: clean tree, tests, commit must be on origin/main, SSH repo only, full sha only; --source tar remains the default. Tests: deploy suite OK, including 6 new host_git_fetch simulations (success with clean tar members, unknown commit, key permissions/missing, HTTPS refusal, short sha) and 4 new deploy.py git-mode helpers plus chunk_key fallback; git-mode dry-run verified against HEAD 630d14f.
This commit is contained in:
host committed 2026-10-29 03:37:55 +08:00
1 parent 630d14fe07
commit e8e256f6e5
5 files changed
+542 -7

No files matched your search

+65
View File
@@ -82,6 +82,71 @@ class TestDeployPure(unittest.TestCase):
self.assertIsNone(deploy_mod.live_commit("Warning: something\n"))
class TestGitModeHelpers(unittest.TestCase):
"""--source git 的纯逻辑门禁:完整 sha、SSH 形式、origin/main
归属。主机只能拉到已推的提交,未推的 HEAD 必须在 VM 侧拦住。"""
def test_is_full_sha(self):
self.assertTrue(deploy_mod.is_full_sha("a" * 40))
self.assertFalse(deploy_mod.is_full_sha("a" * 7))
self.assertFalse(deploy_mod.is_full_sha("g" * 40))
self.assertFalse(deploy_mod.is_full_sha(""))
self.assertFalse(deploy_mod.is_full_sha(None))
def test_is_ssh_git_url(self):
self.assertTrue(deploy_mod.is_ssh_git_url(
"git@github.com:openqj/codex-app-server.git"))
self.assertTrue(deploy_mod.is_ssh_git_url(
"ssh://git@github.com/openqj/codex-app-server.git"))
self.assertFalse(deploy_mod.is_ssh_git_url(
"https://github.com/openqj/codex-app-server.git"))
self.assertFalse(deploy_mod.is_ssh_git_url(""))
def test_commit_on_origin_main_uses_merge_base(self):
calls = []
def fake_run(cmd, cwd=None, check=True):
calls.append(cmd)
return subprocess.CompletedProcess(args=cmd, returncode=0,
stdout="", stderr="")
with mock.patch.object(deploy_mod, "run", side_effect=fake_run):
self.assertTrue(deploy_mod.commit_on_origin_main("a" * 40))
self.assertIn("merge-base", calls[0])
self.assertIn("origin/main", calls[0])
def test_transfer_falls_back_to_chunk_key(self):
"""scp 失败后走 chunk_key 分片(密钥版),不依赖 SSH_PASSWORD;
验远端 sha 通过即成功,不应退出。"""
calls = []
def fake_run(cmd, cwd=None, check=True, **kwargs):
calls.append(cmd)
if cmd[0] == "scp":
return subprocess.CompletedProcess(args=cmd, returncode=1,
stdout="", stderr="reset")
return subprocess.CompletedProcess(args=cmd, returncode=0,
stdout="", stderr="")
def fake_helper(name):
return "/helpers/chunk_send_key_explicit.py" \
if name == "chunk_send_key_explicit.py" else None
with mock.patch.object(deploy_mod.subprocess, "run",
side_effect=fake_run):
with mock.patch.object(deploy_mod, "helper_path",
side_effect=fake_helper):
with mock.patch.object(deploy_mod, "ssh_cmd",
return_value=["ssh", "dummy"]):
with mock.patch.object(
deploy_mod, "remote_sha",
return_value="deadbeef"):
deploy_mod.transfer("h", "u", "/tmp/a",
"/tmp/b", "deadbeef")
self.assertTrue(any("chunk_send_key_explicit.py" in x
for c in calls for x in c))
class TestDefaultTestSuites(unittest.TestCase):
"""P2-ops 回归:部署前默认清单必须含 deploy/tests,
且 run_tests() 确实按 discover -s 逐套跑(不是包路径)。"""