Sync public tree to b67d2a6
This commit is contained in:
parent
21f21fa8b6
commit
1d72f20cce
7 changed files with 466 additions and 6 deletions
|
|
@ -34,6 +34,9 @@ fi
|
|||
node scripts/public-release-dryrun-contract-smoke.js
|
||||
node scripts/public-browser-login-contract-smoke.js
|
||||
node scripts/self-hosted-coordinator-smoke.js
|
||||
if [[ "${DISASMER_CLI_HAPPY_PATH_LIVE:-}" == "1" ]]; then
|
||||
node scripts/cli-happy-path-live-smoke.js
|
||||
fi
|
||||
node scripts/public-local-demo-matrix-smoke.js
|
||||
scripts/release-source-scan.sh
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
|||
const browserLoginFlowSmoke = read("scripts/cli-browser-login-flow-smoke.js");
|
||||
const nodeAttachSmoke = read("scripts/node-attach-smoke.js");
|
||||
const dapSmoke = read("scripts/dap-smoke.js");
|
||||
const cliHappyPathSmoke = read("scripts/cli-happy-path-live-smoke.js");
|
||||
|
||||
expect(criteria, "header", /^# Disasmer CLI-First MVP Acceptance Criteria/m);
|
||||
expect(
|
||||
|
|
@ -123,8 +124,31 @@ assert.deepStrictEqual(
|
|||
expect(
|
||||
criteria,
|
||||
"CLI-first non-e2e gate wording",
|
||||
/scripts\/acceptance-cli-first\.sh[\s\S]*final public-release e2e remains intentionally excluded/
|
||||
/scripts\/acceptance-cli-first\.sh[\s\S]*scripts\/cli-happy-path-live-smoke\.js[\s\S]*live default operator/
|
||||
);
|
||||
expect(
|
||||
cliFirstAcceptance,
|
||||
"CLI-first gate can run live happy path when explicitly enabled",
|
||||
/DISASMER_CLI_HAPPY_PATH_LIVE[\s\S]*cli-happy-path-live-smoke\.js/
|
||||
);
|
||||
for (const [name, pattern] of [
|
||||
["happy path uses released binaries", /disasmer-public-binaries-/],
|
||||
["happy path completes browser login", /--complete-browser-code/],
|
||||
["happy path initializes project", /"project"[\s\S]*"init"/],
|
||||
["happy path inspects project", /"inspect"[\s\S]*"--project"/],
|
||||
["happy path enrolls node", /"node"[\s\S]*"enroll"/],
|
||||
["happy path attaches node", /"node"[\s\S]*"attach"/],
|
||||
["happy path starts worker", /--worker/],
|
||||
["happy path runs build", /"run"[\s\S]*"build"/],
|
||||
["happy path checks logs", /"logs"/],
|
||||
["happy path checks artifacts", /"artifact"[\s\S]*"list"/],
|
||||
["happy path downloads artifact", /"artifact"[\s\S]*"download"/],
|
||||
["happy path restarts process", /"process"[\s\S]*"restart"/],
|
||||
["happy path cancels process", /"process"[\s\S]*"cancel"/],
|
||||
["happy path writes evidence", /cli-happy-path-live\.json/],
|
||||
]) {
|
||||
expect(cliHappyPathSmoke, name, pattern);
|
||||
}
|
||||
expect(
|
||||
criteria,
|
||||
"artifact export explicit local byte write",
|
||||
|
|
|
|||
397
scripts/cli-happy-path-live-smoke.js
Normal file
397
scripts/cli-happy-path-live-smoke.js
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
#!/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);
|
||||
const line = output
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.at(-1);
|
||||
return JSON.parse(line);
|
||||
}
|
||||
|
||||
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();
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
clearTimeout(timer);
|
||||
resolve(parsed);
|
||||
return;
|
||||
} catch (_) {
|
||||
// keep waiting
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
|
@ -203,6 +203,7 @@ for (const [name, pattern] of [
|
|||
["final evidence verifies Forgejo host", /forgejoHost = "git\.michelpaulissen\.com"/],
|
||||
["final evidence verifies default operator", /serviceEndpoint = "https:\/\/disasmer\.michelpaulissen\.com:9443"/],
|
||||
["final evidence accepts external public tree push", /function publicTreeAlreadyPushed\(manifest\)[\s\S]*DISASMER_PUBLIC_TREE_ALREADY_PUSHED/],
|
||||
["final evidence records resolved public repo", /resolvedPublicRepoRemote[\s\S]*public_repository_url: resolvedPublicRepoRemote/],
|
||||
["final evidence requires private hosted coordinator", /operator_implementation[\s\S]*"private-hosted-coordinator"/],
|
||||
["final evidence requires service private coordinator marker", /service\.operator_implementation[\s\S]*"private-hosted-coordinator"/],
|
||||
["final evidence requires current service smoke source", /service\.source_commit[\s\S]*manifest\.source_commit/],
|
||||
|
|
|
|||
|
|
@ -161,8 +161,9 @@ assert.strictEqual(
|
|||
true,
|
||||
"filtered public tree must be pushed to Forgejo"
|
||||
);
|
||||
const resolvedPublicRepoRemote = publicRepoRemote(manifest);
|
||||
assertIncludes(
|
||||
publicRepoRemote(manifest),
|
||||
resolvedPublicRepoRemote,
|
||||
forgejoHost,
|
||||
"manifest public repository must point at Forgejo"
|
||||
);
|
||||
|
|
@ -412,7 +413,7 @@ const finalReport = {
|
|||
},
|
||||
},
|
||||
forgejo_host: forgejoHost,
|
||||
public_repository_url: manifest.public_repo_url,
|
||||
public_repository_url: resolvedPublicRepoRemote,
|
||||
forgejo_release: {
|
||||
owner: forgejoRelease.owner,
|
||||
repo: forgejoRelease.repo,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue