Run every command on the build VM over SSH (M1.3)
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 55s
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 55s
execution.SSHHost.execute is now the only way the agent runs anything (L13). Commands travel as a script on SSH's stdin, so the secrets handed to a call are never on a command line, and every value handed out is redacted from the output. Timeouts kill the remote process group, and an unreachable host is reported as such, not as a failed command. Workspace and GitRepo run on the build VM. The clone uses the repository's SSH URL, so no credential is stored in it (#89); fetches and pushes get the push key over stdin, in a short-lived ssh-agent, and PRs are still opened from the agent's host with its API token. The bench runs on the build host too. Tests reach "the build VM" through a throwaway local sshd (nisse_dev .testing); CI installs openssh-server. The live push test now pushes with the sandbox's deploy key (NISSE_PUSH_KEY). Fixes #89. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
b7f1f3371c
commit
761ebdb857
32 files changed
+1098
-280
No files matched your search
+94
-35
@@ -1,18 +1,28 @@
|
||||
"""Git on the build VM (D4, L13): here, a throwaway sshd on this machine."""
|
||||
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from nisse_dev.git import GitError, GitRepo, _scrub, authenticated_url
|
||||
from nisse_dev.execution import SSHHost
|
||||
from nisse_dev.git import GitError, GitRepo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo(tmp_path: Path) -> GitRepo:
|
||||
r = GitRepo(tmp_path / "work")
|
||||
def repo(ssh_host: SSHHost, tmp_path: Path) -> GitRepo:
|
||||
r = GitRepo(ssh_host, tmp_path / "work")
|
||||
r.init()
|
||||
r.configure_identity("nisse-dev-testing", "nisse@example.test")
|
||||
return r
|
||||
|
||||
|
||||
def bare_remote(path: Path) -> str:
|
||||
subprocess.run(["git", "init", "-q", "--bare", "-b", "main", str(path)], check=True)
|
||||
return str(path)
|
||||
|
||||
|
||||
def test_init_creates_repo_on_default_branch(repo: GitRepo) -> None:
|
||||
assert repo.current_branch() == "main"
|
||||
|
||||
@@ -40,31 +50,8 @@ def test_has_changes_tracks_the_worktree(repo: GitRepo) -> None:
|
||||
assert repo.has_changes()
|
||||
|
||||
|
||||
def test_authenticated_url_shape() -> None:
|
||||
url = authenticated_url("https://gitea.test/", "o/r", "bot", "secret")
|
||||
assert url == "https://bot:secret@gitea.test/o/r.git"
|
||||
|
||||
|
||||
def test_push_failure_does_not_leak_the_token(repo: GitRepo) -> None:
|
||||
(repo.root / "a.txt").write_text("a")
|
||||
repo.commit_all("first")
|
||||
url = authenticated_url("https://127.0.0.1:1", "o/r", "bot", "sup3rsecret")
|
||||
with pytest.raises(GitError) as excinfo:
|
||||
repo.push(url, "main", token="sup3rsecret")
|
||||
# git redacts credentials from its own messages, but that is git's
|
||||
# behaviour, not ours; the scrub below is the guarantee we control.
|
||||
assert "sup3rsecret" not in str(excinfo.value)
|
||||
|
||||
|
||||
def test_scrub_removes_the_token_when_git_does_echo_it() -> None:
|
||||
assert _scrub("failed to push to https://bot:s3cret@h/o/r.git", "s3cret") == (
|
||||
"failed to push to https://bot:***@h/o/r.git"
|
||||
)
|
||||
assert _scrub("no secret here", None) == "no secret here"
|
||||
|
||||
|
||||
def test_git_error_on_bad_command(tmp_path: Path) -> None:
|
||||
repo = GitRepo(tmp_path / "not-a-repo")
|
||||
def test_git_error_on_bad_command(ssh_host: SSHHost, tmp_path: Path) -> None:
|
||||
repo = GitRepo(ssh_host, tmp_path / "not-a-repo")
|
||||
repo.root.mkdir()
|
||||
with pytest.raises(GitError):
|
||||
repo.head_sha()
|
||||
@@ -73,15 +60,87 @@ def test_git_error_on_bad_command(tmp_path: Path) -> None:
|
||||
def test_commits_since_and_remote_sha_against_a_local_remote(
|
||||
repo: GitRepo, tmp_path: Path
|
||||
) -> None:
|
||||
remote = GitRepo(tmp_path / "remote.git")
|
||||
remote.root.mkdir()
|
||||
remote._run("init", "-q", "--bare")
|
||||
remote = bare_remote(tmp_path / "remote.git")
|
||||
(repo.root / "a.txt").write_text("a")
|
||||
base = repo.commit_all("base")
|
||||
repo.push(str(remote.root), "main")
|
||||
repo._run("update-ref", "refs/remotes/origin/main", "HEAD")
|
||||
assert repo.remote_sha(str(remote.root), "main") == base
|
||||
assert repo.remote_sha(str(remote.root), "absent") is None
|
||||
repo.push(remote, "main")
|
||||
repo._git("update-ref", "refs/remotes/origin/main", "HEAD")
|
||||
assert repo.remote_sha(remote, "main") == base
|
||||
assert repo.remote_sha(remote, "absent") is None
|
||||
(repo.root / "b.txt").write_text("b")
|
||||
repo.commit_all("ahead")
|
||||
assert repo.commits_since("origin/main") == 1
|
||||
|
||||
|
||||
# --- the push key (#89) ------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ssh_remote(ssh_host: SSHHost, tmp_path: Path) -> tuple[str, str]:
|
||||
"""A bare repository reached by git over SSH, and the key that may push
|
||||
to it: the forge, as the build VM sees it."""
|
||||
remote = bare_remote(tmp_path / "forge.git")
|
||||
url = f"ssh://{ssh_host.user}@127.0.0.1:{ssh_host.port}{remote}"
|
||||
return url, ssh_host._key.read_text()
|
||||
|
||||
|
||||
def test_clone_and_push_with_the_key_store_no_credential(
|
||||
ssh_host: SSHHost, tmp_path: Path, ssh_remote: tuple[str, str]
|
||||
) -> None:
|
||||
url, key = ssh_remote
|
||||
seed = GitRepo(ssh_host, tmp_path / "seed", known_hosts=str(tmp_path / "kh"))
|
||||
seed.init()
|
||||
seed.configure_identity("t", "t@example.test")
|
||||
(seed.root / "a.txt").write_text("a")
|
||||
seed.commit_all("first")
|
||||
seed.push(url, "main", key=key)
|
||||
|
||||
clone = GitRepo(ssh_host, tmp_path / "clone", known_hosts=str(tmp_path / "kh"))
|
||||
clone.clone(url, key=key)
|
||||
assert (clone.root / "a.txt").read_text() == "a"
|
||||
config = (clone.root / ".git" / "config").read_text()
|
||||
assert "PRIVATE KEY" not in config
|
||||
# The URL recorded is the credential-free one we gave.
|
||||
assert url in config
|
||||
|
||||
|
||||
def test_the_key_is_gone_when_the_command_ends(
|
||||
ssh_host: SSHHost, tmp_path: Path, ssh_remote: tuple[str, str]
|
||||
) -> None:
|
||||
url, key = ssh_remote
|
||||
repo = GitRepo(ssh_host, tmp_path / "w", known_hosts=str(tmp_path / "kh"))
|
||||
repo.init()
|
||||
repo.configure_identity("t", "t@example.test")
|
||||
(repo.root / "a.txt").write_text("a")
|
||||
repo.commit_all("first")
|
||||
before = _agents()
|
||||
repo.push(url, "main", key=key)
|
||||
assert _agents() == before # the throwaway ssh-agent was killed
|
||||
|
||||
|
||||
def test_a_failed_push_still_kills_its_agent(
|
||||
ssh_host: SSHHost, tmp_path: Path, ssh_remote: tuple[str, str]
|
||||
) -> None:
|
||||
_, key = ssh_remote
|
||||
repo = GitRepo(ssh_host, tmp_path / "w", known_hosts=str(tmp_path / "kh"))
|
||||
repo.init()
|
||||
before = _agents()
|
||||
with pytest.raises(GitError):
|
||||
repo.push(
|
||||
f"ssh://nobody@127.0.0.1:{ssh_host.port}/nowhere.git", "main", key=key
|
||||
)
|
||||
assert _agents() == before
|
||||
|
||||
|
||||
def _agents() -> set[str]:
|
||||
out = subprocess.run(
|
||||
["pgrep", "-x", "ssh-agent"], capture_output=True, text=True, check=False
|
||||
).stdout
|
||||
return set(out.split())
|
||||
|
||||
|
||||
def test_a_remote_url_is_quoted_not_interpreted(repo: GitRepo, tmp_path: Path) -> None:
|
||||
marker = tmp_path / "pwned"
|
||||
with pytest.raises(GitError):
|
||||
repo.push(f"x; touch {shlex.quote(str(marker))}", "main")
|
||||
assert not marker.exists()
|
||||
Reference in new issue
Block a user