Run every command on the build VM over SSH (M1.3) #93

Merged
dongar merged 5 commits from ssh-execution into main 2026-10-02 22:44:22 +00:00
5 changed files with 164 additions and 8 deletions
Showing only changes of commit e02bac110d - Show all commits

No files matched your search

+1
View File
@@ -217,6 +217,7 @@ def prepare_workspace(
if not repo.exists():
repo.clone(remote, credential=credential)
repo.configure_identity(account, f"{account}@users.noreply.local")
repo.install_excludes() # build artifacts are never the work (#80)
return repo
+62 -4
View File
@@ -33,6 +33,35 @@ class GitError(RuntimeError):
pass
# Build artifacts and caches that are never the work (D4, #80). Kept out of
# every commit the harness makes, tracked or not, and excluded in the clone
# so they don't even show as changes. Conservative on purpose: a pattern here
# must never match a project's real source.
ARTIFACT_PATTERNS = (
"__pycache__/",
"*.py[cod]",
".pytest_cache/",
".mypy_cache/",
".ruff_cache/",
".tox/",
".nox/",
"*.egg-info/",
".coverage",
"htmlcov/",
".venv/",
"node_modules/",
".DS_Store",
)
_EXCLUDES_MARK = "# nisse-dev: build artifacts are never the work (D4)"
def _pathspec(pattern: str) -> str:
"""A pathspec leaving ``pattern`` out wherever it occurs in the tree."""
glob = pattern.rstrip("/")
tail = "/**" if pattern.endswith("/") else ""
return f":(exclude,glob)**/{glob}{tail}"
@dataclass(frozen=True)
class HTTPSToken:
"""A push-only account's token, for HTTPS remotes (D4)."""
@@ -84,6 +113,7 @@ class GitRepo:
*args: str,
credential: PushCredential | None = None,
cwd: str | None = None,
raw: bool = False,
) -> str:
quoted = " ".join(shlex.quote(a) for a in args)
command, stdin, secrets = f"git {quoted}", None, {}
@@ -117,7 +147,8 @@ class GitRepo:
f"git {args[0]} failed: "
f"{result.stderr.strip() or result.stdout.strip()}"
)
return result.stdout.strip()
# Raw for output whose leading whitespace means something (porcelain).
return result.stdout if raw else result.stdout.strip()
# -- inspection -------------------------------------------------------
@@ -191,15 +222,42 @@ class GitRepo:
return False
return True
def install_excludes(self) -> None:
"""List the build artifacts in the clone's own exclude file, so they
never appear as changes. Local to the clone: nothing in the project
changes. Idempotent."""
path = shlex.quote(str(self.root / ".git" / "info" / "exclude"))
block = "\n".join([_EXCLUDES_MARK, *ARTIFACT_PATTERNS])
result = self.host.execute(
f'mkdir -p -- "$(dirname -- {path})" && touch -- {path} && '
f"grep -qxF {shlex.quote(_EXCLUDES_MARK)} {path} || "
f"cat >> {path}",
stdin=block,
timeout_s=TIMEOUT_S,
)
if not result.ok:
raise GitError(f"could not set excludes: {result.stderr.strip()}")
def commit_all(self, message: str) -> str | None:
"""Returns the new commit sha, or None when there was nothing to
commit — an empty milestone is a fact to report, not an error."""
"""Commit the work: every change except build artifacts, which are
left out even when the project tracks them (#80). Returns the new
commit sha, or None when there was nothing to commit — an empty
milestone is a fact to report, not an error."""
if not self.has_changes():
return None
self._git("add", "-A")
self._git("add", "-A", "--", ".", *(_pathspec(p) for p in ARTIFACT_PATTERNS))
staged = self._git("diff", "--cached", "--name-only")
if not staged:
return None
self._git("commit", "-q", "-m", message)
return self.head_sha()
def left_out(self) -> list[str]:
"""Changes not in the last commit: build artifacts the harness left
out, to be reported rather than silently dropped (D4)."""
out = self._git("status", "--porcelain", "--untracked-files=all", raw=True)
return sorted(line[3:] for line in out.splitlines() if line)
def push(
self, remote_url: str, branch: str, *, credential: PushCredential | None = None
) -> None:
+32 -4
View File
@@ -7,6 +7,7 @@ PR exists rather than repeating the push-and-open — the spec's own example of
an idempotent transition reconciled against external truth.
"""
from collections.abc import Sequence
from dataclasses import dataclass
from nisse_dev.git import GitRepo, PushCredential
@@ -80,6 +81,8 @@ class MilestonePublisher:
return PublishResult(None, "no changes to publish")
# The executor committed its work itself; publish that.
sha = self._git.head_sha()
# D4: what was left out (build artifacts) is reported, not dropped.
left_out = self._git.left_out()
intent_id = self._store.begin_intent(
OPEN_PR_INTENT,
@@ -99,17 +102,19 @@ class MilestonePublisher:
return PublishResult(
existing,
f"PR #{existing.number} was already open for this milestone; "
f"pushed to it: {existing.url}",
f"pushed to it: {existing.url}" + _left_out_note(left_out),
)
pr = self._client.open_pull_request(
self._repo,
head=branch,
base=self._base_branch,
title=f"Milestone {milestone}: {project}",
body=_pr_body(milestone, plan, service_url),
body=_pr_body(milestone, plan, service_url, left_out),
)
self._store.commit_intent(intent_id, cause=f"opened PR #{pr.number}")
return PublishResult(pr, f"opened PR #{pr.number}: {pr.url}")
return PublishResult(
pr, f"opened PR #{pr.number}: {pr.url}" + _left_out_note(left_out)
)
def published_pull_request(
self, task_id: str, milestone: int
@@ -161,7 +166,21 @@ class MilestonePublisher:
return sha
def _pr_body(milestone: int, plan: str, service_url: str | None) -> str:
def _left_out_note(paths: list[str]) -> str:
if not paths:
return ""
shown = ", ".join(paths[:5]) + (
f" and {len(paths) - 5} more" if len(paths) > 5 else ""
)
return f" (left out {len(paths)} build artifact(s): {shown})"
def _pr_body(
milestone: int,
plan: str,
service_url: str | None,
left_out: Sequence[str] = (),
) -> str:
parts = [
f"Milestone {milestone}, implemented by nisse-dev against the approved plan.",
"",
@@ -174,4 +193,13 @@ def _pr_body(milestone: int, plan: str, service_url: str | None) -> str:
# so a reviewer can exercise it. Nothing reports one yet; the browser
# tool (D7) is what will.
parts += ["", "## Running test service", "", service_url]
if left_out:
parts += [
"",
"## Left out",
"",
"Build artifacts in the workspace, never part of the work:",
"",
*(f"- `{path}`" for path in left_out),
]
return "\n".join(parts)
+49
View File
@@ -195,3 +195,52 @@ def test_an_https_token_answers_git_through_the_helper(
assert asked.stdout == "username=[redacted]\npassword=[redacted]\n"
stored = ssh_host.execute(f"{body} store", secrets={"NISSE_PUSH_TOKEN": "x"})
assert stored.stdout == ""
# --- build artifacts are never the work (#80, D4) ----------------------------
def test_artifacts_are_left_out_even_when_the_project_tracks_them(
repo: GitRepo,
) -> None:
# The sandbox's main tracks .pyc files (an earlier run published them).
cache = repo.root / "pkg" / "__pycache__"
cache.mkdir(parents=True)
(repo.root / "pkg" / "m.py").write_text("x = 1\n")
(cache / "m.cpython-312.pyc").write_bytes(b"old")
repo._git("add", "-A")
repo._git("commit", "-qm", "a project that tracks its caches")
# A test run: the tracked .pyc changes, new caches appear, and the work.
(cache / "m.cpython-312.pyc").write_bytes(b"new")
(cache / "m.cpython-310.pyc").write_bytes(b"x")
(repo.root / ".pytest_cache").mkdir()
(repo.root / ".pytest_cache" / "v").write_text("x")
(repo.root / "pkg" / "m.py").write_text("x = 2\n")
assert repo.commit_all("the work") is not None
committed = repo._git("show", "--name-only", "--format=", "HEAD").split()
assert committed == ["pkg/m.py"]
assert repo.left_out() == [
".pytest_cache/v",
"pkg/__pycache__/m.cpython-310.pyc",
"pkg/__pycache__/m.cpython-312.pyc",
]
def test_only_artifacts_changed_means_nothing_to_commit(repo: GitRepo) -> None:
(repo.root / "__pycache__").mkdir()
(repo.root / "__pycache__" / "a.pyc").write_bytes(b"x")
assert repo.commit_all("nothing real") is None
def test_excludes_hide_new_artifacts_and_install_once(repo: GitRepo) -> None:
repo.install_excludes()
repo.install_excludes()
exclude = (repo.root / ".git" / "info" / "exclude").read_text()
assert exclude.count("__pycache__/") == 1
(repo.root / "node_modules").mkdir()
(repo.root / "node_modules" / "x.js").write_text("x")
(repo.root / "real.py").write_text("x")
# Even the model's own `git add -A` now skips them.
assert repo._git("status", "--porcelain", "--untracked-files=all") == "?? real.py"
+20
View File
@@ -90,6 +90,9 @@ class FakeGit:
def commit_all(self, message: str) -> str | None:
return self.repo.commit_all(message)
def left_out(self) -> list[str]:
return self.repo.left_out()
def push(self, remote_url: str, branch: str, *, credential: object = None) -> None:
self.pushed.append(branch)
self.remote[branch] = self.repo.head_sha()
@@ -147,6 +150,23 @@ def test_a_branch_named_after_the_task_does_not_block_publishing(
assert git.pushed == ["demo-abc-milestone-1"]
def test_left_out_artifacts_are_reported_not_published(
store: StateStore, tmp_path: Path
) -> None:
git = FakeGit(tmp_path / "work")
(tmp_path / "work" / "__pycache__").mkdir()
(tmp_path / "work" / "__pycache__" / "work.cpython-310.pyc").write_bytes(b"x")
client = FakeGitea()
result = build(store, git, client).publish(
task_id="t", milestone=1, project="demo", plan="p"
)
assert "left out 1 build artifact(s): __pycache__/work.cpython-310.pyc" in (
result.note
)
assert "## Left out" in client.opened[0]["body"]
assert "`__pycache__/work.cpython-310.pyc`" in client.opened[0]["body"]
def test_intent_is_recorded_then_committed(store: StateStore, tmp_path: Path) -> None:
build(store, FakeGit(tmp_path / "work"), FakeGitea()).publish(
task_id="t", milestone=1, project="demo", plan="p"