Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 78 additions & 21 deletions scripts/work-bundle/control_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,23 @@ def _git(path: Path, *args: str) -> str:
return result.stdout.strip() if result.returncode == 0 else ""


def _git_checkout_observation(path: Path) -> dict[str, str] | None:
resolved = path.expanduser().resolve()
top_level = _git(resolved, "rev-parse", "--show-toplevel")
if not top_level or Path(top_level).expanduser().resolve() != resolved:
return None
branch = _git(resolved, "branch", "--show-current")
head = _git(resolved, "rev-parse", "HEAD")
common_dir = _git(resolved, "rev-parse", "--git-common-dir")
if not branch or not head or not common_dir:
raise ControlPlaneError("WB_CONTROL_PLANE_BOUND_GIT_INVALID")
return {
"observed_branch": branch,
"observed_head": head,
"git_common_dir": common_dir,
}


def _git_remote(path: Path) -> str:
return validated_remote(_git(path, "remote", "get-url", "origin"))

Expand Down Expand Up @@ -2186,6 +2203,7 @@ def rollback_attach() -> None:
owned_member_paths.append(candidate)
state = "absent"
if candidate is not None and candidate.exists():
manual_observation = _git_checkout_observation(candidate) if manual_locator else None
actual_remote = "" if manual_locator else _resolved_git_remote(candidate)
if not manual_locator and actual_remote != canonical_remote(remote):
raise ControlPlaneError(
Expand Down Expand Up @@ -2214,12 +2232,24 @@ def rollback_attach() -> None:
)
)
),
"observed_branch": "" if manual_locator else _git(candidate, "branch", "--show-current"),
"observed_head": "" if manual_locator else _git(candidate, "rev-parse", "HEAD"),
"observed_branch": (
manual_observation["observed_branch"]
if manual_observation is not None
else ("" if manual_locator else _git(candidate, "branch", "--show-current"))
),
"observed_head": (
manual_observation["observed_head"]
if manual_observation is not None
else ("" if manual_locator else _git(candidate, "rev-parse", "HEAD"))
),
"observed_at": utc_now_rfc3339(),
"git_common_dir": "" if manual_locator else _git(candidate, "rev-parse", "--git-common-dir"),
"git_common_dir": (
manual_observation["git_common_dir"]
if manual_observation is not None
else ("" if manual_locator else _git(candidate, "rev-parse", "--git-common-dir"))
),
}
if not manual_locator:
if not manual_locator or manual_observation is not None:
readiness_failures.extend(
_repository_execution_issues(candidate, str(repository.get("default_branch") or ""), repository_id)
)
Expand Down Expand Up @@ -2365,6 +2395,21 @@ def cmd_doctor_workspace(args: list[str], *, command_name: str = "doctor-workspa
bindings = _registry_bindings()
binding = bindings.get(workspace_id)
local_failures: list[str] = []
if parsed.repair and not portable_failures:
try:
_, code = _attach(
workspace_root,
"none",
{},
True,
create_script_index=False,
)
except ControlPlaneError as exc:
local_failures.append(exc.code)
code = 1
if code == 0:
bindings = _registry_bindings()
binding = bindings.get(workspace_id)
if not binding:
local_failures.append("WB_CONTROL_PLANE_BINDING_MISSING")
elif Path(str(binding.get("workspace_root") or "")).resolve() != workspace_root:
Expand Down Expand Up @@ -2401,6 +2446,35 @@ def cmd_doctor_workspace(args: list[str], *, command_name: str = "doctor-workspa
if repo.get("required"):
missing_required.append(repository_id)
continue
if repo.get("locator_type") == "manual":
try:
observation = _git_checkout_observation(project_path)
except ControlPlaneError as exc:
local_failures.append(f"{exc.code}:{repository_id}")
if repo.get("required"):
missing_required.append(f"{exc.code}:{repository_id}")
continue
if observation is None:
continue
if (
not local.get("observed_branch")
or not local.get("observed_head")
or not local.get("git_common_dir")
):
missing_required.append(f"WB_REPOSITORY_OBSERVATION_MISSING:{repository_id}")
elif (
str(local.get("observed_branch")) != observation["observed_branch"]
or str(local.get("observed_head")) != observation["observed_head"]
or str(local.get("git_common_dir")) != observation["git_common_dir"]
):
missing_required.append(f"WB_REPOSITORY_OBSERVATION_STALE:{repository_id}")
readiness_issues = _repository_execution_issues(
project_path, str(repo.get("default_branch") or ""), repository_id
)
for issue in readiness_issues:
if issue not in missing_required:
missing_required.append(issue)
continue
if repo.get("locator_type") != "manual":
try:
actual_remote = _resolved_git_remote(project_path)
Expand All @@ -2419,23 +2493,6 @@ def cmd_doctor_workspace(args: list[str], *, command_name: str = "doctor-workspa
for issue in readiness_issues:
if issue not in missing_required:
missing_required.append(issue)
if parsed.repair and not portable_failures:
try:
result, code = _attach(
workspace_root,
"none",
{},
True,
create_script_index=False,
)
except ControlPlaneError as exc:
local_failures.append(exc.code)
result, code = {"status": "issues-found"}, 1
if code == 0:
bindings = _registry_bindings()
binding = bindings.get(workspace_id)
if binding:
local_failures = [item for item in local_failures if item != "WB_CONTROL_PLANE_BINDING_MISSING"]
status = "passed" if not portable_failures and not local_failures else "issues-found"
out(
{
Expand Down
98 changes: 98 additions & 0 deletions tests/test_control_plane_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,104 @@ def test_non_git_v3_member_migrates_with_manual_locator(tmp_path: Path) -> None:
assert run_wb(config, "doctor-workspace", str(workspace)).returncode == 0


def test_manual_git_member_attach_and_doctor_repair_publish_observations(tmp_path: Path) -> None:
config = config_root(tmp_path / "config-root")
workspace = tmp_path / "workspace"
local = workspace / "manual-main"
local.mkdir(parents=True)
control = workspace / ".work-bundle"
control.mkdir(parents=True)
(control / "project.yaml").write_text(
"\n".join([
"metadata_version: 3", f"workspace_root: {workspace}", "workspace_mode: multi-repository",
f"project_root: {local}", "source_repositories:", " - id: manual-main",
f" project_root: {local}", " origin_id: manual-main", " git_repository: false",
' remote: ""', " checkout_kind: local-project", "",
]), encoding="utf-8"
)
proposal = run_wb(config, "migrate-control-plane", str(workspace), "--dry-run")
assert proposal.returncode == 0, proposal.stdout + proposal.stderr
proposal_id = json.loads(proposal.stdout)["migration"]["proposal_id"]
applied = run_wb(
config,
"migrate-control-plane",
str(workspace),
"--apply",
"--accepted-proposal-id",
proposal_id,
)
assert applied.returncode == 0, applied.stdout + applied.stderr
portable = yaml.safe_load((control / "project.yaml").read_text(encoding="utf-8"))
workspace_id = portable["workspace"]["id"]
default_branch = portable["source_repositories"][0]["default_branch"]

subprocess.run(["git", "init", "-q", "-b", default_branch, str(local)], check=True)
git(local, "config", "user.email", "test@example.com")
git(local, "config", "user.name", "Test")
(local / "README.md").write_text("# manual Git member\n", encoding="utf-8")
git(local, "add", "README.md")
git(local, "commit", "-q", "-m", "init")
head = git(local, "rev-parse", "HEAD")

attached = run_wb(
config,
"attach-workspace",
str(workspace),
"--repository-path",
f"manual-main={local}",
"--materialize",
"none",
"--apply",
)
assert attached.returncode == 0, attached.stdout + attached.stderr
registry = yaml.safe_load((config / "registry/projects.yaml").read_text(encoding="utf-8"))
observation = registry["device_bindings"][workspace_id]["repositories"]["manual-main"]
assert observation["checkout_kind"] == "manual"
assert observation["observed_branch"] == default_branch
assert observation["observed_head"] == head
assert observation["git_common_dir"]

preflight = run_orch(
config,
"--workspace-root",
str(workspace),
"repository-preflight",
"--repository",
str(local),
)
assert preflight.returncode == 0, preflight.stdout + preflight.stderr
row = json.loads(preflight.stdout)["repository_preflight"]["repositories"][0]
assert row["status"] == "clean"

observation["git_common_dir"] = ""
(config / "registry/projects.yaml").write_text(
yaml.safe_dump(registry, sort_keys=False), encoding="utf-8"
)
doctor = run_wb(config, "doctor-workspace", str(workspace))
assert doctor.returncode == 0, doctor.stdout + doctor.stderr
doctor_data = json.loads(doctor.stdout)
assert doctor_data["execution_readiness"]["status"] == "not-ready"
assert "WB_REPOSITORY_OBSERVATION_MISSING:manual-main" in doctor_data["execution_readiness"]["execution_readiness_failures"]

observation["git_common_dir"] = "/stale/git/common-dir"
(config / "registry/projects.yaml").write_text(
yaml.safe_dump(registry, sort_keys=False), encoding="utf-8"
)
doctor = run_wb(config, "doctor-workspace", str(workspace))
assert doctor.returncode == 0, doctor.stdout + doctor.stderr
doctor_data = json.loads(doctor.stdout)
assert doctor_data["execution_readiness"]["status"] == "not-ready"
assert "WB_REPOSITORY_OBSERVATION_STALE:manual-main" in doctor_data["execution_readiness"]["execution_readiness_failures"]

repaired = run_wb(config, "doctor-workspace", str(workspace), "--repair")
assert repaired.returncode == 0, repaired.stdout + repaired.stderr
repaired_registry = yaml.safe_load((config / "registry/projects.yaml").read_text(encoding="utf-8"))
repaired_observation = repaired_registry["device_bindings"][workspace_id]["repositories"]["manual-main"]
assert repaired_observation["observed_branch"] == default_branch
assert repaired_observation["observed_head"] == head
assert repaired_observation["git_common_dir"]


def test_existing_checkout_credential_remote_is_rejected_without_echo(tmp_path: Path) -> None:
config = config_root(tmp_path / "config-root")
workspace, remote, _ = make_v3_workspace(tmp_path / "fixture")
Expand Down
Loading