import { readConfig } from "./src/config"; import { runCiPublishUserServiceDryRunPreflight, type PublishPreflightTransport } from "./src/ci"; import { autoRemoteCiPublishUserServiceDryRunPlan } from "./src/remote"; 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 infraBlockedTransport: PublishPreflightTransport = { coreFetch: async (path) => ({ ok: false, status: 503, body: { ok: false, failureKind: "target-stack-not-running", runnerDisposition: "infra-blocked", degradedReason: "backend-core-container-missing", message: `backend-core unavailable for ${path}`, }, }), dispatchHostSsh: async (command, waitMs, remoteTimeoutMs) => ({ ok: false, taskId: null, status: "infra-blocked", stdout: "", stderr: `backend-core bridge unavailable while dispatching readonly SSH task (${waitMs}/${remoteTimeoutMs})`, exitCode: null, raw: { ok: false, failureKind: "target-stack-not-running", runnerDisposition: "infra-blocked", command, }, }), commandCwd: "/workspace/unidesk", artifactRegistryCommand: (probe) => [process.execPath, "scripts/cli.ts", "ssh", probe.providerId, "argv", "bash", "-lc", probe.script], }; async function main(): Promise { const autoRemote = autoRemoteCiPublishUserServiceDryRunPlan(config, [ "ci", "publish-user-service", "--service", "frontend", "--commit", commit, "--dry-run", ], { CODE_QUEUE_SERVICE_ROLE: "scheduler", CODE_QUEUE_DEV_CONTAINER_MASTER_HOST: "74.48.78.17", }); assertCondition(autoRemote.enabled === true, "Code Queue runner publish dry-run should auto-select remote frontend transport", autoRemote); assertCondition(autoRemote.host === "74.48.78.17", "auto remote plan should use CODE_QUEUE_DEV_CONTAINER_MASTER_HOST", autoRemote); assertCondition(autoRemote.transport === "frontend", "auto remote plan should use frontend transport", autoRemote); assertCondition(String(autoRemote.command ?? "").includes("--main-server-ip 74.48.78.17"), "auto remote plan should expose command shape", autoRemote); const nonRunner = autoRemoteCiPublishUserServiceDryRunPlan(config, [ "ci", "publish-user-service", "--service", "frontend", "--commit", commit, "--dry-run", ], {}); assertCondition(nonRunner.enabled === false, "non-runner local CLI should not auto-remote without a host hint", nonRunner); assertCondition(nonRunner.failureClassification === "local-docker-required", "non-runner disabled plan should classify local docker requirement", nonRunner); const result = await runCiPublishUserServiceDryRunPreflight(config, [ "publish-user-service", "--service", "frontend", "--commit", commit, "--dry-run", ], infraBlockedTransport); const record = asRecord(result, "preflight"); const source = asRecord(record.source, "source"); const channels = Array.isArray(record.channels) ? record.channels.map((item) => asRecord(item, "channel")) : []; const controlChannels = Array.isArray(record.controlChannels) ? record.controlChannels.map((item) => asRecord(item, "control channel")) : []; const registry = asRecord(record.registry, "registry"); const controlledPublish = asRecord(record.controlledPublish, "controlledPublish"); const controlPlane = asRecord(record.controlPlane, "controlPlane"); const backendCore = asRecord(channels.find((item) => item.channel === "backend-core-api")?.detail, "backendCore detail"); const backendCoreTransport = asRecord(backendCore.detail, "backendCore transport payload"); const backendCoreBody = asRecord(backendCoreTransport.body, "backendCore body payload"); const providerDispatch = asRecord(channels.find((item) => item.channel === "provider-dispatch")?.detail, "providerDispatch detail"); const missingChannels = Array.isArray(record.missingChannels) ? record.missingChannels as string[] : []; const missingControlChannels = Array.isArray(record.missingControlChannels) ? record.missingControlChannels as string[] : []; assertCondition(record.ok === false, "infra-blocked preflight should fail", record); assertCondition(record.mode === "dry-run-preflight", "dry-run preflight mode should be reported", record); assertCondition(record.runnerDisposition === "infra-blocked", "runnerDisposition should be infra-blocked", record); assertCondition(record.failureClassification === "local-docker-required", "local backend-core absence should classify as local-docker-required", record); assertCondition(controlPlane.transport === "local-docker", "local preflight should expose local-docker transport", controlPlane); assertCondition(controlPlane.remoteCapable === false, "local preflight should not claim remote-capable transport", controlPlane); assertCondition(controlPlane.preferredRunnerPath === "frontend-private-proxy", "controlPlane should name frontend private proxy as preferred path", controlPlane); assertCondition(Array.isArray(record.missingChannels), "missingChannels should be an array", record); assertCondition(Array.isArray(record.missingControlChannels), "missingControlChannels should be an array", record); assertCondition(Array.isArray(record.failedScopes), "failedScopes should be an array", record); assertCondition((record.failedScopes as unknown[]).includes("local-docker-control-plane"), "failedScopes should expose publish-blocking registry scope", record); assertCondition(missingChannels.includes("backend-core-api"), "backend-core-api should be missing", record); assertCondition(missingChannels.includes("database"), "database should be missing", record); assertCondition(missingChannels.includes("provider-dispatch"), "provider-dispatch should be missing", record); assertCondition(missingChannels.includes("provider-host-ssh"), "provider-host-ssh should be missing", record); assertCondition(missingChannels.includes("artifact-registry"), "artifact-registry should be missing", record); assertCondition(missingControlChannels.join(",") === "backend-core,database,provider,registry", "missingControlChannels should name runner-facing domains", record); assertCondition(controlChannels.length === 4, "controlChannels should report four runner-facing domains", controlChannels); assertCondition(controlChannels.every((item) => item.ok === false), "infra-blocked transport should fail every control channel", controlChannels); assertCondition( (controlChannels.find((item) => item.channel === "provider")?.probes as unknown[]).join(",") === "provider-dispatch,provider-host-ssh", "provider control channel should map provider dispatch and host ssh probes", controlChannels, ); assertCondition(!JSON.stringify(record).includes("No such container: unidesk-database"), "raw container error should not leak", record); assertCondition(backendCoreBody.failureKind === "target-stack-not-running", "backend-core detail should classify target-stack-not-running", backendCoreBody); assertCondition(providerDispatch.status === "infra-blocked", "provider dispatch should be infra-blocked", providerDispatch); assertCondition(registry.ok === false, "registry channel should fail without backend-core bridge", registry); assertCondition(Array.isArray(channels) && channels.length >= 5, "expected five channel probes", channels); assertCondition(source.mode === "planned-only", "source should remain planned-only", source); assertCondition(source.repoFetchUrl === "git@github.com:pikasTech/unidesk.git", "source repo should use CI catalog ssh form", source); assertCondition(asRecord(record.artifactSummary, "artifactSummary").imageRef === `127.0.0.1:5000/unidesk/frontend:${commit}`, "artifact ref should remain commit-pinned", record.artifactSummary); assertCondition(controlledPublish.environment === "D601", "controlledPublish should name the controlled environment", controlledPublish); assertCondition(controlledPublish.namespace === "unidesk-ci", "controlledPublish should name the Tekton namespace", controlledPublish); assertCondition(controlledPublish.pipeline === "unidesk-user-service-artifact-publish", "controlledPublish should name the user-service pipeline", controlledPublish); assertCondition(String(controlledPublish.command ?? "").includes("--wait-ms 1200000"), "controlledPublish should provide the real publish command shape", controlledPublish); assertCondition(String(record.boundary ?? "").includes("read-only"), "boundary should state preflight is read-only", record); process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "dry-run preflight returns infra-blocked when backend-core/database/provider channels are absent", "missing channel list names the absent channels", "artifact summary remains commit-pinned and read-only", "missingControlChannels maps detailed probes to backend-core/database/provider/registry", "controlledPublish identifies D601 unidesk-ci as the only place for the real publish", "runner-like environments auto-select the existing frontend remote transport", "local backend-core absence is classified as local-docker-required", ], missingChannels: record.missingChannels, missingControlChannels: record.missingControlChannels, registry, }, null, 2)}\n`); } if (import.meta.main) { await main(); }