fix: guard code queue hostpath source rollout

This commit is contained in:
Codex
2026-05-20 10:37:44 +00:00
parent ba8bdc828e
commit dd92e42918
6 changed files with 350 additions and 5 deletions
+105 -4
View File
@@ -8,6 +8,7 @@ import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
import { runArtifactRegistryCommand } from "./artifact-registry";
import { startJob } from "./jobs";
import { coreInternalFetch } from "./microservices";
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "./code-queue-source-guard";
type DeployAction = "check" | "plan" | "apply";
type DeployEnvironment = "dev" | "prod";
@@ -128,7 +129,9 @@ const pollIntervalMs = 5_000;
const remoteDeployRoot = "/home/ubuntu/.unidesk/deploy";
const k8sNamespace = "unidesk";
const k8sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const k3sDeployDir = "/home/ubuntu/cq-deploy";
// Production k3s hostPath repo. Code Queue production Pods mount this path as /app and /root/unidesk,
// so deploy guards must validate this tree rather than config.json development.worktreePath.
const k3sProductionHostPathRepoDir = "/home/ubuntu/cq-deploy";
const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789";
const nativeK3sInstallVersion = "v1.34.1+k3s1";
const nativeK3sImage = "rancher/k3s:v1.34.1-k3s1";
@@ -191,7 +194,7 @@ function isHelpArg(value: string | undefined): boolean {
}
export function deployHelp(action: string | undefined = undefined): Record<string, unknown> {
const command = action === undefined || isHelpArg(action) ? "deploy check|plan|apply" : `deploy ${action}`;
const command = action === undefined || isHelpArg(action) ? "deploy check|plan|apply|guard" : `deploy ${action}`;
return {
ok: true,
command,
@@ -199,11 +202,13 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
check: "bun scripts/cli.ts deploy check [--file deploy.json | --env dev|prod] [--service id]",
plan: "bun scripts/cli.ts deploy plan [--file deploy.json | --env dev|prod] [--service id]",
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
guard: "bun scripts/cli.ts deploy guard code-queue-source [--root /home/ubuntu/cq-deploy]",
},
actions: {
check: "Validate desired repo+commit state against live service health and commit markers.",
plan: "Show desired/live drift, or with --env show the environment-ref dry-run plan without touching runtime resources.",
apply: "Start an async target-side reconcile job unless --run-now is explicitly present.",
guard: "Run local deployment guards without mutating runtime resources.",
},
options: [
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js. Local D601 maintenance apply is limited to approved direct exceptions; Code Queue direct rollout is disabled." },
@@ -214,6 +219,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
{ name: "--timeout-ms <n>", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." },
{ name: "--run-now", description: "Run apply in the foreground worker process; omit it for fire-and-forget async job mode." },
{ name: "guard code-queue-source --root <path>", description: "Validate Code Queue hostPath source relative imports before any scheduler rollout; failures report degradedReason and missing import targets." },
],
};
}
@@ -887,7 +893,7 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str
function targetWorkDir(service: UniDeskMicroserviceConfig): string {
if (isDevK3sDeployService(service)) return service.development.worktreePath;
if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir;
if (service.deployment.mode === "k3sctl-managed") return k3sProductionHostPathRepoDir;
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) {
return rootPath(".state", "deploy", "work", safeId(service.id));
}
@@ -1223,6 +1229,79 @@ function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string)
].join("\n");
}
function codeQueueSourcePreflightScript(service: UniDeskMicroserviceConfig): string {
if (service.id !== "code-queue") return "";
const workDir = sourceWorkDir(service);
return [
"set -euo pipefail",
`guard_root=${shellQuote(workDir)}`,
"if [ -f \"$guard_root/scripts/code-queue-source-guard.ts\" ] && command -v bun >/dev/null 2>&1; then",
" bun \"$guard_root/scripts/code-queue-source-guard.ts\" --root \"$guard_root\"",
"else",
" python3 - \"$guard_root\" <<'PY'",
"import json",
"import os",
"import re",
"import sys",
"",
`SOURCE_SUBDIR = ${JSON.stringify(codeQueueSourceSubdir)}`,
"EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json']",
"",
"def rel(root, path):",
" return os.path.relpath(path, root).replace(os.sep, '/')",
"",
"def strip_comments(text):",
" text = re.sub(r'/\\*[\\s\\S]*?\\*/', '', text)",
" return re.sub(r'(^|[^:])//.*$', r'\\1', text, flags=re.MULTILINE)",
"",
"def specifiers(text):",
" clean = strip_comments(text)",
" values = set()",
" patterns = [",
" re.compile(r'\\b(?:import|export)\\s+(?:type\\s+)?(?:[\\s\\S]*?\\s+from\\s+)?[\"\\'](\\.{1,2}/[^\"\\']+)[\"\\']'),",
" re.compile(r'\\bimport\\s*\\(\\s*[\"\\'](\\.{1,2}/[^\"\\']+)[\"\\']\\s*\\)'),",
" ]",
" for pattern in patterns:",
" values.update(match.group(1) for match in pattern.finditer(clean))",
" return sorted(values)",
"",
"def candidates(importer, specifier):",
" base = os.path.abspath(os.path.join(os.path.dirname(importer), specifier))",
" if any(base.endswith(extension) for extension in EXTENSIONS):",
" return [base]",
" return [base + extension for extension in EXTENSIONS] + [os.path.join(base, 'index' + extension) for extension in EXTENSIONS]",
"",
"root = os.path.abspath(sys.argv[1])",
"source_root = os.path.join(root, SOURCE_SUBDIR)",
"if not os.path.isdir(source_root):",
" print(json.dumps({'ok': False, 'guard': 'code-queue-hostpath-source-imports', 'root': root, 'sourceRoot': source_root, 'checkedFiles': 0, 'checkedImports': 0, 'missing': [], 'degradedReason': 'source-root-missing', 'message': 'Code Queue source root is missing: ' + SOURCE_SUBDIR}, ensure_ascii=False))",
" raise SystemExit(1)",
"files = []",
"for current_root, dirs, names in os.walk(source_root):",
" dirs[:] = [name for name in dirs if name not in ('node_modules', 'dist', '.git', '.state')]",
" for name in names:",
" if name.endswith('.ts') or name.endswith('.tsx'):",
" files.append(os.path.join(current_root, name))",
"files.sort()",
"missing = []",
"checked_imports = 0",
"for path in files:",
" with open(path, encoding='utf-8') as handle:",
" text = handle.read()",
" for specifier in specifiers(text):",
" checked_imports += 1",
" expected = candidates(path, specifier)",
" if any(os.path.isfile(candidate) for candidate in expected):",
" continue",
" missing.append({'importer': rel(root, path), 'specifier': specifier, 'expected': [rel(root, candidate) for candidate in expected]})",
"result = {'ok': not missing, 'guard': 'code-queue-hostpath-source-imports', 'root': root, 'sourceRoot': source_root, 'checkedFiles': len(files), 'checkedImports': checked_imports, 'missing': missing, 'degradedReason': 'none' if not missing else 'missing-relative-import-target', 'message': 'Code Queue hostPath source import preflight passed (%d files, %d relative imports).' % (len(files), checked_imports) if not missing else 'Code Queue hostPath source import preflight failed: %d relative import target(s) are missing.' % len(missing)}",
"print(json.dumps(result, ensure_ascii=False))",
"raise SystemExit(0 if result['ok'] else 1)",
"PY",
"fi",
].join("\n");
}
function claudeqqDeployAssetOverlayCommands(): string[] {
const assets = [
{
@@ -2480,6 +2559,12 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
const sync = await step(config, service, "sync-source", syncSourceScript(service, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service));
if (!pushStep(steps, sync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
const codeQueueSourcePreflight = codeQueueSourcePreflightScript(service);
if (codeQueueSourcePreflight.length > 0) {
const sourcePreflight = await step(config, service, "code-queue-hostpath-source-preflight", codeQueueSourcePreflight, targetWorkDir(service), 60_000, !targetIsMain(service));
if (!pushStep(steps, sourcePreflight)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
}
const controlManifestSyncScript = syncK8sControlManifestsScript(service);
if (controlManifestSyncScript.length > 0) {
const controlManifestStep = isDevK3sDeployService(service) ? "verify-target-k3s-manifest" : "sync-k3s-control-manifests";
@@ -2901,10 +2986,26 @@ function applyJob(config: UniDeskConfig, args: string[], options: DeployOptions)
};
}
function runDeployGuardCommand(args: string[]): unknown {
const [guardName] = args;
if (guardName !== "code-queue-source") {
return {
ok: false,
supported: false,
error: "unsupported-deploy-guard",
guard: guardName ?? null,
supportedGuards: ["code-queue-source"],
};
}
const root = optionValue(args.slice(1), ["--root"]) ?? repoRoot;
return codeQueueSourceImportPreflight(root);
}
export async function runDeployCommand(config: UniDeskConfig | null, args: string[]): Promise<unknown> {
const [actionRaw = "check"] = args;
if (isHelpArg(actionRaw) || args.slice(1).some(isHelpArg)) return deployHelp(isHelpArg(actionRaw) ? undefined : actionRaw);
if (!["check", "plan", "apply"].includes(actionRaw)) throw new Error("deploy command must be one of: check, plan, apply");
if (actionRaw === "guard") return runDeployGuardCommand(args.slice(1));
if (!["check", "plan", "apply"].includes(actionRaw)) throw new Error("deploy command must be one of: check, plan, apply, guard");
const action = actionRaw as DeployAction;
const options = parseOptions(args.slice(1));
if (options.environment !== null) {