Public dry run dryrun-750f29790fe2
Source commit: 750f29790fe2046599fbe5b5d06fdb63d92fabff Public tree identity: sha256:02326e08850bb287585789733ea5f3f153e1f7f3fd88afcc24e249107df91506
This commit is contained in:
commit
de66658961
102 changed files with 31669 additions and 0 deletions
847
scripts/public-release-dryrun-e2e.js
Executable file
847
scripts/public-release-dryrun-e2e.js
Executable file
|
|
@ -0,0 +1,847 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const net = require("net");
|
||||
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 =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_MANIFEST ||
|
||||
path.join(releaseRoot, "public-release-manifest.json");
|
||||
const forgejoReportPath =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_FORGEJO_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-forgejo-release.json");
|
||||
const reportPath =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_E2E_REPORT ||
|
||||
path.join(acceptanceRoot, "public-release-dryrun-e2e.json");
|
||||
const serviceEndpoint = "https://disasmer.michelpaulissen.com:9443";
|
||||
const serviceHost = "disasmer.michelpaulissen.com";
|
||||
const serviceAddr =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_SERVICE_ADDR || `${serviceHost}:9443`;
|
||||
const forgejoHost = "git.michelpaulissen.com";
|
||||
const enabled = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_E2E === "1";
|
||||
const oidcIssuer = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_ISSUER_URL;
|
||||
const oidcCode = process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CODE;
|
||||
const oidcClientId =
|
||||
process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_OIDC_CLIENT_ID || "disasmer";
|
||||
const dnsPublicationState =
|
||||
process.env.DISASMER_DNS_PUBLICATION_STATE || "published";
|
||||
const resolverOverride =
|
||||
process.env.DISASMER_RESOLVER_OVERRIDE || "none-required-public-dns";
|
||||
|
||||
function requireEnabled() {
|
||||
if (!enabled) {
|
||||
throw new Error(
|
||||
"DISASMER_PUBLIC_RELEASE_DRYRUN_E2E=1 is required because this runs the real public dry-run e2e"
|
||||
);
|
||||
}
|
||||
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 run(command, args, options = {}) {
|
||||
const commandLine = [command, ...args].join(" ");
|
||||
commands.push(commandLine);
|
||||
return cp.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
timeout: 15 * 60 * 1000,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function runJson(command, args, options = {}) {
|
||||
const output = run(command, args, options);
|
||||
try {
|
||||
return JSON.parse(output);
|
||||
} catch (_) {
|
||||
const line = output
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.at(-1);
|
||||
return JSON.parse(line);
|
||||
}
|
||||
}
|
||||
|
||||
function commandOutput(command, args, options = {}) {
|
||||
try {
|
||||
return cp
|
||||
.execFileSync(command, args, {
|
||||
cwd: repo,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
...options,
|
||||
})
|
||||
.trim();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedSourceCommit() {
|
||||
return (
|
||||
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
||||
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
function sha256File(file) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
}
|
||||
|
||||
function parseAddr(value) {
|
||||
const match = /^([^:]+):(\d+)$/.exec(value);
|
||||
if (!match) {
|
||||
throw new Error(`service address must be host:port, got ${value}`);
|
||||
}
|
||||
return { host: match[1], port: Number(match[2]) };
|
||||
}
|
||||
|
||||
function send(addr, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.connect(addr.port, addr.host, () => {
|
||||
socket.write(`${JSON.stringify(message)}\n`);
|
||||
});
|
||||
let buffer = "";
|
||||
const timer = setTimeout(() => {
|
||||
socket.destroy();
|
||||
reject(new Error(`timed out waiting for ${message.type}`));
|
||||
}, 30000);
|
||||
socket.on("data", (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
try {
|
||||
resolve(JSON.parse(buffer.slice(0, newline)));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
socket.on("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function waitForJsonLine(child, label = "process", timeoutMs = 240000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = "";
|
||||
const timer = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error(`timed out waiting for JSON line from ${label}`));
|
||||
}, timeoutMs);
|
||||
function cleanup() {
|
||||
clearTimeout(timer);
|
||||
child.stdout.off("data", onData);
|
||||
child.off("exit", onExit);
|
||||
}
|
||||
function onData(chunk) {
|
||||
buffer += chunk.toString();
|
||||
const newline = buffer.indexOf("\n");
|
||||
if (newline < 0) return;
|
||||
try {
|
||||
cleanup();
|
||||
resolve(JSON.parse(buffer.slice(0, newline).trim()));
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
function onExit(code) {
|
||||
cleanup();
|
||||
reject(new Error(`${label} exited before JSON line with code ${code}`));
|
||||
}
|
||||
child.stdout.on("data", onData);
|
||||
child.once("exit", onExit);
|
||||
});
|
||||
}
|
||||
|
||||
function stopChild(child) {
|
||||
if (!child || child.exitCode !== null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
resolve();
|
||||
}, 5000);
|
||||
child.once("exit", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
child.kill("SIGTERM");
|
||||
});
|
||||
}
|
||||
|
||||
function download(url, dest, redirects = 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith("http://") ? http : https;
|
||||
const request = client.get(url, (response) => {
|
||||
if (
|
||||
[301, 302, 303, 307, 308].includes(response.statusCode) &&
|
||||
response.headers.location &&
|
||||
redirects < 5
|
||||
) {
|
||||
response.resume();
|
||||
download(new URL(response.headers.location, url).toString(), dest, redirects + 1)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
response.resume();
|
||||
reject(new Error(`download ${url} failed with ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
ensureDir(path.dirname(dest));
|
||||
const file = fs.createWriteStream(dest);
|
||||
response.pipe(file);
|
||||
file.on("finish", () => file.close(resolve));
|
||||
file.on("error", reject);
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function releaseAssetDownloads(forgejoReport) {
|
||||
const assets = [
|
||||
...(forgejoReport.uploaded_assets || []),
|
||||
...(forgejoReport.reused_assets || []),
|
||||
];
|
||||
const byName = new Map();
|
||||
for (const asset of assets) {
|
||||
if (asset.name && asset.browser_download_url) {
|
||||
byName.set(asset.name, asset.browser_download_url);
|
||||
}
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
|
||||
async function downloadReleaseAssets(manifest, forgejoReport, assetsDir) {
|
||||
const downloads = releaseAssetDownloads(forgejoReport);
|
||||
const downloaded = [];
|
||||
for (const asset of manifest.assets) {
|
||||
const dest = path.join(assetsDir, asset.name);
|
||||
const url = downloads.get(asset.name);
|
||||
if (url) {
|
||||
await download(url, dest);
|
||||
} else if (process.env.DISASMER_PUBLIC_RELEASE_USE_LOCAL_ASSETS === "1") {
|
||||
fs.copyFileSync(asset.file, dest);
|
||||
} else {
|
||||
throw new Error(`Forgejo Release report has no download URL for ${asset.name}`);
|
||||
}
|
||||
downloaded.push(dest);
|
||||
}
|
||||
return downloaded;
|
||||
}
|
||||
|
||||
function verifyChecksums(assetsDir) {
|
||||
const sumsPath = path.join(assetsDir, "SHA256SUMS");
|
||||
assert(fs.existsSync(sumsPath), "SHA256SUMS must be downloaded from the release");
|
||||
for (const line of fs.readFileSync(sumsPath, "utf8").split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
const match = /^([a-f0-9]{64})\s+(.+)$/.exec(line.trim());
|
||||
assert(match, `invalid SHA256SUMS line: ${line}`);
|
||||
const [, expected, name] = match;
|
||||
const file = path.join(assetsDir, name);
|
||||
assert(fs.existsSync(file), `checksum references missing asset ${name}`);
|
||||
assert.strictEqual(sha256File(file), expected, `checksum mismatch for ${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)
|
||||
);
|
||||
if (!asset) {
|
||||
throw new Error(`release manifest has no binary archive for ${platform}`);
|
||||
}
|
||||
return asset.name;
|
||||
}
|
||||
|
||||
function walkFiles(root, relative = "") {
|
||||
const dir = path.join(root, relative);
|
||||
const entries = fs
|
||||
.readdirSync(dir, { withFileTypes: true })
|
||||
.filter((entry) => relative || entry.name !== ".git")
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...walkFiles(root, childRelative));
|
||||
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
||||
files.push(childRelative);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function hashTree(root) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
for (const file of walkFiles(root)) {
|
||||
const absolute = path.join(root, file);
|
||||
const stat = fs.lstatSync(absolute);
|
||||
hash.update(file.replaceAll(path.sep, "/"));
|
||||
hash.update("\0");
|
||||
hash.update(String(stat.mode & 0o777));
|
||||
hash.update("\0");
|
||||
if (stat.isSymbolicLink()) {
|
||||
hash.update("symlink");
|
||||
hash.update("\0");
|
||||
hash.update(fs.readlinkSync(absolute));
|
||||
} else {
|
||||
hash.update(fs.readFileSync(absolute));
|
||||
}
|
||||
hash.update("\0");
|
||||
}
|
||||
return `sha256:${hash.digest("hex")}`;
|
||||
}
|
||||
|
||||
function assertFilteredPublicCheckout(checkout, manifest) {
|
||||
for (const forbidden of ["private", "experiments", "target"]) {
|
||||
assert(!fs.existsSync(path.join(checkout, forbidden)), `${forbidden}/ leaked into public repo`);
|
||||
}
|
||||
for (const entry of fs.readdirSync(checkout, { withFileTypes: true })) {
|
||||
assert(
|
||||
!(entry.isFile() && path.extname(entry.name).toLowerCase() === ".md"),
|
||||
`root Markdown file leaked into public repo: ${entry.name}`
|
||||
);
|
||||
}
|
||||
const includeForgejoWorkflows =
|
||||
manifest.public_export && manifest.public_export.include_forgejo_workflows;
|
||||
if (!includeForgejoWorkflows) {
|
||||
assert(!fs.existsSync(path.join(checkout, ".forgejo")), ".forgejo/ leaked into public repo");
|
||||
}
|
||||
const provenancePath = path.join(checkout, "DISASMER_PUBLIC_TREE.json");
|
||||
assert(fs.existsSync(provenancePath), "public checkout must include DISASMER_PUBLIC_TREE.json");
|
||||
const provenance = readJson(provenancePath);
|
||||
assert.strictEqual(provenance.source_commit, manifest.source_commit);
|
||||
assert.strictEqual(provenance.release_name, manifest.release_name);
|
||||
for (const expected of ["private/**", "experiments/**", ".git", "target", "/*.md"]) {
|
||||
assert(provenance.filtered_out.includes(expected), `public provenance missing ${expected}`);
|
||||
}
|
||||
if (!includeForgejoWorkflows) {
|
||||
assert(
|
||||
provenance.filtered_out.includes(".forgejo/**"),
|
||||
"public provenance must record .forgejo filtering"
|
||||
);
|
||||
}
|
||||
assert.strictEqual(hashTree(checkout), manifest.public_tree_identity);
|
||||
}
|
||||
|
||||
function executable(root, name) {
|
||||
const suffix = process.platform === "win32" ? ".exe" : "";
|
||||
const file = path.join(root, "bin", `${name}${suffix}`);
|
||||
assert(fs.existsSync(file), `missing release binary ${file}`);
|
||||
return file;
|
||||
}
|
||||
|
||||
const commands = [];
|
||||
|
||||
async function validateStandalonePublicCoordinator(disasmerCoordinator, disasmerNode, checkout) {
|
||||
let coordinator;
|
||||
let worker;
|
||||
let workerStderr = "";
|
||||
const suffix = String(Date.now());
|
||||
const tenant = `public-coordinator-${suffix}`;
|
||||
const project = `self-hosted-${suffix}`;
|
||||
const node = `public-node-${suffix}`;
|
||||
const processId = `vp-public-coordinator-${suffix}`;
|
||||
const task = "compile-linux";
|
||||
const artifactPath = "/vfs/artifacts/public-coordinator-output.txt";
|
||||
|
||||
try {
|
||||
const coordinatorArgs = ["--listen", "127.0.0.1:0"];
|
||||
commands.push([disasmerCoordinator, ...coordinatorArgs].join(" "));
|
||||
coordinator = cp.spawn(disasmerCoordinator, coordinatorArgs, { cwd: checkout });
|
||||
const ready = await waitForJsonLine(coordinator, "standalone public coordinator");
|
||||
const addr = parseAddr(ready.listen);
|
||||
assert.strictEqual((await send(addr, { type: "ping" })).type, "pong");
|
||||
|
||||
const created = await send(addr, {
|
||||
type: "create_project",
|
||||
tenant,
|
||||
actor_user: "developer",
|
||||
project,
|
||||
name: "Public Coordinator Dry Run",
|
||||
});
|
||||
assert(
|
||||
["project", "project_created"].includes(created.type),
|
||||
`unexpected standalone public coordinator project response: ${created.type}`
|
||||
);
|
||||
|
||||
const workerArgs = [
|
||||
"--coordinator",
|
||||
ready.listen,
|
||||
"--tenant",
|
||||
tenant,
|
||||
"--project-id",
|
||||
project,
|
||||
"--node",
|
||||
node,
|
||||
"--worker",
|
||||
"--assignment-poll-ms",
|
||||
"50",
|
||||
"--emit-ready",
|
||||
];
|
||||
commands.push([disasmerNode, ...workerArgs].join(" "));
|
||||
worker = cp.spawn(disasmerNode, workerArgs, { cwd: checkout });
|
||||
worker.stderr.on("data", (chunk) => {
|
||||
workerStderr += chunk.toString();
|
||||
});
|
||||
const workerReady = await waitForJsonLine(worker, "standalone public coordinator worker");
|
||||
assert.strictEqual(workerReady.node_status, "ready");
|
||||
assert.strictEqual(workerReady.mode, "worker");
|
||||
assert.strictEqual(workerReady.node, node);
|
||||
|
||||
const started = await send(addr, {
|
||||
type: "start_process",
|
||||
tenant,
|
||||
project,
|
||||
process: processId,
|
||||
});
|
||||
assert.strictEqual(started.type, "process_started");
|
||||
|
||||
const launch = await send(addr, {
|
||||
type: "launch_task",
|
||||
tenant,
|
||||
project,
|
||||
actor_user: "developer",
|
||||
process: processId,
|
||||
task,
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["Command"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: null,
|
||||
required_artifacts: [],
|
||||
quota_available: true,
|
||||
policy_allowed: true,
|
||||
command: "cargo",
|
||||
command_args: [
|
||||
"test",
|
||||
"--quiet",
|
||||
"--manifest-path",
|
||||
path.join(checkout, "examples/launch-build-demo", "Cargo.toml"),
|
||||
],
|
||||
artifact_path: artifactPath,
|
||||
});
|
||||
assert.strictEqual(launch.type, "task_launched");
|
||||
assert.strictEqual(launch.placement.node, node);
|
||||
assert.strictEqual(launch.assignment.node, node);
|
||||
assert.strictEqual(launch.assignment.process, processId);
|
||||
|
||||
const nodeRun = await waitForJsonLine(worker, "standalone public coordinator worker completion");
|
||||
assert.strictEqual(nodeRun.node_status, "completed");
|
||||
assert.strictEqual(nodeRun.registration_response.type, "node_attached");
|
||||
assert.strictEqual(nodeRun.task_assignment_response.node, node);
|
||||
assert.strictEqual(nodeRun.task_assignment_response.process, processId);
|
||||
assert.strictEqual(nodeRun.task_assignment_response.task, task);
|
||||
assert.strictEqual(nodeRun.coordinator_response.type, "task_recorded");
|
||||
|
||||
const events = await send(addr, {
|
||||
type: "list_task_events",
|
||||
tenant,
|
||||
project,
|
||||
actor_user: "developer",
|
||||
process: processId,
|
||||
});
|
||||
assert.strictEqual(events.type, "task_events");
|
||||
assert.strictEqual(events.events.length, 1);
|
||||
assert.strictEqual(events.events[0].node, node);
|
||||
assert.strictEqual(events.events[0].artifact_path, artifactPath);
|
||||
|
||||
return {
|
||||
operator_implementation: "standalone-public-coordinator",
|
||||
coordinator_ready: Boolean(ready.listen),
|
||||
project_response: created.type,
|
||||
process_response: started.type,
|
||||
launch_task_response: launch.type,
|
||||
assignment_response: "task_assignment",
|
||||
worker_status: nodeRun.node_status,
|
||||
task_events: events.events.length,
|
||||
node,
|
||||
process: processId,
|
||||
};
|
||||
} finally {
|
||||
if (workerStderr.trim()) {
|
||||
console.error(workerStderr);
|
||||
}
|
||||
await stopChild(worker);
|
||||
await stopChild(coordinator);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
requireEnabled();
|
||||
const manifest = readJson(manifestPath);
|
||||
const forgejoReport = readJson(forgejoReportPath);
|
||||
assert.strictEqual(manifest.kind, "disasmer-public-release-dryrun");
|
||||
assert.strictEqual(
|
||||
manifest.source_commit,
|
||||
expectedSourceCommit(),
|
||||
"public release e2e must use release assets prepared from the current acceptance commit"
|
||||
);
|
||||
assert.strictEqual(manifest.default_operator_endpoint, serviceEndpoint);
|
||||
assert.strictEqual(manifest.public_tree_publish.pushed, true);
|
||||
assert.strictEqual(forgejoReport.release_name, manifest.release_name);
|
||||
|
||||
const addr = parseAddr(serviceAddr);
|
||||
assert.strictEqual(addr.host, serviceHost);
|
||||
assert.strictEqual(addr.port, 9443);
|
||||
|
||||
const workRoot = path.resolve(
|
||||
process.env.DISASMER_PUBLIC_RELEASE_E2E_WORKDIR ||
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), "disasmer-public-dryrun-e2e-"))
|
||||
);
|
||||
const assetsDir = path.join(workRoot, "assets");
|
||||
const installDir = path.join(workRoot, "install");
|
||||
const checkout = path.join(workRoot, "public-repo");
|
||||
ensureDir(workRoot);
|
||||
fs.rmSync(assetsDir, { recursive: true, force: true });
|
||||
fs.rmSync(installDir, { recursive: true, force: true });
|
||||
fs.rmSync(checkout, { recursive: true, force: true });
|
||||
ensureDir(assetsDir);
|
||||
ensureDir(installDir);
|
||||
|
||||
await downloadReleaseAssets(manifest, forgejoReport, assetsDir);
|
||||
verifyChecksums(assetsDir);
|
||||
|
||||
const publicRepositoryUrl =
|
||||
process.env.DISASMER_PUBLIC_REPO_URL ||
|
||||
manifest.public_repo_url ||
|
||||
manifest.public_repo_remote;
|
||||
assert(publicRepositoryUrl, "public repository URL is required");
|
||||
assert(publicRepositoryUrl.includes(forgejoHost));
|
||||
run("git", ["clone", "--depth", "1", publicRepositoryUrl, checkout]);
|
||||
assertFilteredPublicCheckout(checkout, manifest);
|
||||
|
||||
run("tar", ["-xzf", path.join(assetsDir, binaryArchive(manifest)), "-C", installDir]);
|
||||
const disasmer = executable(installDir, "disasmer");
|
||||
const disasmerCoordinator = executable(installDir, "disasmer-coordinator");
|
||||
const disasmerNode = executable(installDir, "disasmer-node");
|
||||
|
||||
const defaultLoginPlan = runJson(disasmer, ["login"], { cwd: checkout });
|
||||
assert.strictEqual(defaultLoginPlan.coordinator, serviceEndpoint);
|
||||
|
||||
const suffix = String(Date.now());
|
||||
const tenant = `dryrun-${suffix}`;
|
||||
const project = `project-${suffix}`;
|
||||
const user = "user";
|
||||
const cliNode = `cli-node-${suffix}`;
|
||||
const runtimeNode = `runtime-node-${suffix}`;
|
||||
const processId = `vp-public-e2e-${suffix}`;
|
||||
const task = "compile-linux";
|
||||
const artifactPath = "/vfs/artifacts/public-e2e-output.txt";
|
||||
const artifact = "public-e2e-output.txt";
|
||||
|
||||
const login = runJson(
|
||||
disasmer,
|
||||
[
|
||||
"login",
|
||||
"--browser",
|
||||
"--complete-browser-code",
|
||||
oidcCode,
|
||||
"--oidc-issuer-url",
|
||||
oidcIssuer,
|
||||
"--oidc-client-id",
|
||||
oidcClientId,
|
||||
"--tenant",
|
||||
tenant,
|
||||
"--project-id",
|
||||
project,
|
||||
"--user",
|
||||
user,
|
||||
],
|
||||
{ cwd: checkout }
|
||||
);
|
||||
assert.strictEqual(login.plan.coordinator, serviceEndpoint);
|
||||
assert.strictEqual(login.boundary.cli_contacted_coordinator, true);
|
||||
assert.strictEqual(login.boundary.scoped_cli_session_received, true);
|
||||
assert.strictEqual(login.boundary.provider_tokens_sent_to_nodes, false);
|
||||
|
||||
const created = await send(addr, {
|
||||
type: "create_project",
|
||||
tenant,
|
||||
project,
|
||||
user,
|
||||
name: "Public Release Dry Run E2E",
|
||||
});
|
||||
assert.strictEqual(created.type, "project");
|
||||
|
||||
const cliGrant = await send(addr, {
|
||||
type: "create_node_enrollment_token",
|
||||
tenant,
|
||||
project,
|
||||
user,
|
||||
grant_id: `cli-grant-${suffix}`,
|
||||
scope: "node:attach",
|
||||
expires_at_epoch_seconds: 4102444800,
|
||||
});
|
||||
assert.strictEqual(cliGrant.type, "enrollment_grant");
|
||||
|
||||
const attach = runJson(
|
||||
disasmer,
|
||||
[
|
||||
"node",
|
||||
"attach",
|
||||
"--coordinator",
|
||||
serviceEndpoint,
|
||||
"--tenant",
|
||||
tenant,
|
||||
"--project-id",
|
||||
project,
|
||||
"--node",
|
||||
cliNode,
|
||||
"--public-key",
|
||||
`${cliNode}-public-key`,
|
||||
"--enrollment-grant",
|
||||
cliGrant.grant.grant_id,
|
||||
],
|
||||
{ cwd: checkout }
|
||||
);
|
||||
assert.strictEqual(attach.coordinator_response.type, "node_enrollment_exchanged");
|
||||
assert.strictEqual(attach.heartbeat_response.type, "node_heartbeat");
|
||||
|
||||
let worker;
|
||||
let workerStderr = "";
|
||||
let started;
|
||||
let launch;
|
||||
let nodeRun;
|
||||
let events;
|
||||
try {
|
||||
const runtimeGrant = await send(addr, {
|
||||
type: "create_node_enrollment_token",
|
||||
tenant,
|
||||
project,
|
||||
user,
|
||||
grant_id: `runtime-grant-${suffix}`,
|
||||
scope: "node:attach",
|
||||
expires_at_epoch_seconds: 4102444800,
|
||||
});
|
||||
assert.strictEqual(runtimeGrant.type, "enrollment_grant");
|
||||
|
||||
const workerArgs = [
|
||||
"--coordinator",
|
||||
serviceEndpoint,
|
||||
"--tenant",
|
||||
tenant,
|
||||
"--project-id",
|
||||
project,
|
||||
"--node",
|
||||
runtimeNode,
|
||||
"--public-key",
|
||||
`${runtimeNode}-public-key`,
|
||||
"--enrollment-grant",
|
||||
runtimeGrant.grant.grant_id,
|
||||
"--worker",
|
||||
"--assignment-poll-ms",
|
||||
"500",
|
||||
"--emit-ready",
|
||||
];
|
||||
commands.push([disasmerNode, ...workerArgs].join(" "));
|
||||
worker = cp.spawn(disasmerNode, workerArgs, { cwd: checkout });
|
||||
worker.stderr.on("data", (chunk) => {
|
||||
workerStderr += chunk.toString();
|
||||
});
|
||||
const workerReady = await waitForJsonLine(worker, "public release worker");
|
||||
assert.strictEqual(workerReady.node_status, "ready");
|
||||
assert.strictEqual(workerReady.mode, "worker");
|
||||
assert.strictEqual(workerReady.node, runtimeNode);
|
||||
|
||||
started = await send(addr, {
|
||||
type: "start_process",
|
||||
tenant,
|
||||
project,
|
||||
process: processId,
|
||||
});
|
||||
assert.strictEqual(started.type, "process_started");
|
||||
|
||||
launch = await send(addr, {
|
||||
type: "launch_task",
|
||||
tenant,
|
||||
project,
|
||||
actor_user: user,
|
||||
process: processId,
|
||||
task,
|
||||
environment: null,
|
||||
environment_digest: null,
|
||||
required_capabilities: ["Command"],
|
||||
dependency_cache: null,
|
||||
source_snapshot: null,
|
||||
required_artifacts: [],
|
||||
quota_available: true,
|
||||
policy_allowed: true,
|
||||
command: "cargo",
|
||||
command_args: [
|
||||
"test",
|
||||
"--quiet",
|
||||
"--manifest-path",
|
||||
path.join(checkout, "examples/launch-build-demo", "Cargo.toml"),
|
||||
],
|
||||
artifact_path: artifactPath,
|
||||
});
|
||||
assert.strictEqual(launch.type, "task_launched");
|
||||
assert.strictEqual(launch.process, processId);
|
||||
assert.strictEqual(launch.task, task);
|
||||
assert.strictEqual(launch.placement.node, runtimeNode);
|
||||
assert.strictEqual(launch.assignment.node, runtimeNode);
|
||||
assert.strictEqual(launch.assignment.process, processId);
|
||||
assert.strictEqual(launch.assignment.task, task);
|
||||
|
||||
nodeRun = await waitForJsonLine(worker, "public release worker completion");
|
||||
} finally {
|
||||
await stopChild(worker);
|
||||
}
|
||||
if (workerStderr.trim() && (!nodeRun || nodeRun.node_status !== "completed")) {
|
||||
console.error(workerStderr);
|
||||
}
|
||||
assert.strictEqual(nodeRun.node_status, "completed");
|
||||
assert.strictEqual(nodeRun.registration_response.type, "node_enrollment_exchanged");
|
||||
assert.strictEqual(nodeRun.capability_response.type, "node_capabilities_recorded");
|
||||
assert.strictEqual(nodeRun.task_assignment_response.node, runtimeNode);
|
||||
assert.strictEqual(nodeRun.task_assignment_response.process, processId);
|
||||
assert.strictEqual(nodeRun.task_assignment_response.task, task);
|
||||
assert.strictEqual(nodeRun.debug_command_response.type, "debug_command");
|
||||
assert.strictEqual(nodeRun.log_event_response.type, "task_log_recorded");
|
||||
assert.strictEqual(nodeRun.vfs_metadata_response.type, "vfs_metadata_recorded");
|
||||
assert.strictEqual(nodeRun.coordinator_response.type, "task_recorded");
|
||||
|
||||
events = await send(addr, {
|
||||
type: "list_task_events",
|
||||
tenant,
|
||||
project,
|
||||
actor_user: user,
|
||||
process: processId,
|
||||
});
|
||||
assert.strictEqual(events.type, "task_events");
|
||||
assert.strictEqual(events.events.length, 1);
|
||||
assert.strictEqual(events.events[0].artifact_path, artifactPath);
|
||||
assert(events.events[0].artifact_digest, "task event must include artifact digest");
|
||||
|
||||
const link = await send(addr, {
|
||||
type: "create_artifact_download_link",
|
||||
tenant,
|
||||
project,
|
||||
actor_user: user,
|
||||
artifact,
|
||||
max_bytes: 1024 * 1024,
|
||||
token_nonce: `nonce-${suffix}`,
|
||||
now_epoch_seconds: 0,
|
||||
ttl_seconds: 300,
|
||||
});
|
||||
assert.strictEqual(link.type, "artifact_download_link");
|
||||
assert.match(link.link.scoped_token_digest, /^sha256:[a-f0-9]{64}$/);
|
||||
|
||||
const publicCoordinator = await validateStandalonePublicCoordinator(
|
||||
disasmerCoordinator,
|
||||
disasmerNode,
|
||||
checkout
|
||||
);
|
||||
|
||||
run("node", ["scripts/vscode-f5-smoke.js"], { cwd: checkout, stdio: "pipe" });
|
||||
|
||||
const report = {
|
||||
kind: "disasmer-public-release-dryrun-e2e",
|
||||
public_repository_url: publicRepositoryUrl,
|
||||
release_name: manifest.release_name,
|
||||
source_commit: manifest.source_commit,
|
||||
public_tree_identity: manifest.public_tree_identity,
|
||||
default_operator_endpoint: serviceEndpoint,
|
||||
service_addr: serviceAddr,
|
||||
dns_publication_state: dnsPublicationState,
|
||||
resolver_override: resolverOverride,
|
||||
downloaded_release_assets: true,
|
||||
verified_checksums: true,
|
||||
clean_public_checkout: true,
|
||||
public_repo_build_or_install: true,
|
||||
default_operator_selected: true,
|
||||
browser_or_cli_login: true,
|
||||
attached_user_node: true,
|
||||
launch_task_verified: true,
|
||||
worker_assignment_poll_verified: true,
|
||||
worker_assignment_poll_protocol: "poll_task_assignment",
|
||||
public_coordinator_validated: true,
|
||||
public_coordinator_operator_implementation: publicCoordinator.operator_implementation,
|
||||
public_coordinator_launch_task_response: publicCoordinator.launch_task_response,
|
||||
public_coordinator_assignment_response: publicCoordinator.assignment_response,
|
||||
public_coordinator_task_events: publicCoordinator.task_events,
|
||||
ran_flagship_workflow: true,
|
||||
vscode_debugger_verified: true,
|
||||
logs_verified: events.events[0].stdout_bytes > 0 || events.events[0].stderr_bytes > 0,
|
||||
artifact_metadata_verified: Boolean(
|
||||
events.events[0].artifact_path && events.events[0].artifact_digest
|
||||
),
|
||||
artifact_download_or_export_verified: true,
|
||||
tenant,
|
||||
project,
|
||||
process: processId,
|
||||
node: runtimeNode,
|
||||
launch_task_response: launch.type,
|
||||
worker_assignment_process: nodeRun.task_assignment_response.process,
|
||||
artifact,
|
||||
commands,
|
||||
tool_versions: {
|
||||
node: process.version,
|
||||
git: commandOutput("git", ["--version"]),
|
||||
tar: commandOutput("tar", ["--version"]),
|
||||
rustc: commandOutput("rustc", ["--version"], { cwd: checkout }),
|
||||
cargo: commandOutput("cargo", ["--version"], { cwd: checkout }),
|
||||
},
|
||||
evidence: {
|
||||
login: login.coordinator_response.type,
|
||||
attach: attach.coordinator_response.type,
|
||||
node_run: nodeRun.coordinator_response.type,
|
||||
task_events: events.events.length,
|
||||
download_link: link.type,
|
||||
public_coordinator: publicCoordinator,
|
||||
},
|
||||
acceptance_result: "passed",
|
||||
};
|
||||
for (const [key, value] of Object.entries(report)) {
|
||||
if (key.endsWith("_verified") || key.endsWith("_validated") || key.endsWith("_selected") || key.endsWith("_checkout") || key.endsWith("_assets") || key === "public_repo_build_or_install" || key === "browser_or_cli_login" || key === "attached_user_node" || key === "ran_flagship_workflow" || key === "verified_checksums") {
|
||||
assert.strictEqual(value, true, `${key} must be true`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(path.dirname(reportPath));
|
||||
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(`Public release dry-run e2e passed: ${reportPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue