Keep fixed checks and lifecycle fixtures isolated in CI (U5)

Why:
Go-to-Python tests leave ignored bytecode in fixed sources, lifecycle clones need a fixed main reference, and boundary gates require ripgrep.

Evidence:
SELF real byproduct and clone regressions failed before repair; host passed 11 component cases, 4 hook cases and 33 CI credential cases.

Impact:
Execute versioned Go checks in disposable clones while preserving editable overlays and strict dirty guards; install the boundary prerequisite and seed only isolated pinned lifecycle refs.

Refs:
R8-R10, R14-R15; dependency-workspace U5; Dev PR 537 run 3036
This commit is contained in:
2026-10-14 02:29:36 +00:00
parent ed923b3aa5
commit adb6cbcf48
6 changed files with 153 additions and 3 deletions
+15
View File
@@ -263,6 +263,16 @@ jobs:
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Install boundary checker prerequisite
run: &install_boundary_prerequisite |
set -euo pipefail
if ! command -v rg >/dev/null 2>&1; then
SUDO=""
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then SUDO=sudo; fi
$SUDO apt-get update -y
$SUDO apt-get install -y ripgrep
fi
rg --version
- name: Prefetch Go modules without executing candidate code - name: Prefetch Go modules without executing candidate code
env: env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
@@ -769,6 +779,8 @@ jobs:
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Install boundary checker prerequisite
run: *install_boundary_prerequisite
- name: Install house-style Python packages - name: Install house-style Python packages
run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl
- name: Install PostgreSQL, pgvector and Redis - name: Install PostgreSQL, pgvector and Redis
@@ -995,6 +1007,7 @@ jobs:
# Future lifecycle fetches use only local fixed-source mirrors. # Future lifecycle fetches use only local fixed-source mirrors.
git(directory, "remote", "set-url", "origin", (mirrors / "Agent-System-RDM.git").as_uri()) git(directory, "remote", "set-url", "origin", (mirrors / "Agent-System-RDM.git").as_uri())
git(directory, "update-ref", "refs/remotes/origin/main", pins["sha"]) git(directory, "update-ref", "refs/remotes/origin/main", pins["sha"])
git(directory, "update-ref", "refs/heads/main", pins["sha"])
mirror_env() mirror_env()
(root / ".ci-rdm-pins").write_text("\n".join(revisions) + "\n") (root / ".ci-rdm-pins").write_text("\n".join(revisions) + "\n")
except (ValueError, OSError, subprocess.CalledProcessError) as error: except (ValueError, OSError, subprocess.CalledProcessError) as error:
@@ -1082,6 +1095,8 @@ jobs:
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz" tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Install boundary checker prerequisite
run: *install_boundary_prerequisite
- name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition) - name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition)
env: env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }} SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
+37 -1
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Prepare selected fixed sources and run explicitly local component checks.""" """Prepare selected fixed sources and run explicitly local component checks."""
import argparse import argparse
from contextlib import contextmanager
import os import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import sys import sys
import tempfile
from workspace.cli import Workspace, process_environment from workspace.cli import Workspace, process_environment
from workspace.common import WorkspaceError from workspace.common import WorkspaceError
@@ -15,6 +17,8 @@ SINGLE = {"test-backend": "Agent-System-Backend", "test-backend-pg": "Agent-Syst
"test-frontend": "Agent-System-Frontend", "test-ui": "Agent-System-UI", "test-frontend": "Agent-System-Frontend", "test-ui": "Agent-System-UI",
"test-multi-instance": "Agent-System-Backend", "eval-deliverables": "Agent-System-Backend", "test-multi-instance": "Agent-System-Backend", "eval-deliverables": "Agent-System-Backend",
"dev-backend": "Agent-System-Backend", "dev-frontend": "Agent-System-Frontend"} "dev-backend": "Agent-System-Backend", "dev-frontend": "Agent-System-Frontend"}
GO_CHECKS = {"test-backend", "test-backend-pg", "test-core", "test-agentcmd",
"test-libs", "test-multi-instance", "eval-deliverables"}
def run(command, cwd, env): def run(command, cwd, env):
@@ -40,6 +44,34 @@ def prepare_local_source(workspace, component):
workspace.prepare_one(component) workspace.prepare_one(component)
@contextmanager
def check_sources(workspace, selected, target, overlay):
paths = {component["id"]: workspace.root / component["path"] for component in selected}
fixed = [component for component in selected
if workspace.state.data["components"][component["id"]].get("mode") == "versioned"]
if overlay or target not in GO_CHECKS or not fixed:
yield paths
return
# Go tests can create ignored outputs (including Python bytecode). Keep
# strict source guards and execute fixed commits in owned disposable clones.
# Editable sources and explicit overlays still exercise the owner's code.
with tempfile.TemporaryDirectory(prefix="component-check-") as temporary:
area = Path(temporary)
for component in fixed:
name = component["id"]
source = paths[name]
actual = workspace.git.safe(source, component["repo"])
commit = workspace.state.data["components"][name]["baseline"]
if actual["head"] != commit or actual["branch"]:
raise WorkspaceError(f"fixed source changed before local check: {name}")
destination = area / name
workspace.git.call(area, "clone", "--no-checkout", "--no-hardlinks", "--", str(source), str(destination))
workspace.git.call(destination, "checkout", "--detach", commit)
paths[name] = destination
print(f"[component-check] fixed check snapshot {name} commit={commit}", flush=True)
yield paths
def check(root, target, overlay): def check(root, target, overlay):
root = root.resolve() root = root.resolve()
if target in ("test-backend-pg", "test-multi-instance") and not os.environ.get("AGENT_PG_TEST_DSN"): if target in ("test-backend-pg", "test-multi-instance") and not os.environ.get("AGENT_PG_TEST_DSN"):
@@ -67,7 +99,11 @@ def check(root, target, overlay):
else: else:
env["GOWORK"] = "off" env["GOWORK"] = "off"
print(f"[component-check] local developer sources; GOWORK={env['GOWORK']}; registered edit branches and unsaved code retained", flush=True) print(f"[component-check] local developer sources; GOWORK={env['GOWORK']}; registered edit branches and unsaved code retained", flush=True)
paths = {component["id"]: root / component["path"] for component in selected} with check_sources(workspace, selected, target, overlay) as paths:
run_checks(root, target, selected, names, paths, env)
def run_checks(root, target, selected, names, paths, env):
if target == "dev": if target == "dev":
print("Backend: make dev-backend [LOCAL_OVERLAY=1]\nFrontend: make dev-frontend\nPrivate module transport is scoped to those processes.") print("Backend: make dev-backend [LOCAL_OVERLAY=1]\nFrontend: make dev-frontend\nPrivate module transport is scoped to those processes.")
return return
+1
View File
@@ -2,6 +2,7 @@
# Public SDK isolation and sibling-library ratchets use the fixed composition map # Public SDK isolation and sibling-library ratchets use the fixed composition map
# when supplied; a manual invocation uses sources prepared by the checkout owner. # when supplied; a manual invocation uses sources prepared by the checkout owner.
set -euo pipefail set -euo pipefail
command -v rg >/dev/null 2>&1 || { echo "boundary: requires ripgrep (rg)" >&2; exit 2; }
root=$(cd "$(dirname "$0")/.." && pwd) root=$(cd "$(dirname "$0")/.." && pwd)
fail=0 fail=0
source_path() { python3 "$root/scripts/component-source.py" "$1"; } source_path() { python3 "$root/scripts/component-source.py" "$1"; }
+5
View File
@@ -337,6 +337,11 @@ class CheckoutTests(unittest.TestCase):
for sha in shas: for sha in shas:
self.assertEqual(self.git(work / "rdm", "cat-file", "-t", sha), "commit") self.assertEqual(self.git(work / "rdm", "cat-file", "-t", sha), "commit")
self.assertEqual(self.git(work / "rdm", "rev-parse", "HEAD"), shas[2]) self.assertEqual(self.git(work / "rdm", "rev-parse", "HEAD"), shas[2])
# A lifecycle cache clones this isolated checkout. Local clones copy
# heads, so the fixed source must expose a deterministic main head.
lifecycle = self.base / "lifecycle-source"
self.git(self.base, "clone", "--no-hardlinks", str(work / "rdm"), str(lifecycle))
self.assertEqual(self.git(lifecycle, "rev-parse", "origin/main"), shas[2])
# Candidate builds are a separate phase: the shell has no checkout token, # Candidate builds are a separate phase: the shell has no checkout token,
# and lifecycle source fetches resolve through public local mirror paths. # and lifecycle source fetches resolve through public local mirror paths.
from tests.test_dependency_ci import phase from tests.test_dependency_ci import phase
+88 -2
View File
@@ -31,6 +31,7 @@ class ComponentCheckTests(unittest.TestCase):
shutil.copyfile(SOURCE / "Makefile", self.root / "Makefile") shutil.copyfile(SOURCE / "Makefile", self.root / "Makefile")
(self.root / ".gitignore").write_text(".workspace/\ngo.work\ngo.work.sum\ntests/integration/multi-instance/.build/\n") (self.root / ".gitignore").write_text(".workspace/\ngo.work\ngo.work.sum\ntests/integration/multi-instance/.build/\n")
app = self.base / "backend" app = self.base / "backend"
self.backend = app
app.mkdir() app.mkdir()
self.call(app, "git", "init", "--initial-branch=main") self.call(app, "git", "init", "--initial-branch=main")
(app / "go.mod").write_text("module example.com/backend\n\ngo 1.23\n") (app / "go.mod").write_text("module example.com/backend\n\ngo 1.23\n")
@@ -77,13 +78,98 @@ class ComponentCheckTests(unittest.TestCase):
self.call(self.root, "make", "test-backend-pg", env=self.env | {"AGENT_PG_TEST_DSN": "fixture-dsn", "AGENT_REDIS_TEST_ADDR": "fixture-redis"}) self.call(self.root, "make", "test-backend-pg", env=self.env | {"AGENT_PG_TEST_DSN": "fixture-dsn", "AGENT_REDIS_TEST_ADDR": "fixture-redis"})
def test_multi_instance_binary_is_built_inside_dev_root_and_reached_by_suite(self): def test_multi_instance_binary_is_built_inside_dev_root_and_reached_by_suite(self):
suite = self.add_multi_instance_suite()
self.call(self.root, "make", "test-multi-instance", env=self.env | {"AGENT_PG_TEST_DSN": "fixture-dsn"})
self.assertTrue((suite / ".build/agent-system").is_file())
self.assertFalse((self.root / ".workspace/tests").exists())
def add_multi_instance_suite(self):
suite = self.root / "tests/integration/multi-instance" suite = self.root / "tests/integration/multi-instance"
suite.mkdir(parents=True) suite.mkdir(parents=True)
(suite / "go.mod").write_text("module example.com/multi\n\ngo 1.23\n") (suite / "go.mod").write_text("module example.com/multi\n\ngo 1.23\n")
(suite / "binary_test.go").write_text('package multi\nimport("os";"os/exec";"testing")\nfunc TestBinary(t *testing.T){b,e:=exec.Command(os.Getenv("AGENT_BIN")).Output();if e!=nil||string(b)!="fixed-binary"{t.Fatalf("%s %v",b,e)}}\n') (suite / "binary_test.go").write_text('package multi\nimport("os";"os/exec";"testing")\nfunc TestBinary(t *testing.T){b,e:=exec.Command(os.Getenv("AGENT_BIN")).Output();if e!=nil||string(b)!="fixed-binary"{t.Fatalf("%s %v",b,e)}}\n')
self.call(self.root, "make", "test-multi-instance", env=self.env | {"AGENT_PG_TEST_DSN": "fixture-dsn"}) return suite
def add_go_byproduct_fixture(self, fail=False):
(self.backend / ".gitignore").write_text("__pycache__/\n.fixture-output\n")
(self.backend / "fixture_module.py").write_text("VALUE = 'imported from Go'\n")
(self.backend / "byproduct_test.go").write_text('''package backend
import("os";"os/exec";"testing")
func TestByproducts(t *testing.T) {
if err := os.WriteFile(".fixture-output", []byte("generated by Go test"), 0600); err != nil {t.Fatal(err)}
cmd := exec.Command("python3", "-c", "import fixture_module")
cmd.Env = append(os.Environ(), "PYTHONDONTWRITEBYTECODE=", "PYTHONPYCACHEPREFIX=")
if out, err := cmd.CombinedOutput(); err != nil {t.Fatalf("%s %v", out, err)}
''' + (' t.Fatal("deliberate fixture failure")\n' if fail else '') + '}\n')
self.call(self.backend, "git", "add", ".")
self.call(self.backend, "git", "commit", "-m", "Add Go and Python byproducts fixture")
lock = json.loads((self.root / "components.lock.json").read_text())
lock["components"]["Agent-System-Backend"]["commit"] = self.call(self.backend, "git", "rev-parse", "HEAD").stdout.strip()
(self.root / "components.lock.json").write_text(json.dumps(lock))
self.call(self.root, "git", "add", "components.lock.json")
self.call(self.root, "git", "commit", "-m", "Pin byproducts fixture")
self.call(self.root, "python3", "scripts/workspace.py", "--root", str(self.root), "prepare")
return self.root / ".workspace/Agent-System-Backend"
def source_snapshot(self, child):
return {"head": self.call(child, "git", "rev-parse", "HEAD").stdout,
"branch": self.call(child, "git", "branch", "--show-current").stdout,
"index": (child / ".git/index").read_bytes(),
"status": self.call(child, "git", "status", "--porcelain", "--untracked-files=all").stdout,
"ignored": self.call(child, "git", "ls-files", "--others", "--ignored", "--exclude-standard").stdout,
"files": {path.relative_to(child).as_posix(): path.read_bytes() for path in child.rglob("*")
if path.is_file() and ".git" not in path.relative_to(child).parts}}
def test_fixed_go_checks_keep_source_clean_across_pg_and_multi_instance(self):
child = self.add_go_byproduct_fixture()
suite = self.add_multi_instance_suite()
before = self.source_snapshot(child)
env = self.env | {"AGENT_PG_TEST_DSN": "fixture-dsn", "AGENT_REDIS_TEST_ADDR": "fixture-redis"}
self.call(self.root, "make", "test-backend-pg", env=env)
generated = self.call(child, "git", "ls-files", "--others", "--ignored", "--exclude-standard").stdout
print("fixed checkout byproducts after PostgreSQL fixture: " + repr(generated), flush=True)
self.call(self.root, "make", "test-multi-instance", env=env)
self.assertEqual(self.source_snapshot(child), before)
self.assertTrue((suite / ".build/agent-system").is_file()) self.assertTrue((suite / ".build/agent-system").is_file())
self.assertFalse((self.root / ".workspace/tests").exists())
def test_fixed_go_failure_is_reported_without_polluting_source(self):
child = self.add_go_byproduct_fixture(fail=True)
before = self.source_snapshot(child)
result = self.call(self.root, "make", "test-backend", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("deliberate fixture failure", result.stdout)
self.assertIn("local component command failed (1)", result.stdout)
self.assertEqual(self.source_snapshot(child), before)
def test_fixed_go_checks_refuse_and_preserve_owner_changes_and_ignored_files(self):
child = self.add_go_byproduct_fixture()
for kind in ("unstaged", "staged", "ignored"):
with self.subTest(kind=kind):
if kind == "ignored":
(child / ".fixture-output").write_text("owner's unique ignored file")
else:
(child / "source.go").write_text('package backend\nfunc Value() string {return "owner-edit"}\n')
if kind == "staged":
self.call(child, "git", "add", "source.go")
before = self.source_snapshot(child)
result = self.call(self.root, "make", "test-backend", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("dirty or ignored unique files", result.stdout)
self.assertEqual(self.source_snapshot(child), before)
# This fixture owns these files and its index; reset only between cases.
if kind != "ignored":
self.call(child, "git", "restore", "--source=HEAD", "--staged", "--worktree", "source.go")
def test_pg_and_multi_instance_prerequisites_still_refuse_before_preparing_sources(self):
for target, supplied in (("test-backend-pg", {}),
("test-backend-pg", {"AGENT_PG_TEST_DSN": "fixture-dsn"}),
("test-multi-instance", {})):
with self.subTest(target=target, supplied=supplied):
env = {key: value for key, value in self.env.items()
if key not in ("AGENT_PG_TEST_DSN", "AGENT_REDIS_TEST_ADDR")}
result = self.call(self.root, "make", target, check=False, env=env | supplied)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertFalse((self.root / ".workspace").exists())
def test_new_pin_gate_validates_fixed_sources_and_rejects_local_replaces(self): def test_new_pin_gate_validates_fixed_sources_and_rejects_local_replaces(self):
self.call(self.root, "python3", "scripts/workspace.py", "--root", str(self.root), "prepare") self.call(self.root, "python3", "scripts/workspace.py", "--root", str(self.root), "prepare")
+7
View File
@@ -90,6 +90,13 @@ class ComponentWorkflowFixtureTests(unittest.TestCase):
class DependencyCITests(unittest.TestCase): class DependencyCITests(unittest.TestCase):
def test_boundary_gate_refuses_missing_ripgrep_before_resolving_sources(self):
with tempfile.TemporaryDirectory(prefix="boundary-no-tools-") as directory:
result = subprocess.run([shutil.which("bash"), str(WORKFLOW.parents[2] / "scripts/lint-boundaries.sh")],
cwd=directory, env=os.environ | {"PATH": directory}, capture_output=True, text=True)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("requires ripgrep", result.stderr)
def test_privileged_acquisition_finishes_before_candidate_execution_in_each_job(self): def test_privileged_acquisition_finishes_before_candidate_execution_in_each_job(self):
content = WORKFLOW.read_text() content = WORKFLOW.read_text()
jobs = re.split(r"^ ([A-Za-z0-9_-]+):\n", content, flags=re.M) jobs = re.split(r"^ ([A-Za-z0-9_-]+):\n", content, flags=re.M)