160 lines
9.6 KiB
TypeScript
160 lines
9.6 KiB
TypeScript
import { readConfig } from "./src/config";
|
|
import { runCiPublishUserServiceDryRunPreflight, type PublishPreflightTransport } from "./src/ci";
|
|
import { autoRemoteCiPublishUserServiceDryRunPlan } from "./src/remote";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
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<void> {
|
|
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();
|
|
}
|