feat: govern merged NC01 worktrees

This commit is contained in:
pikastech
2026-07-21 09:44:04 +02:00
parent 4bc272f47d
commit c001309ed5
7 changed files with 323 additions and 8 deletions
+26
View File
@@ -120,6 +120,32 @@ rm -rf -- /tmp/unidesk-cli-output
- provider host 统一使用 `bun scripts/cli.ts gc remote <providerId> ...`
- 远端长任务必须异步执行,并用 `status --job-id` 短查询,不得维持长 SSH 会话。
### 已合并 worktree
先读取 YAML 策略并只生成计划:
```bash
bun scripts/cli.ts gc remote <providerId> merged-worktrees plan
```
- 仅枚举 Git 登记且位于 `workspaceRoots` 的 worktree
- dirty、活跃、近期、未被 `baseRef` 吸收、cherry 检查超时和主 worktree 必须显示为 protected
- 手动入口与 policy timer 复用同一筛选器,批量上限分别读取 YAML 的手动和自动字段;
- 执行前逐项重新计划,只允许 `git worktree remove`,不得删除 branch 或使用 `rm -rf`
复核候选后才允许执行:
```bash
bun scripts/cli.ts gc remote <providerId> merged-worktrees run --confirm
```
周期 stage 通过以下入口核对 YAML 字段、stage 和 `inSync`
```bash
bun scripts/cli.ts gc remote <providerId> policy plan
bun scripts/cli.ts gc remote <providerId> policy status
```
### 内存分布与 Kubernetes 留存
先用紧凑 overview 归因真实内存压力:
+14
View File
@@ -154,6 +154,19 @@ gc:
remote:
targets:
NC01:
mergedWorktrees:
enabled: true
mainRoot: /root/unidesk
workspaceRoots:
- /root/unidesk/.worktree
baseRef: origin/master
minAgeHours: 24
scanBudgetMs: 20000
cherryCheckTimeoutSeconds: 2
manualMaxDeletePerRun: 20
automatic:
enabled: true
maxDeletePerRun: 5
registryRetention:
enabled: true
configRef: config/hwlab-node-control-plane.yaml#nodes.NC01.registry
@@ -363,6 +376,7 @@ gc:
includeHostContainerdCache: false
includeLocalPathOrphans: false
includeKubernetesObjectRetention: true
includeMergedWorktrees: true
pvcAttribution:
namespaces:
- agentrun-ci
+202
View File
@@ -0,0 +1,202 @@
import os
import subprocess
import time
def _command(args, timeout_seconds):
started = time.time()
try:
result = subprocess.run(args, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout_seconds)
return {
"exitCode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"elapsedMs": int((time.time() - started) * 1000),
}
except subprocess.TimeoutExpired as exc:
return {
"exitCode": 124,
"stdout": exc.stdout or "" if isinstance(exc.stdout, str) else "",
"stderr": exc.stderr or "" if isinstance(exc.stderr, str) else "",
"elapsedMs": int((time.time() - started) * 1000),
"timedOut": True,
}
def _worktree_entries(main_root):
result = _command(["git", "-C", main_root, "worktree", "list", "--porcelain"], 10)
if result["exitCode"] != 0:
raise RuntimeError(result["stderr"].strip() or "git worktree list failed")
entries = []
current = None
for raw_line in result["stdout"].splitlines() + [""]:
line = raw_line.strip()
if not line:
if current:
entries.append(current)
current = None
continue
key, _, value = line.partition(" ")
if key == "worktree":
current = {"path": os.path.realpath(value), "head": None, "branch": None, "bare": False}
elif current is not None and key == "HEAD":
current["head"] = value
elif current is not None and key == "branch":
current["branch"] = value
elif current is not None and key == "bare":
current["bare"] = True
return entries
def _inside_roots(path, roots):
return any(path.startswith(root.rstrip("/") + "/") for root in roots)
def _recent(path, cutoff, deadline):
for root, dirs, files in os.walk(path):
if time.time() > deadline:
raise TimeoutError("scan-budget-exceeded")
for name in dirs + files:
try:
if os.lstat(os.path.join(root, name)).st_mtime >= cutoff:
return True
except OSError:
return True
try:
return os.lstat(path).st_mtime >= cutoff
except OSError:
return True
def _open_or_current(path):
resolved = os.path.realpath(path)
prefix = resolved.rstrip("/") + "/"
try:
pids = [name for name in os.listdir("/proc") if name.isdigit()]
except OSError:
return True
for pid in pids:
base = os.path.join("/proc", pid)
for name in ["cwd", "root"]:
try:
target = os.path.realpath(os.readlink(os.path.join(base, name)))
except OSError:
continue
if target == resolved or target.startswith(prefix):
return True
return False
def _clean(path):
result = _command(["git", "-C", path, "status", "--porcelain", "--untracked-files=all"], 15)
return result["exitCode"] == 0 and not result["stdout"].strip(), result
def _absorbed(main_root, base_ref, head, timeout_seconds):
ancestor = _command(["git", "-C", main_root, "merge-base", "--is-ancestor", head, base_ref], timeout_seconds)
if ancestor["exitCode"] == 0:
return True, "ancestor", 0
cherry = _command(["git", "-C", main_root, "cherry", base_ref, head], timeout_seconds)
if cherry["exitCode"] != 0:
raise RuntimeError(cherry["stderr"].strip() or "git cherry failed")
unabsorbed = [line for line in cherry["stdout"].splitlines() if line.startswith("+")]
return len(unabsorbed) == 0, "cherry-equivalent" if not unabsorbed else "unmerged", len(unabsorbed)
def merged_worktree_plan(config, automatic=False):
cfg = config.get("mergedWorktrees") if isinstance(config, dict) else None
if not isinstance(cfg, dict) or cfg.get("enabled") is not True:
return {"id": "merged-worktrees", "ok": True, "skipped": True, "candidates": [], "protected": []}
automatic_cfg = cfg.get("automatic") if isinstance(cfg.get("automatic"), dict) else {}
if automatic and automatic_cfg.get("enabled") is not True:
return {"id": "merged-worktrees", "ok": True, "skipped": True, "candidates": [], "protected": []}
main_root = os.path.realpath(str(cfg.get("mainRoot") or ""))
roots = [os.path.realpath(item) for item in (cfg.get("workspaceRoots") or []) if isinstance(item, str) and item.startswith("/")]
base_ref = str(cfg.get("baseRef") or "")
min_age_hours = float(cfg.get("minAgeHours") or 0)
scan_budget_ms = max(1, int(cfg.get("scanBudgetMs") or 1))
timeout_seconds = max(1, int(cfg.get("cherryCheckTimeoutSeconds") or 1))
limit_key = "maxDeletePerRun" if automatic else "manualMaxDeletePerRun"
limit_source = automatic_cfg if automatic else cfg
limit = max(1, int(limit_source.get(limit_key) or 1))
if not main_root.startswith("/") or not roots or not base_ref:
raise RuntimeError("mergedWorktrees requires absolute mainRoot/workspaceRoots and baseRef")
deadline = time.time() + scan_budget_ms / 1000.0
cutoff = time.time() - min_age_hours * 3600.0
candidates = []
protected = []
for entry in _worktree_entries(main_root):
path = entry["path"]
if not _inside_roots(path, roots):
continue
reason = None
detail = None
if time.time() > deadline:
reason = "scan-budget-exceeded"
elif entry.get("bare") or path == main_root or _open_or_current(path):
reason = "active-worktree"
elif not os.path.isdir(path) or os.path.islink(path):
reason = "missing-or-nonplain-worktree"
else:
try:
if _recent(path, cutoff, deadline):
reason = "recent-worktree"
except TimeoutError:
reason = "scan-budget-exceeded"
if reason is None:
clean, clean_result = _clean(path)
if not clean:
reason = "dirty-worktree" if clean_result["exitCode"] == 0 else "worktree-status-error"
detail = clean_result["stderr"].strip()[-400:]
if reason is None and not entry.get("head"):
reason = "worktree-head-missing"
if reason is None:
try:
absorbed, merge_mode, unabsorbed = _absorbed(main_root, base_ref, entry["head"], timeout_seconds)
if not absorbed:
reason = "unmerged-worktree"
detail = "unabsorbedCommitCount=%d" % unabsorbed
except Exception as exc:
reason = "worktree-merge-check-error"
detail = str(exc)
if reason is not None:
protected.append({"path": path, "reason": reason, "detail": detail})
continue
candidates.append({"path": path, "head": entry["head"], "branch": entry.get("branch"), "mergeMode": merge_mode})
return {
"id": "merged-worktrees",
"ok": True,
"automatic": automatic,
"configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.mergedWorktrees" % config.get("providerId"),
"baseRef": base_ref,
"candidateCount": len(candidates),
"protectedCount": len(protected),
"limit": limit,
"candidates": candidates[:limit],
"protected": protected,
}
def run_merged_worktrees(config, automatic=False):
plan = merged_worktree_plan(config, automatic=automatic)
if plan.get("skipped"):
return plan
results = []
for candidate in plan["candidates"]:
refreshed = merged_worktree_plan(config, automatic=automatic)
safe_paths = {item["path"] for item in refreshed.get("candidates") or []}
path = candidate["path"]
if path not in safe_paths:
results.append({"path": path, "status": "protected-on-recheck"})
continue
result = _command(["git", "-C", os.path.realpath(config["mergedWorktrees"]["mainRoot"]), "worktree", "remove", path], 30)
results.append({"path": path, "status": "removed" if result["exitCode"] == 0 else "failed", "error": result["stderr"].strip()[-400:] or None})
return {
**plan,
"mutation": True,
"attemptedCount": len(results),
"removedCount": len([item for item in results if item["status"] == "removed"]),
"failedCount": len([item for item in results if item["status"] == "failed"]),
"results": results,
"ok": all(item["status"] != "failed" for item in results),
}
@@ -0,0 +1,42 @@
import importlib.util
import os
import subprocess
import tempfile
import unittest
MODULE_PATH = os.path.join(os.path.dirname(__file__), "gc-remote-merged-worktrees.py")
SPEC = importlib.util.spec_from_file_location("gc_remote_merged_worktrees", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
class MergedWorktreeTest(unittest.TestCase):
def test_plan_protects_dirty_and_selects_absorbed_clean_worktree(self):
with tempfile.TemporaryDirectory() as root:
main = os.path.join(root, "repo")
worktrees = os.path.join(main, ".worktree")
subprocess.run(["git", "init", "-b", "master", main], check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "-C", main, "config", "user.email", "test@example.com"], check=True)
subprocess.run(["git", "-C", main, "config", "user.name", "Test"], check=True)
with open(os.path.join(main, "base.txt"), "w", encoding="utf-8") as handle:
handle.write("base\n")
subprocess.run(["git", "-C", main, "add", "base.txt"], check=True)
subprocess.run(["git", "-C", main, "commit", "-m", "base"], check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "-C", main, "remote", "add", "origin", main], check=True)
subprocess.run(["git", "-C", main, "fetch", "origin", "master"], check=True, stdout=subprocess.DEVNULL)
os.makedirs(worktrees)
clean = os.path.join(worktrees, "clean")
dirty = os.path.join(worktrees, "dirty")
subprocess.run(["git", "-C", main, "worktree", "add", "-b", "clean-branch", clean, "master"], check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "-C", main, "worktree", "add", "-b", "dirty-branch", dirty, "master"], check=True, stdout=subprocess.DEVNULL)
with open(os.path.join(dirty, "dirty.txt"), "w", encoding="utf-8") as handle:
handle.write("dirty\n")
config = {"providerId": "TEST", "mergedWorktrees": {"enabled": True, "mainRoot": main, "workspaceRoots": [worktrees], "baseRef": "origin/master", "minAgeHours": 0, "scanBudgetMs": 5000, "cherryCheckTimeoutSeconds": 2, "manualMaxDeletePerRun": 10}}
plan = MODULE.merged_worktree_plan(config)
self.assertEqual([clean], [item["path"] for item in plan["candidates"]])
self.assertIn("dirty-worktree", [item["reason"] for item in plan["protected"]])
if __name__ == "__main__":
unittest.main()
+3 -1
View File
@@ -10,6 +10,8 @@ from datetime import datetime, timezone
# __UNIDESK_GC_REMOTE_POLICY_KUBERNETES_RETENTION_HELPERS__
# __UNIDESK_GC_REMOTE_POLICY_MERGED_WORKTREE_HELPERS__
def now_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -461,7 +463,7 @@ def main():
config_path = sys.argv[1] if len(sys.argv) > 1 else ""
config = load_config(config_path)
results = []
for fn in [run_journal, run_apt, run_tmp, run_tool_caches, run_web_observe, run_session_pvcs, run_k3s_images, run_host_containerd, run_local_path_orphans, run_kubernetes_object_retention_policy]:
for fn in [run_journal, run_apt, run_tmp, run_tool_caches, run_web_observe, run_session_pvcs, run_k3s_images, run_host_containerd, run_local_path_orphans, run_kubernetes_object_retention_policy, lambda value: run_merged_worktrees(value, automatic=True)]:
try:
results.append(fn(config))
except Exception as exc:
+14 -1
View File
@@ -25,11 +25,14 @@ CONTAINERD_CONFIG = REMOTE_TARGET.get("containerdImageCache") if isinstance(REMO
HOST_CONTAINERD_CONFIG = REMOTE_TARGET.get("hostContainerdCache") if isinstance(REMOTE_TARGET.get("hostContainerdCache"), dict) else {}
LOCAL_PATH_CONFIG = REMOTE_TARGET.get("localPathStorage") if isinstance(REMOTE_TARGET.get("localPathStorage"), dict) else {}
KUBERNETES_RETENTION_CONFIG = REMOTE_TARGET.get("kubernetesObjectRetention") if isinstance(REMOTE_TARGET.get("kubernetesObjectRetention"), dict) else {}
MERGED_WORKTREE_CONFIG = REMOTE_TARGET.get("mergedWorktrees") if isinstance(REMOTE_TARGET.get("mergedWorktrees"), dict) else {}
POLICY_TIMER_CONFIG = REMOTE_TARGET.get("policyTimer") if isinstance(REMOTE_TARGET.get("policyTimer"), dict) else {}
HOST_DOCKER_GC_CONFIG = REMOTE_TARGET.get("hostDockerGc") if isinstance(REMOTE_TARGET.get("hostDockerGc"), dict) else {}
REGISTRY_CONFIG = REMOTE_TARGET.get("registryRetention") if isinstance(REMOTE_TARGET.get("registryRetention"), dict) else {}
POLICY_RUNNER_SOURCE = base64.b64decode("__UNIDESK_GC_REMOTE_POLICY_RUNNER_BASE64__").decode("utf-8")
# __UNIDESK_GC_REMOTE_MERGED_WORKTREE_HELPERS__
TMP_PREFIX_ALLOWLIST = [
"hwlab-agent-",
"hwlab-cd-",
@@ -1945,6 +1948,7 @@ def render_remote_policy():
include_host_containerd = required_policy_bool("includeHostContainerdCache")
include_local_path_orphans = required_policy_bool("includeLocalPathOrphans")
include_kubernetes_retention = required_policy_bool("includeKubernetesObjectRetention")
include_merged_worktrees = required_policy_bool("includeMergedWorktrees")
state_dir = config_str(POLICY_TIMER_CONFIG, "stateDir", "/var/lib/unidesk-gc")
config_dir = config_str(POLICY_TIMER_CONFIG, "configDir", "/etc/unidesk-gc")
script_path = "/usr/local/sbin/%s.py" % unit_name
@@ -1973,6 +1977,7 @@ def render_remote_policy():
{**KUBERNETES_RETENTION_CONFIG, "enabled": True}
if include_kubernetes_retention else {"enabled": False}
),
"mergedWorktrees": ({**MERGED_WORKTREE_CONFIG, "enabled": True} if include_merged_worktrees else {"enabled": False}),
"agentrunSessionPvcs": POLICY_TIMER_CONFIG.get("agentrunSessionPvcs") if isinstance(POLICY_TIMER_CONFIG.get("agentrunSessionPvcs"), dict) else {"enabled": False},
}
stages = [
@@ -1986,6 +1991,7 @@ def render_remote_policy():
{"id": "host-containerd-orphans", "enabled": include_host_containerd, "risk": "medium", "mode": "ctr-metadata-empty-yaml-orphan-state"},
{"id": "local-path-orphans", "enabled": include_local_path_orphans, "risk": "medium", "mode": "pv-unreferenced-direct-child"},
{"id": "kubernetes-object-retention", "enabled": include_kubernetes_retention, "risk": "medium", "mode": "terminal-owner-cascade-bounded"},
{"id": "merged-worktrees", "enabled": include_merged_worktrees, "risk": "medium", "mode": "git-registered-clean-inactive-absorbed-bounded"},
]
service = "\n".join([
"[Unit]",
@@ -2036,6 +2042,7 @@ def render_remote_policy():
"includeHostContainerdCache": include_host_containerd,
"includeLocalPathOrphans": include_local_path_orphans,
"includeKubernetesObjectRetention": include_kubernetes_retention,
"includeMergedWorktrees": include_merged_worktrees,
"stages": stages,
"policyConfig": policy_config,
"script": POLICY_RUNNER_SOURCE,
@@ -2054,7 +2061,7 @@ def remote_policy_plan_payload(observed_at):
"observedAt": observed_at,
"configSource": "config/unidesk-cli.yaml#gc.remote.targets.%s.policyTimer" % PROVIDER_ID,
"enabled": config_bool(POLICY_TIMER_CONFIG, "enabled", False),
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "configPath", "statePath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeJournal", "includeTmp", "includeAptCache", "includeToolCaches", "includeWebObserveArtifacts", "includeK3sImageCache", "includeHostContainerdCache", "includeLocalPathOrphans", "includeKubernetesObjectRetention"]},
"timer": {key: rendered.get(key) for key in ["unitName", "scriptPath", "configPath", "statePath", "servicePath", "timerPath", "onCalendar", "randomizedDelaySec", "journalTargetBytes", "journalTarget", "tmpMinAgeHours", "includeJournal", "includeTmp", "includeAptCache", "includeToolCaches", "includeWebObserveArtifacts", "includeK3sImageCache", "includeHostContainerdCache", "includeLocalPathOrphans", "includeKubernetesObjectRetention", "includeMergedWorktrees"]},
"stages": rendered.get("stages"),
"servicePreview": rendered["service"] if bool(OPTIONS.get("full")) else None,
"timerPreview": rendered["timer"] if bool(OPTIONS.get("full")) else None,
@@ -2808,6 +2815,12 @@ def main():
if ACTION == "retention-run":
emit_json(start_kubernetes_retention_job(observed_at), persist_large=False)
return 0
if ACTION == "merged-worktrees-plan":
emit_json(merged_worktree_plan({"providerId": PROVIDER_ID, "mergedWorktrees": MERGED_WORKTREE_CONFIG}), persist_large=True)
return 0
if ACTION == "merged-worktrees-run":
emit_json(run_merged_worktrees({"providerId": PROVIDER_ID, "mergedWorktrees": MERGED_WORKTREE_CONFIG}), persist_large=True)
return 0
if bool(OPTIONS.get("memoryPressureOnly")) and ACTION in set(["plan", "run"]):
candidates = collect_memory_reclaim_candidates()
visible = visible_items(candidates)
+22 -6
View File
@@ -5,7 +5,7 @@ import { type UniDeskConfig, rootPath } from "./config";
import { remoteGcDegradedFailure } from "./gc-remote-degraded";
import { runSshCommandCapture } from "./ssh";
type RemoteGcAction = "plan" | "memory" | "memory-distribution" | "retention-plan" | "retention-run" | "snapshot" | "trend" | "run" | "status" | "policy-plan" | "policy-install" | "policy-status";
type RemoteGcAction = "plan" | "memory" | "memory-distribution" | "retention-plan" | "retention-run" | "merged-worktrees-plan" | "merged-worktrees-run" | "snapshot" | "trend" | "run" | "status" | "policy-plan" | "policy-install" | "policy-status";
interface RemoteGcOptions {
confirm: boolean;
@@ -84,6 +84,9 @@ const GC_REMOTE_MEMORY_DISTRIBUTION_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_MEMORY_
const GC_REMOTE_KUBERNETES_RETENTION_RELATIVE_PATH = "scripts/src/gc-remote-kubernetes-retention.py";
const GC_REMOTE_KUBERNETES_RETENTION_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_KUBERNETES_RETENTION_HELPERS__";
const GC_REMOTE_POLICY_KUBERNETES_RETENTION_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_POLICY_KUBERNETES_RETENTION_HELPERS__";
const GC_REMOTE_MERGED_WORKTREE_RELATIVE_PATH = "scripts/src/gc-remote-merged-worktrees.py";
const GC_REMOTE_MERGED_WORKTREE_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_MERGED_WORKTREE_HELPERS__";
const GC_REMOTE_POLICY_MERGED_WORKTREE_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_POLICY_MERGED_WORKTREE_HELPERS__";
const GC_REMOTE_CONTAINERD_RELATIVE_PATH = "scripts/src/gc-remote-containerd.py";
const GC_REMOTE_CONTAINERD_PLACEHOLDER = "# __UNIDESK_GC_REMOTE_CONTAINERD_HELPERS__";
const GC_REMOTE_PVC_RELATIVE_PATH = "scripts/src/gc-remote-pvc.py";
@@ -134,6 +137,16 @@ export async function runRemoteGcCommand(config: UniDeskConfig, providerId: stri
supportedActions: ["plan", "run", "status"],
};
}
if (subaction === "merged-worktrees") {
const [worktreeAction = "plan", ...worktreeArgs] = args;
const options = parseRemoteGcOptions(worktreeArgs);
if (worktreeAction === "plan" || worktreeAction === "dry-run") return await runRemoteGc(config, providerId, "merged-worktrees-plan", options);
if (worktreeAction === "run") {
if (!options.confirm) return { ok: false, error: "gc-remote-merged-worktrees-run-requires-confirm", dryRun: true, mutation: false, requiredFlag: "--confirm", planCommand: `bun scripts/cli.ts gc remote ${providerId} merged-worktrees plan`, runCommand: `bun scripts/cli.ts gc remote ${providerId} merged-worktrees run --confirm` };
return await runRemoteGc(config, providerId, "merged-worktrees-run", options);
}
return { ok: false, error: "unsupported-gc-remote-merged-worktrees-action", action: worktreeAction, supportedActions: ["plan", "run"] };
}
if (subaction === "policy") {
const [policyAction = "plan", ...policyArgs] = args;
const options = parseRemoteGcOptions(policyArgs);
@@ -202,7 +215,7 @@ export async function runRemoteGcCommand(config: UniDeskConfig, providerId: stri
ok: false,
error: "unsupported-gc-remote-action",
action: subaction,
supportedActions: ["memory", "memory-distribution", "retention", "plan", "snapshot", "trend", "run", "status", "policy"],
supportedActions: ["memory", "memory-distribution", "retention", "merged-worktrees", "plan", "snapshot", "trend", "run", "status", "policy"],
};
}
@@ -629,6 +642,7 @@ function remoteGcPython(configBase64: string): string {
if (!template.includes(GC_REMOTE_KUBERNETES_RETENTION_PLACEHOLDER)) {
throw new Error(`${GC_REMOTE_RUNNER_RELATIVE_PATH} missing ${GC_REMOTE_KUBERNETES_RETENTION_PLACEHOLDER}`);
}
if (!template.includes(GC_REMOTE_MERGED_WORKTREE_PLACEHOLDER)) throw new Error(`${GC_REMOTE_RUNNER_RELATIVE_PATH} missing ${GC_REMOTE_MERGED_WORKTREE_PLACEHOLDER}`);
if (!template.includes(GC_REMOTE_CONTAINERD_PLACEHOLDER)) {
throw new Error(`${GC_REMOTE_RUNNER_RELATIVE_PATH} missing ${GC_REMOTE_CONTAINERD_PLACEHOLDER}`);
}
@@ -647,6 +661,7 @@ function remoteGcPython(configBase64: string): string {
const webObserveHelpers = readFileSync(rootPath(GC_REMOTE_WEB_OBSERVE_RELATIVE_PATH), "utf8");
const memoryDistributionHelpers = readFileSync(rootPath(GC_REMOTE_MEMORY_DISTRIBUTION_RELATIVE_PATH), "utf8");
const kubernetesRetentionHelpers = readFileSync(rootPath(GC_REMOTE_KUBERNETES_RETENTION_RELATIVE_PATH), "utf8");
const mergedWorktreeHelpers = readFileSync(rootPath(GC_REMOTE_MERGED_WORKTREE_RELATIVE_PATH), "utf8");
const containerdHelpers = readFileSync(rootPath(GC_REMOTE_CONTAINERD_RELATIVE_PATH), "utf8");
const pvcHelpers = readFileSync(rootPath(GC_REMOTE_PVC_RELATIVE_PATH), "utf8");
const growthHelpers = readFileSync(rootPath(GC_REMOTE_GROWTH_RELATIVE_PATH), "utf8");
@@ -655,13 +670,14 @@ function remoteGcPython(configBase64: string): string {
if (!policyRunnerTemplate.includes(GC_REMOTE_POLICY_KUBERNETES_RETENTION_PLACEHOLDER)) {
throw new Error(`${GC_REMOTE_POLICY_RUNNER_RELATIVE_PATH} missing ${GC_REMOTE_POLICY_KUBERNETES_RETENTION_PLACEHOLDER}`);
}
const policyRunner = policyRunnerTemplate.replace(
GC_REMOTE_POLICY_KUBERNETES_RETENTION_PLACEHOLDER,
() => kubernetesRetentionHelpers.trimEnd(),
);
if (!policyRunnerTemplate.includes(GC_REMOTE_POLICY_MERGED_WORKTREE_PLACEHOLDER)) throw new Error(`${GC_REMOTE_POLICY_RUNNER_RELATIVE_PATH} missing ${GC_REMOTE_POLICY_MERGED_WORKTREE_PLACEHOLDER}`);
const policyRunner = policyRunnerTemplate
.replace(GC_REMOTE_POLICY_KUBERNETES_RETENTION_PLACEHOLDER, () => kubernetesRetentionHelpers.trimEnd())
.replace(GC_REMOTE_POLICY_MERGED_WORKTREE_PLACEHOLDER, () => mergedWorktreeHelpers.trimEnd());
return template
.replace(GC_REMOTE_MEMORY_DISTRIBUTION_PLACEHOLDER, () => memoryDistributionHelpers.trimEnd())
.replace(GC_REMOTE_KUBERNETES_RETENTION_PLACEHOLDER, () => kubernetesRetentionHelpers.trimEnd())
.replace(GC_REMOTE_MERGED_WORKTREE_PLACEHOLDER, () => mergedWorktreeHelpers.trimEnd())
.replace(GC_REMOTE_WEB_OBSERVE_PLACEHOLDER, () => webObserveHelpers.trimEnd())
.replace(GC_REMOTE_CONTAINERD_PLACEHOLDER, () => containerdHelpers.trimEnd())
.replace(GC_REMOTE_PVC_PLACEHOLDER, () => pvcHelpers.trimEnd())