G7 증명과 G8 clean-head 승격 준비
This commit is contained in:
parent
94c681d450
commit
5221f79e3f
52 changed files with 6876 additions and 506 deletions
|
|
@ -2,7 +2,8 @@
|
|||
"""Fail-closed G8 release agent for the isolated Vignette NAS preview.
|
||||
|
||||
The default is a read-only dry run. ``--execute`` is intentionally narrow:
|
||||
it accepts only the dedicated Tailnet preview, builds a clean HEAD+patch
|
||||
it accepts only the dedicated Tailnet preview, builds either the default
|
||||
verified HEAD+patch candidate or an explicitly requested clean-HEAD archive
|
||||
candidate, runs release/API/Web/session gates, snapshots the current API and
|
||||
Web images, promotes only a changed deterministic SHA, and rolls back the
|
||||
previous images when any post-promotion proof fails.
|
||||
|
|
@ -54,6 +55,7 @@ SSOT_REQUIRED_PATHS = (
|
|||
# operator surface has exercised its browser contract. Keep this list explicit
|
||||
# so a newly added goal cannot disappear behind a broad directory glob.
|
||||
RELEASE_UI_E2E_SPECS = (
|
||||
"e2e/insecure-context-uuid.spec.ts",
|
||||
"e2e/session-layout.spec.ts",
|
||||
"e2e/session-persistence.spec.ts",
|
||||
"e2e/self-directed-learning-loop.spec.ts",
|
||||
|
|
@ -66,6 +68,36 @@ RELEASE_UI_E2E_SPECS = (
|
|||
"e2e/multimodal-alliance.spec.ts",
|
||||
"e2e/continuous-improvement-admin.spec.ts",
|
||||
)
|
||||
# The UUID source-contract spec intentionally imports ``/src/lib/uuid.ts`` and
|
||||
# scans the local source tree, so it belongs to the Vite candidate gate above.
|
||||
# A production NAS build serves the SPA HTML for ``/src/*``. The postdeploy
|
||||
# gate therefore exercises the exact built review routes that previously
|
||||
# crashed on insecure HTTP, while retaining every production-origin-compatible
|
||||
# release spec. The real returned-practice DB closed loop remains an explicit
|
||||
# disposable-DB-only gate and must never be silently skipped against NAS.
|
||||
POSTDEPLOY_NAS_E2E_SPECS = (
|
||||
"e2e/session-layout.spec.ts",
|
||||
"e2e/session-persistence.spec.ts",
|
||||
"e2e/self-directed-learning-loop.spec.ts",
|
||||
"e2e/alliance-pulse.spec.ts",
|
||||
"e2e/outcome-trajectory.spec.ts",
|
||||
"e2e/rupture-repair.spec.ts",
|
||||
"e2e/deliberate-practice.spec.ts",
|
||||
"e2e/calibration-transfer.spec.ts",
|
||||
"e2e/supervision-research.spec.ts",
|
||||
"e2e/multimodal-alliance.spec.ts",
|
||||
"e2e/continuous-improvement-admin.spec.ts",
|
||||
)
|
||||
POSTDEPLOY_SOURCE_ONLY_E2E_SPECS = ("e2e/insecure-context-uuid.spec.ts",)
|
||||
POSTDEPLOY_DISPOSABLE_DB_E2E_SPECS = (
|
||||
"e2e/returned-practice-db-closed-loop.spec.ts",
|
||||
)
|
||||
SOURCE_ONLY_E2E_PORT = 5198
|
||||
RELEASE_BROWSER_PROJECTS = (
|
||||
"chromium-desktop",
|
||||
"chromium-mobile",
|
||||
"chromium-single-run",
|
||||
)
|
||||
RELEASE_E2E_TIMEOUT_SECONDS = 15 * 60
|
||||
|
||||
# Existing preview volumes do not replay docker-entrypoint-initdb.d. These
|
||||
|
|
@ -100,8 +132,10 @@ REQUIRED_OPENAPI_PATHS = {
|
|||
|
||||
IMAGE_ID_RE = re.compile(r"^sha256:[a-f0-9]{64}$")
|
||||
SHA256_RE = re.compile(r"^[a-f0-9]{64}$")
|
||||
GIT_OBJECT_RE = re.compile(r"^(?:[a-f0-9]{40}|[a-f0-9]{64})$")
|
||||
SSH_TARGET_RE = re.compile(r"^(?:[A-Za-z0-9._-]+@)?100\.116\.83\.60$")
|
||||
RELEASE_KEY_RE = re.compile(r"^[a-f0-9]{16}-\d{8}T\d{6}Z-\d+$")
|
||||
SOURCE_MODES = ("patch", "clean-head")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -147,6 +181,7 @@ class ReleaseAgentConfig:
|
|||
repo_root: Path = REPO_ROOT
|
||||
manifest_path: Path = DEFAULT_MANIFEST
|
||||
hunk_map_path: Path = DEFAULT_HUNK_MAP
|
||||
source_mode: str = "patch"
|
||||
execute: bool = False
|
||||
milestone: MaterialMilestone | None = None
|
||||
target: DeploymentTarget = NAS_PREVIEW_TARGET
|
||||
|
|
@ -215,6 +250,8 @@ class RuntimeProbe(Protocol):
|
|||
class CandidateManager(Protocol):
|
||||
def materialize(self, base_commit: str, patch_path: str) -> Path: ...
|
||||
|
||||
def materialize_archive(self, archive_path: Path, expected_sha256: str) -> Path: ...
|
||||
|
||||
def cleanup(self) -> None: ...
|
||||
|
||||
|
||||
|
|
@ -226,6 +263,16 @@ def sha256_bytes(value: bytes) -> str:
|
|||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> tuple[str, int]:
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
with path.open("rb") as source:
|
||||
while chunk := source.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
size += len(chunk)
|
||||
return digest.hexdigest(), size
|
||||
|
||||
|
||||
def validate_target(target: DeploymentTarget) -> None:
|
||||
if target != NAS_PREVIEW_TARGET:
|
||||
raise ValueError(
|
||||
|
|
@ -237,6 +284,15 @@ def validate_target(target: DeploymentTarget) -> None:
|
|||
raise ValueError("isolated NAS preview root must be an absolute canonical path")
|
||||
|
||||
|
||||
def validate_source_repo_root(repo_root: Path) -> None:
|
||||
if not repo_root.is_absolute():
|
||||
raise StageFailure("source_repository", "source repository root must be absolute")
|
||||
if not repo_root.exists():
|
||||
raise StageFailure("source_repository", "source repository root does not exist")
|
||||
if not repo_root.is_dir():
|
||||
raise StageFailure("source_repository", "source repository root is not a directory")
|
||||
|
||||
|
||||
def validate_required_release_payload(manifest: dict[str, Any]) -> None:
|
||||
classifications = manifest.get("classifications")
|
||||
related = classifications.get("related") if isinstance(classifications, dict) else None
|
||||
|
|
@ -437,6 +493,45 @@ class CleanCandidateManager:
|
|||
_normalize_candidate_file_to_lf(shell_script, "shell script")
|
||||
return root
|
||||
|
||||
def materialize_archive(self, archive_path: Path, expected_sha256: str) -> Path:
|
||||
"""Extract an exact clean-HEAD archive without applying a patch."""
|
||||
|
||||
if not SHA256_RE.fullmatch(expected_sha256):
|
||||
raise StageFailure("candidate_archive_binding", "expected archive SHA is invalid")
|
||||
try:
|
||||
actual_sha256, _ = sha256_file(archive_path)
|
||||
except OSError as exc:
|
||||
raise StageFailure("candidate_archive_binding", str(exc)) from exc
|
||||
if actual_sha256 != expected_sha256:
|
||||
raise StageFailure(
|
||||
"candidate_archive_binding",
|
||||
f"archive SHA drifted: expected={expected_sha256} actual={actual_sha256}",
|
||||
)
|
||||
|
||||
self._temp = tempfile.TemporaryDirectory(prefix="vignette-release-agent-")
|
||||
root = Path(self._temp.name) / "candidate"
|
||||
try:
|
||||
root.mkdir(parents=True)
|
||||
with tarfile.open(archive_path, "r:") as archive:
|
||||
_safe_extract(archive, root)
|
||||
generated_api = root / "apps/web/src/lib/api.gen.ts"
|
||||
if not generated_api.is_file():
|
||||
raise StageFailure("candidate_archive", "generated API contract is missing")
|
||||
# git archive stores canonical LF blobs, while a Windows checkout can
|
||||
# materialize this generated file with CRLF. openapi-typescript
|
||||
# --check compares bytes, so apply the same platform normalization as
|
||||
# patch-mode after the archive SHA has already been verified.
|
||||
_normalize_candidate_file_to_lf(generated_api, "generated API contract")
|
||||
for shell_script in sorted(root.rglob("*.sh")):
|
||||
_normalize_candidate_file_to_lf(shell_script, "shell script")
|
||||
except StageFailure:
|
||||
self.cleanup()
|
||||
raise
|
||||
except (OSError, tarfile.TarError) as exc:
|
||||
self.cleanup()
|
||||
raise StageFailure("candidate_archive", str(exc)) from exc
|
||||
return root
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self._temp is not None:
|
||||
self._temp.cleanup()
|
||||
|
|
@ -970,6 +1065,8 @@ class ReleaseAgent:
|
|||
self.runtime_probe = runtime_probe
|
||||
self.candidate_manager = candidate_manager
|
||||
self.stage_evidence: list[dict[str, Any]] = []
|
||||
self.source_evidence: dict[str, Any] | None = None
|
||||
self._source_temp: tempfile.TemporaryDirectory[str] | None = None
|
||||
|
||||
def _run(
|
||||
self,
|
||||
|
|
@ -1089,6 +1186,152 @@ class ReleaseAgent:
|
|||
raise StageFailure("release_determinism", "builder patch SHA is invalid")
|
||||
return reports[1]
|
||||
|
||||
def _clean_head_identity(self, suffix: str) -> tuple[str, str]:
|
||||
status = self._run(
|
||||
f"clean_head_worktree_{suffix}",
|
||||
["git", "status", "--porcelain=v1", "--untracked-files=no"],
|
||||
cwd=self.config.repo_root,
|
||||
timeout=120,
|
||||
)
|
||||
if status.stdout.strip():
|
||||
raise StageFailure(
|
||||
"clean_head_worktree",
|
||||
"tracked worktree/index changes are forbidden in clean-head mode",
|
||||
)
|
||||
|
||||
head_result = self._run(
|
||||
f"clean_head_head_{suffix}",
|
||||
["git", "rev-parse", "--verify", "HEAD"],
|
||||
cwd=self.config.repo_root,
|
||||
timeout=120,
|
||||
)
|
||||
tree_result = self._run(
|
||||
f"clean_head_tree_{suffix}",
|
||||
["git", "rev-parse", "--verify", "HEAD^{tree}"],
|
||||
cwd=self.config.repo_root,
|
||||
timeout=120,
|
||||
)
|
||||
head = head_result.stdout.strip()
|
||||
tree = tree_result.stdout.strip()
|
||||
if not GIT_OBJECT_RE.fullmatch(head):
|
||||
raise StageFailure("clean_head_identity", "Git HEAD is invalid")
|
||||
if not GIT_OBJECT_RE.fullmatch(tree):
|
||||
raise StageFailure("clean_head_identity", "Git tree is invalid")
|
||||
return head, tree
|
||||
|
||||
def _validate_clean_head_worktree_root(self) -> None:
|
||||
result = self._run(
|
||||
"clean_head_repository",
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
cwd=self.config.repo_root,
|
||||
timeout=120,
|
||||
)
|
||||
reported_root = result.stdout.strip()
|
||||
if not reported_root:
|
||||
raise StageFailure("clean_head_repository", "Git worktree root is missing")
|
||||
try:
|
||||
actual_root = Path(reported_root).resolve(strict=True)
|
||||
expected_root = self.config.repo_root.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise StageFailure("clean_head_repository", str(exc)) from exc
|
||||
if actual_root != expected_root:
|
||||
raise StageFailure(
|
||||
"clean_head_repository",
|
||||
f"source repository is not the Git worktree root: {actual_root}",
|
||||
)
|
||||
|
||||
def _build_clean_head_source_twice(self) -> dict[str, Any]:
|
||||
self._validate_clean_head_worktree_root()
|
||||
before_head, before_tree = self._clean_head_identity("before")
|
||||
self._source_temp = tempfile.TemporaryDirectory(prefix="vignette-release-source-")
|
||||
source_root = Path(self._source_temp.name)
|
||||
archives: list[Path] = []
|
||||
archive_reports: list[dict[str, Any]] = []
|
||||
for run_number in (1, 2):
|
||||
archive_path = source_root / f"clean-head-{run_number}.tar"
|
||||
self._run(
|
||||
f"clean_head_archive_run_{run_number}",
|
||||
[
|
||||
"git",
|
||||
"archive",
|
||||
"--format=tar",
|
||||
"--output",
|
||||
str(archive_path),
|
||||
before_head,
|
||||
],
|
||||
cwd=self.config.repo_root,
|
||||
timeout=900,
|
||||
)
|
||||
try:
|
||||
archive_sha256, archive_size = sha256_file(archive_path)
|
||||
except OSError as exc:
|
||||
raise StageFailure("clean_head_archive", str(exc)) from exc
|
||||
archives.append(archive_path)
|
||||
archive_reports.append(
|
||||
{
|
||||
"sha256": archive_sha256,
|
||||
"bytes": archive_size,
|
||||
}
|
||||
)
|
||||
|
||||
after_head, after_tree = self._clean_head_identity("after")
|
||||
if (before_head, before_tree) != (after_head, after_tree):
|
||||
raise StageFailure(
|
||||
"clean_head_identity",
|
||||
"Git HEAD/tree changed while clean-head archives were built",
|
||||
)
|
||||
if archive_reports[0] != archive_reports[1]:
|
||||
raise StageFailure(
|
||||
"clean_head_determinism",
|
||||
f"two clean-head archives differ: {archive_reports}",
|
||||
)
|
||||
archive_sha256 = archive_reports[1]["sha256"]
|
||||
if not SHA256_RE.fullmatch(archive_sha256):
|
||||
raise StageFailure("clean_head_determinism", "archive SHA is invalid")
|
||||
return {
|
||||
"source_mode": "clean-head",
|
||||
"head": after_head,
|
||||
"tree": after_tree,
|
||||
"archive_sha256": archive_sha256,
|
||||
"archive_bytes": archive_reports[1]["bytes"],
|
||||
"archive_path": archives[1],
|
||||
"deterministic_runs": 2,
|
||||
}
|
||||
|
||||
def _prepare_source(self) -> tuple[dict[str, Any], str]:
|
||||
if self.config.source_mode == "patch":
|
||||
release = self._build_release_twice()
|
||||
self._check_manifest_binding(release)
|
||||
self.source_evidence = {
|
||||
"repo_root": str(self.config.repo_root),
|
||||
"base_commit": release["base_commit"],
|
||||
"patch": {
|
||||
"sha256": release["patch_sha256"],
|
||||
"bytes": release["patch_bytes"],
|
||||
"files": release["patch_files"],
|
||||
"output": release["output_patch"],
|
||||
"deterministic_runs": 2,
|
||||
},
|
||||
}
|
||||
return release, release["patch_sha256"]
|
||||
if self.config.source_mode == "clean-head":
|
||||
release = self._build_clean_head_source_twice()
|
||||
self.source_evidence = {
|
||||
"repo_root": str(self.config.repo_root),
|
||||
"head": release["head"],
|
||||
"tree": release["tree"],
|
||||
"archive": {
|
||||
"sha256": release["archive_sha256"],
|
||||
"bytes": release["archive_bytes"],
|
||||
"deterministic_runs": release["deterministic_runs"],
|
||||
},
|
||||
}
|
||||
return release, release["archive_sha256"]
|
||||
raise StageFailure(
|
||||
"configuration",
|
||||
f"unsupported source mode: {self.config.source_mode!r}",
|
||||
)
|
||||
|
||||
def _check_manifest_binding(self, release: dict[str, Any]) -> None:
|
||||
result = self._run(
|
||||
"release_manifest",
|
||||
|
|
@ -1169,6 +1412,33 @@ class ReleaseAgent:
|
|||
self._run("web_typecheck", ["npm.cmd", "run", "typecheck"], cwd=web, timeout=300)
|
||||
self._run("web_build", ["npm.cmd", "run", "build"], cwd=web, timeout=600)
|
||||
|
||||
# This spec imports /src/lib/uuid.ts and therefore must run against a
|
||||
# source Vite server, not the production Compose build (which correctly
|
||||
# serves the SPA document for /src/*). CI=1 prevents reuse of an
|
||||
# unrelated long-lived Vite process on the workstation.
|
||||
source_e2e_env = os.environ.copy()
|
||||
source_e2e_env.pop("PLAYWRIGHT_BASE_URL", None)
|
||||
source_e2e_env.pop("PLAYWRIGHT_SKIP_WEB_SERVER", None)
|
||||
source_e2e_env["PLAYWRIGHT_HOST"] = "127.0.0.1"
|
||||
source_e2e_env["PLAYWRIGHT_PORT"] = str(SOURCE_ONLY_E2E_PORT)
|
||||
source_e2e_env["CI"] = "1"
|
||||
self._run(
|
||||
"source_insecure_context_e2e",
|
||||
[
|
||||
"node.exe",
|
||||
str(web / "node_modules/@playwright/test/cli.js"),
|
||||
"test",
|
||||
*POSTDEPLOY_SOURCE_ONLY_E2E_SPECS,
|
||||
"--project=chromium-desktop",
|
||||
"--project=chromium-mobile",
|
||||
"--workers=1",
|
||||
"--reporter=line",
|
||||
],
|
||||
cwd=web,
|
||||
env=source_e2e_env,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
stack_attempted = False
|
||||
stack_env: Path | None = None
|
||||
project = candidate_compose_project(desired_sha)
|
||||
|
|
@ -1207,7 +1477,7 @@ class ReleaseAgent:
|
|||
"node.exe",
|
||||
str(web / "node_modules/@playwright/test/cli.js"),
|
||||
"test",
|
||||
*RELEASE_UI_E2E_SPECS,
|
||||
*POSTDEPLOY_NAS_E2E_SPECS,
|
||||
"--project=chromium-desktop",
|
||||
"--project=chromium-mobile",
|
||||
"--project=chromium-single-run",
|
||||
|
|
@ -1288,20 +1558,29 @@ class ReleaseAgent:
|
|||
"node.exe",
|
||||
str(web / "node_modules/@playwright/test/cli.js"),
|
||||
"test",
|
||||
"e2e/session-persistence.spec.ts",
|
||||
"--project=chromium-single-run",
|
||||
*POSTDEPLOY_NAS_E2E_SPECS,
|
||||
*(f"--project={project}" for project in RELEASE_BROWSER_PROJECTS),
|
||||
"--workers=1",
|
||||
"--grep",
|
||||
"persists the browser SSE stream into DB-backed review",
|
||||
"--reporter=line",
|
||||
],
|
||||
cwd=web,
|
||||
env=env,
|
||||
timeout=300,
|
||||
timeout=RELEASE_E2E_TIMEOUT_SECONDS,
|
||||
)
|
||||
return {
|
||||
"status": "passed",
|
||||
"test": "session-persistence: browser SSE stream -> DB-backed review",
|
||||
"base_url": self.config.target.base_url,
|
||||
"specs": list(POSTDEPLOY_NAS_E2E_SPECS),
|
||||
"projects": list(RELEASE_BROWSER_PROJECTS),
|
||||
"source_only_candidate_specs": list(POSTDEPLOY_SOURCE_ONLY_E2E_SPECS),
|
||||
"separate_disposable_db_specs": list(POSTDEPLOY_DISPOSABLE_DB_E2E_SPECS),
|
||||
"uuid_runtime_route_specs": [
|
||||
"e2e/session-persistence.spec.ts",
|
||||
"e2e/self-directed-learning-loop.spec.ts",
|
||||
"e2e/rupture-repair.spec.ts",
|
||||
"e2e/deliberate-practice.spec.ts",
|
||||
"e2e/calibration-transfer.spec.ts",
|
||||
],
|
||||
"physical_microphone_used": False,
|
||||
"stdout_sha256": sha256_bytes(result.stdout.encode("utf-8")),
|
||||
}
|
||||
|
|
@ -1312,6 +1591,8 @@ class ReleaseAgent:
|
|||
"captured_at": utc_now(),
|
||||
"ok": False,
|
||||
"mode": "execute" if self.config.execute else "dry-run",
|
||||
"source_mode": self.config.source_mode,
|
||||
"source": self.source_evidence,
|
||||
"target": asdict(self.config.target),
|
||||
"milestone": asdict(self.config.milestone) if self.config.milestone else None,
|
||||
"desired_sha": desired_sha,
|
||||
|
|
@ -1342,10 +1623,9 @@ class ReleaseAgent:
|
|||
desired_sha: str | None = None
|
||||
try:
|
||||
validate_target(self.config.target)
|
||||
validate_source_repo_root(self.config.repo_root)
|
||||
validate_milestone(self.config.milestone)
|
||||
release = self._build_release_twice()
|
||||
desired_sha = release["patch_sha256"]
|
||||
self._check_manifest_binding(release)
|
||||
release, desired_sha = self._prepare_source()
|
||||
active_state = self.deployment.read_active_state()
|
||||
active_sha = active_state.get("active_sha") if active_state else None
|
||||
if active_sha is not None and (
|
||||
|
|
@ -1391,9 +1671,14 @@ class ReleaseAgent:
|
|||
"remote release-agent state is untracked; adopt it only after an image/Compose rollback snapshot audit",
|
||||
)
|
||||
|
||||
candidate = self.candidate_manager.materialize(
|
||||
release["base_commit"], release["output_patch"]
|
||||
)
|
||||
if self.config.source_mode == "clean-head":
|
||||
candidate = self.candidate_manager.materialize_archive(
|
||||
release["archive_path"], release["archive_sha256"]
|
||||
)
|
||||
else:
|
||||
candidate = self.candidate_manager.materialize(
|
||||
release["base_commit"], release["output_patch"]
|
||||
)
|
||||
self._run_candidate_gates(candidate, desired_sha)
|
||||
snapshot = self.deployment.snapshot()
|
||||
self._validate_snapshot_binding(active_state, snapshot)
|
||||
|
|
@ -1472,6 +1757,9 @@ class ReleaseAgent:
|
|||
finally:
|
||||
if candidate is not None:
|
||||
self.candidate_manager.cleanup()
|
||||
if self._source_temp is not None:
|
||||
self._source_temp.cleanup()
|
||||
self._source_temp = None
|
||||
|
||||
|
||||
def load_milestone(path: Path) -> MaterialMilestone:
|
||||
|
|
@ -1502,6 +1790,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
parser.add_argument("--milestone", type=Path, required=True)
|
||||
parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST)
|
||||
parser.add_argument("--hunk-map", type=Path, default=DEFAULT_HUNK_MAP)
|
||||
parser.add_argument(
|
||||
"--source-mode",
|
||||
choices=SOURCE_MODES,
|
||||
default="patch",
|
||||
help="Release source: verified patch (default) or exact clean Git HEAD archive",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-repo-root",
|
||||
type=Path,
|
||||
default=REPO_ROOT,
|
||||
help="Absolute Git worktree root used as the release source",
|
||||
)
|
||||
parser.add_argument("--nas-env-file", type=Path, default=DEFAULT_NAS_ENV)
|
||||
parser.add_argument("--evidence-out", type=Path)
|
||||
parser.add_argument("--ssh-target", help="Required for live state inspection/execution")
|
||||
|
|
@ -1560,9 +1860,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
else:
|
||||
deployment = StaticDeploymentState(args.current_deployment_sha)
|
||||
config = ReleaseAgentConfig(
|
||||
repo_root=REPO_ROOT,
|
||||
repo_root=args.source_repo_root,
|
||||
manifest_path=args.manifest,
|
||||
hunk_map_path=args.hunk_map,
|
||||
source_mode=args.source_mode,
|
||||
execute=args.execute,
|
||||
milestone=milestone,
|
||||
target=NAS_PREVIEW_TARGET,
|
||||
|
|
@ -1575,7 +1876,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
runner=runner,
|
||||
deployment=deployment,
|
||||
runtime_probe=HttpRuntimeProbe(NAS_PREVIEW_TARGET.base_url),
|
||||
candidate_manager=CleanCandidateManager(REPO_ROOT, runner),
|
||||
candidate_manager=CleanCandidateManager(args.source_repo_root, runner),
|
||||
)
|
||||
report = agent.run()
|
||||
if args.json:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue