import { readConfig } from "./src/config"; import { runCiPublishBackendCoreDryRunPreflight, type PublishPreflightTransport } from "./src/ci"; type JsonRecord = Record; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); } function asRecord(value: unknown, label: string): JsonRecord { assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, { value }); return value as JsonRecord; } const commit = "0123456789abcdef0123456789abcdef01234567"; const config = readConfig(); const readyTransport: PublishPreflightTransport = { kind: "remote-frontend", remoteHost: "https://example.invalid", coreFetch: async () => ({ ok: true, status: 200, body: { ok: true, dbReady: true, }, }), dispatchHostSsh: async (command) => { if (command.includes("kubectl get namespace unidesk-ci")) { return { ok: true, taskId: "task-ci-runner", status: "succeeded", stdout: [ "kubectl=ok", "docker=ok", "docker_socket=true", "namespace=true", "tekton_pipeline=true", "tekton_task=true", "service_account=true", "pvc=true", "source_parent_directory=true", ].join("\n"), stderr: "", exitCode: 0, raw: {}, }; } if (command.includes("v2/")) { return { ok: true, taskId: "task-registry", status: "succeeded", stdout: [ "systemctl_available=true", "docker_available=true", "curl_available=true", "unit_exists=true", "unit_active=active", "unit_enabled=enabled", "compose_exists=true", "config_exists=true", "storage_exists=true", "container_running=true", "container_status=running", "container_image=registry:2.8.3", "container_restart_policy=always", "listener_count=1", "bad_listener_count=0", "loopback_only=true", "v2_http_code=200", "image_matches=true", "config_hash_matches=true", "compose_hash_matches=true", "unit_hash_matches=true", ].join("\n"), stderr: "", exitCode: 0, raw: {}, }; } return { ok: true, taskId: "task-host-ssh", status: "succeeded", stdout: "provider_host_ssh=ok\n", stderr: "", exitCode: 0, raw: {}, }; }, commandCwd: "/workspace/unidesk", artifactRegistryCommand: (probe) => ["mock", probe.action, probe.remoteCommandShape], }; async function main(): Promise { const result = await runCiPublishBackendCoreDryRunPreflight(config, [ "publish-backend-core", "--commit", commit, "--dry-run", ], readyTransport); const record = asRecord(result, "backend-core preflight"); const source = asRecord(record.source, "source"); const sourceAuth = asRecord(record.sourceAuth, "sourceAuth"); const d601Ci = asRecord(record.d601Ci, "d601Ci"); const artifactSummary = asRecord(record.artifactSummary, "artifactSummary"); const artifactRequirements = asRecord(record.artifactRequirements, "artifactRequirements"); const requiredLabels = asRecord(artifactRequirements.requiredLabels, "requiredLabels"); const devApplyPath = asRecord(record.devApplyPath, "devApplyPath"); const controlledPublish = asRecord(record.controlledPublish, "controlledPublish"); const blockedScopes = Array.isArray(record.blockedScopes) ? record.blockedScopes as string[] : []; assertCondition(record.ok === true, "ready backend-core preflight should pass", record); assertCondition(record.mode === "dry-run-preflight", "backend-core dry-run must be preflight mode", record); assertCondition(record.targetCommit === commit, "target commit should be surfaced", record); assertCondition(record.sourceRepo === "https://github.com/pikasTech/unidesk", "source repo should come from CI.json", record); assertCondition(record.registryTarget === "127.0.0.1:5000/unidesk/backend-core", "registry target should be D601 backend-core repository", record); assertCondition(record.wouldBuildOnD601 === true, "preflight should state real publish would build on D601", record); assertCondition(record.dryRunBuildStarted === false, "dry-run must not start a build", record); assertCondition(blockedScopes.length === 0, "ready preflight should have no blocked scopes", record); assertCondition(source.repoFetchUrl === "git@github.com:pikasTech/unidesk.git", "source auth should use GitHub SSH fetch URL", source); assertCondition(sourceAuth.egressProxy === "http://127.0.0.1:18789", "source auth should name provider egress proxy", sourceAuth); assertCondition(d601Ci.wouldBuildOnD601 === true, "D601 CI runner preflight should expose D601 build boundary", d601Ci); assertCondition(artifactSummary.imageRef === `127.0.0.1:5000/unidesk/backend-core:${commit}`, "artifact should be commit-pinned", artifactSummary); assertCondition(artifactSummary.digest === null, "dry-run must not fake digest", artifactSummary); assertCondition(requiredLabels["unidesk.ai/service-id"] === "backend-core", "service id label should be required", requiredLabels); assertCondition(requiredLabels["unidesk.ai/source-commit"] === commit, "source commit label should be required", requiredLabels); assertCondition(devApplyPath.pullOnly === true, "dev apply path should be pull-only", devApplyPath); assertCondition(String(devApplyPath.apply ?? "").includes("deploy apply --env dev --service backend-core"), "dev apply command should be surfaced", devApplyPath); assertCondition(!String(devApplyPath.apply ?? "").includes("--env prod"), "dev apply path must not point to prod", devApplyPath); assertCondition(controlledPublish.environment === "D601", "controlled publish should name D601", controlledPublish); assertCondition(String(record.boundary ?? "").includes("no Rust compile"), "boundary should explicitly forbid Rust compile during dry-run", record); assertCondition(String(record.recommendedAction ?? "").includes("ci publish-backend-core"), "recommended action should name real publish command", record); process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "backend-core dry-run exposes target commit, source repo, D601 runner and registry target", "backend-core dry-run reports wouldBuildOnD601 without starting a build", "artifact labels and digest requirements are visible without faking a digest", "standard path is publish artifact, verify labels/digest, then dev pull-only apply", ], }, null, 2)}\n`); } if (import.meta.main) { await main(); }