clusterflux-public/scripts/cli-happy-path-live-smoke.js
2026-07-04 17:47:40 +02:00

413 lines
12 KiB
JavaScript

#!/usr/bin/env node
const assert = require("assert");
const cp = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
const repo = path.resolve(__dirname, "..");
const releaseRoot = path.resolve(
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
path.join(repo, "target/public-release-dryrun")
);
const acceptanceRoot = path.join(repo, "target/acceptance");
const manifestPath = path.join(releaseRoot, "public-release-manifest.json");
const reportPath = path.join(acceptanceRoot, "cli-happy-path-live.json");
const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443";
const serviceAddr =
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR ||
"disasmer.michelpaulissen.com:9443";
const oidcIssuer = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL;
const oidcCode = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE;
const enabled = process.env.DISASMER_CLI_HAPPY_PATH_LIVE === "1";
const commands = [];
function requireEnabled() {
if (!enabled) {
throw new Error(
"DISASMER_CLI_HAPPY_PATH_LIVE=1 is required because this runs the live CLI happy path"
);
}
if (!oidcIssuer || !oidcCode) {
throw new Error(
"DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL and DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE are required"
);
}
}
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function nonInteractiveGitEnv(extra = {}) {
return {
...process.env,
GIT_TERMINAL_PROMPT: "0",
GIT_ASKPASS: process.env.GIT_ASKPASS || "/bin/false",
SSH_ASKPASS: process.env.SSH_ASKPASS || "/bin/false",
GIT_SSH_COMMAND:
process.env.GIT_SSH_COMMAND ||
"ssh -o BatchMode=yes -o NumberOfPasswordPrompts=0",
...extra,
};
}
function commandEnv(command, env) {
if (command !== "git") return env;
return nonInteractiveGitEnv(env);
}
function run(command, args, options = {}) {
commands.push([command, ...args].join(" "));
return cp.execFileSync(command, args, {
cwd: repo,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 15 * 60 * 1000,
...options,
env: commandEnv(command, options.env),
});
}
function runJson(command, args, options = {}) {
const output = run(command, args, options);
return parseJsonOutput(output);
}
function parseJsonOutput(output) {
const trimmed = output.trim();
if (!trimmed) {
throw new Error("expected JSON output, got empty stdout");
}
try {
return JSON.parse(trimmed);
} catch (_) {
// Commands may print progress before their final JSON report. Try each
// trailing block so both compact and pretty-printed JSON are accepted.
}
const lines = trimmed.split(/\r?\n/).filter(Boolean);
for (let index = lines.length - 1; index >= 0; index -= 1) {
try {
return JSON.parse(lines.slice(index).join("\n"));
} catch (_) {
// Keep scanning for the start of the trailing JSON value.
}
}
throw new Error(`could not parse JSON output:\n${trimmed}`);
}
function executable(root, name) {
return path.join(root, "bin", process.platform === "win32" ? `${name}.exe` : name);
}
function binaryArchive(manifest) {
const platform = `${os.platform()}-${os.arch()}`;
const asset = manifest.assets.find(
(candidate) =>
candidate.name.startsWith("disasmer-public-binaries-") &&
candidate.name.includes(platform)
);
assert(asset, `missing released binary archive for ${platform}`);
assert(fs.existsSync(asset.file), `missing released binary archive ${asset.file}`);
return asset.file;
}
function publicRepoUrl(manifest) {
const url =
process.env.DISASMER_PUBLIC_REPO_URL ||
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
manifest.public_repo_url ||
manifest.public_repo_remote;
assert(url, "public repository URL is required");
assert(url.includes("git.michelpaulissen.com"));
return url;
}
function waitForJsonLine(child, label) {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
reject(new Error(`${label} did not produce a JSON line\n${stderr}`));
}, 120000);
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
try {
const parsed = parseJsonOutput(stdout);
clearTimeout(timer);
resolve(parsed);
return;
} catch (_) {
// Keep waiting for a complete JSON value.
}
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.once("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.once("exit", (code) => {
if (code !== 0) {
clearTimeout(timer);
reject(new Error(`${label} exited with ${code}\n${stderr}`));
}
});
});
}
async function stopChild(child) {
if (!child || child.exitCode !== null) return;
child.kill("SIGTERM");
await new Promise((resolve) => child.once("exit", resolve));
}
async function main() {
requireEnabled();
const manifest = readJson(manifestPath);
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
assert.strictEqual(manifest.default_operator_endpoint, serviceEndpoint);
assert.strictEqual(serviceAddr, "disasmer.michelpaulissen.com:9443");
const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-cli-happy-"));
const installDir = path.join(workRoot, "install");
const checkout = path.join(workRoot, "public-repo");
ensureDir(installDir);
run("tar", ["-xzf", binaryArchive(manifest), "-C", installDir]);
run("git", ["clone", "--depth", "1", publicRepoUrl(manifest), checkout]);
const disasmer = executable(installDir, "disasmer");
const disasmerNode = executable(installDir, "disasmer-node");
const projectDir = path.join(checkout, "examples/launch-build-demo");
const suffix = String(Date.now());
const tenant = `cli-${suffix}`;
const project = `project-${suffix}`;
const user = "user";
const cliNode = `cli-node-${suffix}`;
const workerNode = `worker-${suffix}`;
const scope = [
"--coordinator",
serviceEndpoint,
"--tenant",
tenant,
"--project-id",
project,
"--user",
user,
"--json",
];
const login = runJson(
disasmer,
[
"login",
"--browser",
"--json",
"--complete-browser-code",
oidcCode,
"--oidc-issuer-url",
oidcIssuer,
"--tenant",
tenant,
"--project-id",
project,
"--user",
user,
],
{ cwd: projectDir }
);
assert.strictEqual(login.plan.coordinator, serviceEndpoint);
assert.strictEqual(login.boundary.scoped_cli_session_received, true);
const projectInit = runJson(
disasmer,
[
"project",
"init",
...scope,
"--new-project",
project,
"--name",
"CLI Happy Path",
"--yes",
],
{ cwd: projectDir }
);
assert.strictEqual(projectInit.command, "project init");
assert.strictEqual(projectInit.project_config_written, true);
assert.strictEqual(projectInit.coordinator_response.type, "project");
const projectList = runJson(disasmer, ["project", "list", ...scope], {
cwd: projectDir,
});
assert(projectList.project_count >= 1);
const projectSelect = runJson(
disasmer,
["project", "select", ...scope, project],
{ cwd: projectDir }
);
assert.strictEqual(projectSelect.command, "project select");
const inspection = runJson(disasmer, ["inspect", "--project", ".", "--json"], {
cwd: projectDir,
});
assert(
inspection.discovered_environments.some((environment) => environment.name === "linux")
);
assert(inspection.source_providers);
const cliGrant = runJson(
disasmer,
["node", "enroll", ...scope, "--grant", `cli-grant-${suffix}`],
{ cwd: projectDir }
);
assert.strictEqual(cliGrant.command, "node enroll");
const attach = runJson(
disasmer,
[
"node",
"attach",
"--coordinator",
serviceEndpoint,
"--tenant",
tenant,
"--project-id",
project,
"--node",
cliNode,
"--public-key",
`${cliNode}-public-key`,
"--enrollment-grant",
cliGrant.enrollment_grant.grant,
"--json",
],
{ cwd: projectDir }
);
assert.strictEqual(attach.command, "node attach");
assert.strictEqual(attach.boundary.used_enrollment_exchange, true);
const workerGrant = runJson(
disasmer,
["node", "enroll", ...scope, "--grant", `worker-grant-${suffix}`],
{ cwd: projectDir }
);
const workerArgs = [
"--coordinator",
serviceEndpoint,
"--tenant",
tenant,
"--project-id",
project,
"--node",
workerNode,
"--public-key",
`${workerNode}-public-key`,
"--enrollment-grant",
workerGrant.enrollment_grant.grant,
"--worker",
"--assignment-poll-ms",
"500",
"--emit-ready",
];
commands.push([disasmerNode, ...workerArgs].join(" "));
const worker = cp.spawn(disasmerNode, workerArgs, { cwd: projectDir });
try {
const ready = await waitForJsonLine(worker, "CLI happy-path worker ready");
assert.strictEqual(ready.node_status, "ready");
const runReport = runJson(disasmer, ["run", "build", "--project", ".", "--json"], {
cwd: projectDir,
});
assert.strictEqual(runReport.command, "run");
assert.strictEqual(runReport.status, "task_launched");
assert.strictEqual(runReport.task_launch.type, "task_launched");
assert.strictEqual(runReport.task_launch.placement.node, workerNode);
const completion = await waitForJsonLine(worker, "CLI happy-path worker completion");
assert.strictEqual(completion.node_status, "completed");
const processStatus = runJson(
disasmer,
["process", "status", ...scope, "--process", "vp-current"],
{ cwd: projectDir }
);
assert.strictEqual(processStatus.command, "process status");
assert(processStatus.current_task_count >= 1);
const logs = runJson(disasmer, ["logs", ...scope, "--process", "vp-current"], {
cwd: projectDir,
});
assert(logs.log_entries.length >= 1);
const artifacts = runJson(
disasmer,
["artifact", "list", ...scope, "--process", "vp-current"],
{ cwd: projectDir }
);
assert(artifacts.artifacts.length >= 1);
const artifact = artifacts.artifacts[0].artifact;
const download = runJson(disasmer, ["artifact", "download", ...scope, artifact], {
cwd: projectDir,
});
assert.strictEqual(download.download_session.link_issued, true);
const restart = runJson(
disasmer,
["process", "restart", ...scope, "--process", "vp-current", "--yes"],
{ cwd: projectDir }
);
assert.strictEqual(restart.restart_request.accepted, true);
const cancel = runJson(
disasmer,
["process", "cancel", ...scope, "--process", "vp-current", "--yes"],
{ cwd: projectDir }
);
assert.strictEqual(cancel.cancel_request.accepted, true);
const report = {
kind: "disasmer-cli-happy-path-live",
release_name: manifest.release_name,
source_commit: manifest.source_commit,
service_addr: serviceAddr,
default_operator_endpoint: serviceEndpoint,
public_repository_url: publicRepoUrl(manifest),
tenant,
project,
cli_node: cliNode,
worker_node: workerNode,
signed_in: true,
cli_installed_and_login_ran: true,
project_initialized_or_selected: true,
inspect_discovered_linux: true,
node_attached_with_cli: true,
run_launched_task: true,
work_ran_on_attached_node: true,
status_and_logs_seen: true,
artifact_download_requested: true,
process_restart_requested: true,
process_cancel_requested: true,
commands,
acceptance_result: "passed",
};
ensureDir(path.dirname(reportPath));
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(`CLI happy-path live smoke passed: ${reportPath}`);
} finally {
await stopChild(worker);
}
}
main().catch((error) => {
console.error(error.stack || error.message);
process.exit(1);
});