Public release release-e47f9c27bbeb
Source commit: e47f9c27bbebe6759f6b6fe7b12fd00bb20d116d Public tree identity: sha256:4c1af1dfd67d3f2b5088531ea8727b11124b013d2251a4d5088f51a6424c0196
This commit is contained in:
parent
3a4d4fa7ae
commit
9223c54939
30 changed files with 4195 additions and 434 deletions
|
|
@ -47,12 +47,12 @@ function agentWorkflowSignatureMessage({
|
|||
|
||||
function signedAgentWorkflowProof(identity, request, options = {}) {
|
||||
const nonce =
|
||||
options.nonce ||
|
||||
options.nonce ??
|
||||
`${request.type}-${process.pid}-${Date.now()}-${crypto
|
||||
.randomBytes(8)
|
||||
.toString("hex")}`;
|
||||
const issuedAtEpochSeconds =
|
||||
options.issuedAtEpochSeconds || Math.floor(Date.now() / 1000);
|
||||
options.issuedAtEpochSeconds ?? Math.floor(Date.now() / 1000);
|
||||
const processId =
|
||||
request.type === "launch_task" ? request.task_spec?.process : request.process;
|
||||
const task =
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,6 +14,55 @@ const { DapClient } = require("./dap-client");
|
|||
sourceLines.findIndex((line) => line.includes("pub async fn build_main()")) + 1;
|
||||
assert(buildMainLine > 0, "flagship source must contain build_main");
|
||||
|
||||
const hostileDapClient = new DapClient();
|
||||
try {
|
||||
const initialize = hostileDapClient.send("initialize", {
|
||||
adapterID: "clusterflux",
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true,
|
||||
});
|
||||
await hostileDapClient.response(initialize, "initialize");
|
||||
for (const processId of [
|
||||
"",
|
||||
" ",
|
||||
"bad\u0000process",
|
||||
"bad process!",
|
||||
"x".repeat(256),
|
||||
]) {
|
||||
const malformedLaunch = hostileDapClient.send("launch", {
|
||||
entry: "build",
|
||||
project,
|
||||
runtimeBackend: "local-services",
|
||||
processId,
|
||||
});
|
||||
const rejection = await hostileDapClient.failure(
|
||||
malformedLaunch,
|
||||
"launch"
|
||||
);
|
||||
assert.match(
|
||||
rejection.message,
|
||||
/invalid DAP processId: ProcessId is invalid/,
|
||||
`unexpected malformed DAP identifier response: ${JSON.stringify(rejection)}`
|
||||
);
|
||||
}
|
||||
const validLaunch = hostileDapClient.send("launch", {
|
||||
entry: "build",
|
||||
project,
|
||||
runtimeBackend: "local-services",
|
||||
processId: "vp-valid-after-malformed",
|
||||
});
|
||||
await hostileDapClient.response(validLaunch, "launch");
|
||||
await hostileDapClient.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
await hostileDapClient.close();
|
||||
} catch (error) {
|
||||
if (hostileDapClient.child.exitCode === null) {
|
||||
hostileDapClient.child.kill("SIGKILL");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const asynchronousClient = new DapClient();
|
||||
try {
|
||||
const initialize = asynchronousClient.send("initialize", {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,53 @@
|
|||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
agentIdentity,
|
||||
signedAgentWorkflowRequest,
|
||||
} = require("./agent-signing");
|
||||
const {
|
||||
nodeIdentity,
|
||||
signedNodeHeartbeat,
|
||||
} = require("./node-signing");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
const signingInstrumentAgent = agentIdentity(
|
||||
"hostile-input-contract-agent",
|
||||
"agent-hostile-input-contract"
|
||||
);
|
||||
const explicitlyEmptyAgentNonce = signedAgentWorkflowRequest(
|
||||
signingInstrumentAgent,
|
||||
{
|
||||
type: "start_process",
|
||||
tenant: "tenant",
|
||||
project: "project",
|
||||
actor_agent: "agent-hostile-input-contract",
|
||||
process: "process",
|
||||
launch_attempt: "attempt",
|
||||
restart: false,
|
||||
},
|
||||
{ nonce: "" }
|
||||
);
|
||||
assert.strictEqual(
|
||||
explicitlyEmptyAgentNonce.agent_signature.nonce,
|
||||
"",
|
||||
"Agent signing instrument replaced an explicitly empty hostile nonce"
|
||||
);
|
||||
const signingInstrumentNode = nodeIdentity(
|
||||
"hostile-input-contract-node",
|
||||
"node-hostile-input-contract"
|
||||
);
|
||||
assert.strictEqual(
|
||||
signedNodeHeartbeat(
|
||||
"node-hostile-input-contract",
|
||||
signingInstrumentNode,
|
||||
{ nonce: "" }
|
||||
).nonce,
|
||||
"",
|
||||
"Node signing instrument replaced an explicitly empty hostile nonce"
|
||||
);
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(repo, relativePath), "utf8");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ function nodeSignatureMessage(
|
|||
|
||||
function signedNodeProof(node, identity, requestKind, request, options = {}) {
|
||||
const nonce =
|
||||
options.nonce ||
|
||||
options.nonce ??
|
||||
`${requestKind}-${process.pid}-${Date.now()}-${crypto
|
||||
.randomBytes(8)
|
||||
.toString("hex")}`;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function copyVerifiedAsset(asset, destinationRoot) {
|
|||
return destination;
|
||||
}
|
||||
|
||||
function extractBinaries(archive, destinationRoot) {
|
||||
function extractBinaries(archive, destinationRoot, expectedDigests) {
|
||||
const staging = path.join(destinationRoot, ".binary-extract");
|
||||
fs.mkdirSync(staging, { recursive: true });
|
||||
const extracted = spawnSync("tar", ["-xzf", archive, "-C", staging], {
|
||||
|
|
@ -69,6 +69,11 @@ function extractBinaries(archive, destinationRoot) {
|
|||
const destination = path.join(destinationRoot, "binaries", name);
|
||||
fs.copyFileSync(source, destination);
|
||||
fs.chmodSync(destination, 0o755);
|
||||
assert.strictEqual(
|
||||
`sha256:${sha256(destination)}`,
|
||||
expectedDigests[name],
|
||||
`tested binary digest changed: ${name}`
|
||||
);
|
||||
copied.push(destination);
|
||||
}
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
|
|
@ -87,11 +92,21 @@ function main() {
|
|||
assert.strictEqual(result.source_tree_digest, manifest.source_tree_digest);
|
||||
assert.deepStrictEqual(result.binary_digests, manifest.binary_digests);
|
||||
assert.strictEqual(result.scenario_skips.length, 0);
|
||||
assert.strictEqual(result.strict_requirement_ledger.length, 25);
|
||||
assert(result.strict_requirement_ledger.length >= 29);
|
||||
assert(
|
||||
result.strict_requirement_ledger.every((requirement) => requirement.passed === true),
|
||||
result.strict_requirement_ledger.every(
|
||||
(requirement) =>
|
||||
requirement.passed === true &&
|
||||
Number.isFinite(requirement.duration_ms) &&
|
||||
requirement.duration_ms > 0 &&
|
||||
requirement.evidence
|
||||
),
|
||||
"final validation contains a failed launch requirement"
|
||||
);
|
||||
assert.strictEqual(
|
||||
result.named_scenarios.length,
|
||||
result.strict_requirement_ledger.length
|
||||
);
|
||||
|
||||
const destinationRoot = path.resolve(
|
||||
process.env.CLUSTERFLUX_GITHUB_RELEASE_DIR ||
|
||||
|
|
@ -104,11 +119,26 @@ function main() {
|
|||
const copiedAssets = manifest.assets.map((asset) =>
|
||||
copyVerifiedAsset(asset, destinationRoot)
|
||||
);
|
||||
const publicationGuidance = [
|
||||
JSON.stringify(manifest.notes || []),
|
||||
...copiedAssets
|
||||
.filter((file) => file.endsWith(".md"))
|
||||
.map((file) => fs.readFileSync(file, "utf8")),
|
||||
].join("\n");
|
||||
assert.doesNotMatch(
|
||||
publicationGuidance,
|
||||
/(?:download|upload|release downloads?)[^\n]*Forgejo/i,
|
||||
"manual GitHub package contains Forgejo release-publication instructions"
|
||||
);
|
||||
const binaryArchive = copiedAssets.find((file) =>
|
||||
path.basename(file).startsWith("clusterflux-public-binaries-")
|
||||
);
|
||||
assert(binaryArchive, "final manifest omitted the public binary archive");
|
||||
const binaries = extractBinaries(binaryArchive, destinationRoot);
|
||||
const binaries = extractBinaries(
|
||||
binaryArchive,
|
||||
destinationRoot,
|
||||
manifest.binary_digests
|
||||
);
|
||||
const vsix = copiedAssets.find((file) => file.endsWith(".vsix"));
|
||||
assert(vsix, "final manifest omitted the tested VSIX");
|
||||
assert.strictEqual(
|
||||
|
|
@ -124,7 +154,6 @@ function main() {
|
|||
);
|
||||
write(path.join(destinationRoot, "VSIX_SHA256"), `${sha256(vsix)} ${path.basename(vsix)}\n`);
|
||||
fs.copyFileSync(manifestPath, path.join(destinationRoot, "public-release-manifest.json"));
|
||||
fs.copyFileSync(resultPath, path.join(destinationRoot, "final-result.json"));
|
||||
fs.copyFileSync(transcriptPath, path.join(destinationRoot, "VALIDATION_TRANSCRIPT.log"));
|
||||
write(
|
||||
path.join(destinationRoot, "RELEASE_NOTES.md"),
|
||||
|
|
@ -138,6 +167,31 @@ function main() {
|
|||
"- Full Windows sandbox validation is deferred.\n" +
|
||||
"- Providers, billing, teams, managed compute, and operator-panel UI are not part of this release.\n"
|
||||
);
|
||||
const testedUploadAssets = [
|
||||
...copiedAssets,
|
||||
...binaries,
|
||||
]
|
||||
.sort()
|
||||
.map((file) => ({
|
||||
file: path.relative(destinationRoot, file),
|
||||
sha256: `sha256:${sha256(file)}`,
|
||||
}));
|
||||
write(
|
||||
path.join(destinationRoot, "final-result.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
...result,
|
||||
manual_release: {
|
||||
source_revision: manifest.source_commit,
|
||||
source_tree_digest: manifest.source_tree_digest,
|
||||
tested_upload_assets: testedUploadAssets,
|
||||
publication: "manual GitHub upload from this directory",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
|
||||
const checksummed = [
|
||||
...copiedAssets,
|
||||
|
|
|
|||
|
|
@ -632,10 +632,22 @@ function stageEvidenceAsset(
|
|||
}
|
||||
if (
|
||||
!Array.isArray(liveResult.named_scenarios) ||
|
||||
liveResult.named_scenarios.length !== 25 ||
|
||||
liveResult.named_scenarios.some((scenario) => scenario.status !== "passed")
|
||||
!Array.isArray(liveResult.strict_requirement_ledger) ||
|
||||
liveResult.named_scenarios.length !==
|
||||
liveResult.strict_requirement_ledger.length ||
|
||||
liveResult.named_scenarios.length < 29 ||
|
||||
liveResult.named_scenarios.some(
|
||||
(scenario) =>
|
||||
scenario.status !== "passed" ||
|
||||
!Number.isFinite(scenario.duration_ms) ||
|
||||
scenario.duration_ms <= 0
|
||||
) ||
|
||||
new Set(liveResult.named_scenarios.map((scenario) => scenario.id))
|
||||
.size !== liveResult.named_scenarios.length
|
||||
) {
|
||||
throw new Error("all twenty-five named final live checks must pass");
|
||||
throw new Error(
|
||||
"every named final live check must be unique, measured, and passed"
|
||||
);
|
||||
}
|
||||
if (
|
||||
!liveResult.release_binding?.deployment ||
|
||||
|
|
@ -649,7 +661,17 @@ function stageEvidenceAsset(
|
|||
if (
|
||||
!Array.isArray(liveResult.strict_requirement_ledger) ||
|
||||
!liveResult.strict_requirement_ledger.length ||
|
||||
liveResult.strict_requirement_ledger.some((requirement) => requirement.passed !== true)
|
||||
liveResult.strict_requirement_ledger.some(
|
||||
(requirement) =>
|
||||
requirement.passed !== true ||
|
||||
!Number.isFinite(requirement.duration_ms) ||
|
||||
requirement.duration_ms <= 0 ||
|
||||
!requirement.evidence
|
||||
) ||
|
||||
liveResult.strict_requirement_ledger.some(
|
||||
(requirement, index) =>
|
||||
requirement.id !== liveResult.named_scenarios[index].id
|
||||
)
|
||||
) {
|
||||
throw new Error("the strict final requirement ledger must be complete and passed");
|
||||
}
|
||||
|
|
@ -685,8 +707,7 @@ function stageEvidenceAsset(
|
|||
evidence_packaged_at: packagedAt,
|
||||
},
|
||||
release_commands: [
|
||||
"nix develop -c ./scripts/acceptance-private.sh",
|
||||
"nix develop -c ./scripts/acceptance-public.sh",
|
||||
"nix develop -c node scripts/release-quality-gates.js",
|
||||
"node scripts/cli-happy-path-live-smoke.js (strict production-shaped environment)",
|
||||
],
|
||||
};
|
||||
|
|
@ -827,7 +848,7 @@ ${resolution}
|
|||
|
||||
## Install
|
||||
|
||||
1. Download the binary archive for your platform from the Forgejo release.
|
||||
1. Download the binary archive for your platform from the manually published GitHub release.
|
||||
2. Check the archive against \`SHA256SUMS\`.
|
||||
3. Extract it and put \`bin/\` on your \`PATH\`.
|
||||
4. Install \`clusterflux-vscode-*.vsix\` when you want the VS Code debugger:
|
||||
|
|
@ -880,8 +901,8 @@ function writeReleaseNotesAsset(releaseName, publicTreeIdentity, resolution) {
|
|||
process.env.CLUSTERFLUX_PUBLIC_REPO_URL ||
|
||||
"https://git.michelpaulissen.com/michel/clusterflux-public";
|
||||
const releaseUrl =
|
||||
process.env.CLUSTERFLUX_FORGEJO_RELEASE_URL ||
|
||||
"the Forgejo release attached to the public repository";
|
||||
process.env.CLUSTERFLUX_GITHUB_RELEASE_URL ||
|
||||
"the manually published GitHub release";
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
`# Clusterflux Release Notes
|
||||
|
|
@ -1200,7 +1221,8 @@ function main() {
|
|||
public_repo_remote:
|
||||
process.env.CLUSTERFLUX_PUBLIC_REPO_REMOTE || candidate?.public_repo_remote || null,
|
||||
public_tree_publish: publicTreePublish,
|
||||
forgejo_release_url: process.env.CLUSTERFLUX_FORGEJO_RELEASE_URL || null,
|
||||
github_release_url: process.env.CLUSTERFLUX_GITHUB_RELEASE_URL || null,
|
||||
forgejo_release_url: null,
|
||||
default_hosted_coordinator_endpoint: defaultHostedCoordinatorEndpoint,
|
||||
dns_publication_state:
|
||||
process.env.CLUSTERFLUX_DNS_PUBLICATION_STATE || "published",
|
||||
|
|
@ -1239,10 +1261,18 @@ function main() {
|
|||
name: path.basename(asset),
|
||||
sha256: sha256File(asset),
|
||||
})),
|
||||
notes: [
|
||||
"Upload the assets to the Forgejo Release for the filtered public repository.",
|
||||
"The real service deployment and full e2e release remain separate acceptance evidence.",
|
||||
],
|
||||
notes:
|
||||
releaseStage === "final"
|
||||
? [
|
||||
"Publish manually to GitHub from the prepared manual release directory.",
|
||||
"The manifest embeds the exact production-shaped validation evidence and candidate binding.",
|
||||
"Forgejo release publication is not launch authority.",
|
||||
]
|
||||
: [
|
||||
"This is a validation candidate, not a published release.",
|
||||
"GitHub publication remains manual after final production-shaped validation.",
|
||||
"Forgejo release publication is not launch authority.",
|
||||
],
|
||||
};
|
||||
|
||||
const manifestPath = path.join(outputRoot, "public-release-manifest.json");
|
||||
|
|
|
|||
|
|
@ -316,14 +316,27 @@ if (manifest.candidate_proxy_configuration_identity) {
|
|||
}
|
||||
assert(
|
||||
Array.isArray(finalResult.named_scenarios) &&
|
||||
finalResult.named_scenarios.length === 25 &&
|
||||
finalResult.named_scenarios.every((scenario) => scenario.status === "passed"),
|
||||
"all twenty-five named final checks must be present and passed"
|
||||
finalResult.named_scenarios.length >= 29 &&
|
||||
finalResult.named_scenarios.length ===
|
||||
finalResult.strict_requirement_ledger.length &&
|
||||
finalResult.named_scenarios.every(
|
||||
(scenario) =>
|
||||
scenario.status === "passed" &&
|
||||
Number.isFinite(scenario.duration_ms) &&
|
||||
scenario.duration_ms > 0
|
||||
),
|
||||
"all named final checks must be present, measured, and passed"
|
||||
);
|
||||
assert(
|
||||
Array.isArray(finalResult.strict_requirement_ledger) &&
|
||||
finalResult.strict_requirement_ledger.length > 0 &&
|
||||
finalResult.strict_requirement_ledger.every((requirement) => requirement.passed === true),
|
||||
finalResult.strict_requirement_ledger.every(
|
||||
(requirement) =>
|
||||
requirement.passed === true &&
|
||||
Number.isFinite(requirement.duration_ms) &&
|
||||
requirement.duration_ms > 0 &&
|
||||
requirement.evidence
|
||||
),
|
||||
"the strict final requirement ledger must be present and passed"
|
||||
);
|
||||
const finalTranscript = readArchiveMember(
|
||||
|
|
|
|||
322
scripts/release-quality-gates.js
Executable file
322
scripts/release-quality-gates.js
Executable file
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("assert");
|
||||
const cp = require("child_process");
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const manifestPath = path.resolve(
|
||||
process.env.CLUSTERFLUX_PUBLIC_RELEASE_MANIFEST ||
|
||||
path.join(
|
||||
process.env.CLUSTERFLUX_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/release-candidate"),
|
||||
"public-release-manifest.json"
|
||||
)
|
||||
);
|
||||
const evidencePath = path.resolve(
|
||||
process.env.CLUSTERFLUX_QUALITY_GATE_EVIDENCE_PATH ||
|
||||
path.join(repo, "target/acceptance/quality-gates.json")
|
||||
);
|
||||
const transcriptPath = path.resolve(
|
||||
process.env.CLUSTERFLUX_QUALITY_GATE_TRANSCRIPT_PATH ||
|
||||
path.join(repo, "target/acceptance/quality-gates-transcript.txt")
|
||||
);
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function sha256(file) {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(fs.readFileSync(file))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
function commandText(command, args) {
|
||||
return [command, ...args]
|
||||
.map((value) =>
|
||||
/^[A-Za-z0-9_./:=+-]+$/.test(value)
|
||||
? value
|
||||
: JSON.stringify(value)
|
||||
)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function main() {
|
||||
assert(fs.existsSync(manifestPath), `missing release manifest ${manifestPath}`);
|
||||
const manifest = readJson(manifestPath);
|
||||
fs.mkdirSync(path.dirname(evidencePath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(transcriptPath), { recursive: true });
|
||||
const transcript = fs.openSync(transcriptPath, "w");
|
||||
const evidence = {
|
||||
kind: "clusterflux-release-quality-gates",
|
||||
source_commit: manifest.source_commit,
|
||||
source_tree_digest: manifest.source_tree_digest,
|
||||
public_tree_identity: manifest.public_tree_identity,
|
||||
checks: {},
|
||||
};
|
||||
|
||||
const runGroup = (id, commands, extra = {}) => {
|
||||
const startedAt = Date.now();
|
||||
const commandEvidence = [];
|
||||
let status = "passed";
|
||||
for (const command of commands) {
|
||||
const args = command.args || [];
|
||||
const cwd = command.cwd || repo;
|
||||
const text = commandText(command.command, args);
|
||||
fs.writeSync(transcript, `\n[${id}] ${text}\n`);
|
||||
const commandStartedAt = Date.now();
|
||||
const result = cp.spawnSync(command.command, args, {
|
||||
cwd,
|
||||
env: { ...process.env, ...(command.env || {}) },
|
||||
stdio: ["ignore", transcript, transcript],
|
||||
});
|
||||
const durationMs = Math.max(1, Date.now() - commandStartedAt);
|
||||
commandEvidence.push({
|
||||
command: text,
|
||||
cwd,
|
||||
exit_status: result.status,
|
||||
duration_ms: durationMs,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
status = "failed";
|
||||
evidence.checks[id] = {
|
||||
status,
|
||||
duration_ms: Math.max(1, Date.now() - startedAt),
|
||||
commands: commandEvidence,
|
||||
error: result.error?.message || `command exited ${result.status}`,
|
||||
...extra,
|
||||
};
|
||||
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
throw new Error(`${id} failed: ${text}`);
|
||||
}
|
||||
}
|
||||
evidence.checks[id] = {
|
||||
status,
|
||||
duration_ms: Math.max(1, Date.now() - startedAt),
|
||||
commands: commandEvidence,
|
||||
...extra,
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
runGroup("repository_structure", [
|
||||
{ command: path.join(repo, "scripts/check-old-name.sh") },
|
||||
{ command: "node", args: [path.join(repo, "scripts/check-docs.js")] },
|
||||
{ command: path.join(repo, "scripts/check-code-size.sh") },
|
||||
]);
|
||||
runGroup("formatting", [
|
||||
{ command: "cargo", args: ["fmt", "--all", "--check"] },
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"fmt",
|
||||
"--all",
|
||||
"--manifest-path",
|
||||
"private/hosted-policy/Cargo.toml",
|
||||
"--check",
|
||||
],
|
||||
},
|
||||
]);
|
||||
runGroup("clippy_warnings_denied", [
|
||||
{
|
||||
command: "cargo",
|
||||
args: ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"clippy",
|
||||
"--manifest-path",
|
||||
"private/hosted-policy/Cargo.toml",
|
||||
"--all-targets",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
],
|
||||
},
|
||||
]);
|
||||
runGroup("public_workspace_tests", [
|
||||
{
|
||||
command: "cargo",
|
||||
args: ["test", "--workspace", "--all-targets"],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: ["build", "--workspace", "--all-targets"],
|
||||
},
|
||||
]);
|
||||
runGroup("private_hosted_policy_locked_tests", [
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"test",
|
||||
"--locked",
|
||||
"--manifest-path",
|
||||
"private/hosted-policy/Cargo.toml",
|
||||
"--all-targets",
|
||||
],
|
||||
},
|
||||
]);
|
||||
runGroup("process_lifecycle_regressions", [
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"test",
|
||||
"-p",
|
||||
"clusterflux-coordinator",
|
||||
"completed_main_waits_for_final_child_then_preserves_history_and_releases_the_slot",
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"test",
|
||||
"-p",
|
||||
"clusterflux-coordinator",
|
||||
"failed_main_aborts_unfinished_children_and_clears_process_debug_state",
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"test",
|
||||
"-p",
|
||||
"clusterflux-coordinator",
|
||||
"service_cancels_whole_process_and_blocks_new_task_launches",
|
||||
],
|
||||
},
|
||||
]);
|
||||
runGroup("wasm_example_builds", [
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"build",
|
||||
"-p",
|
||||
"hello-build",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"build",
|
||||
"-p",
|
||||
"recovery-build",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
],
|
||||
},
|
||||
{
|
||||
command: "cargo",
|
||||
args: [
|
||||
"build",
|
||||
"-p",
|
||||
"runtime-conformance",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
],
|
||||
},
|
||||
]);
|
||||
runGroup("filtered_public_tree_build_and_tests", [
|
||||
{ command: path.join(repo, "scripts/verify-public-split.sh") },
|
||||
]);
|
||||
|
||||
const extensionAsset = manifest.assets.find((asset) =>
|
||||
asset.name.endsWith(".vsix")
|
||||
);
|
||||
assert(extensionAsset, "candidate manifest omitted its VSIX");
|
||||
const extensionFile = path.resolve(
|
||||
path.dirname(manifestPath),
|
||||
extensionAsset.file
|
||||
);
|
||||
assert(fs.existsSync(extensionFile), `missing candidate VSIX ${extensionFile}`);
|
||||
const extensionDigest = sha256(extensionFile);
|
||||
assert.strictEqual(extensionDigest, extensionAsset.sha256);
|
||||
assert.strictEqual(
|
||||
extensionDigest,
|
||||
manifest.release_candidate.extension_sha256
|
||||
);
|
||||
const extractedVsix = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clusterflux-tested-vsix-")
|
||||
);
|
||||
try {
|
||||
runGroup(
|
||||
"vscode_extension_candidate",
|
||||
[
|
||||
{
|
||||
command: "npm",
|
||||
args: ["ci", "--ignore-scripts"],
|
||||
cwd: path.join(repo, "vscode-extension"),
|
||||
},
|
||||
{
|
||||
command: "unzip",
|
||||
args: ["-q", extensionFile, "-d", extractedVsix],
|
||||
},
|
||||
{
|
||||
command: "node",
|
||||
args: [path.join(repo, "scripts/vscode-extension-smoke.js")],
|
||||
env: {
|
||||
CLUSTERFLUX_VSCODE_EXTENSION_ROOT: path.join(
|
||||
extractedVsix,
|
||||
"extension"
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
candidate_vsix: {
|
||||
file: extensionFile,
|
||||
sha256: extensionDigest,
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(extractedVsix, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const publicCheckIds = [
|
||||
"repository_structure",
|
||||
"formatting",
|
||||
"clippy_warnings_denied",
|
||||
"public_workspace_tests",
|
||||
"process_lifecycle_regressions",
|
||||
"wasm_example_builds",
|
||||
"filtered_public_tree_build_and_tests",
|
||||
"vscode_extension_candidate",
|
||||
];
|
||||
evidence.public = {
|
||||
status: "passed",
|
||||
duration_ms: publicCheckIds.reduce(
|
||||
(total, id) => total + evidence.checks[id].duration_ms,
|
||||
0
|
||||
),
|
||||
independent_filtered_tree: true,
|
||||
};
|
||||
evidence.private = {
|
||||
status: "passed",
|
||||
duration_ms:
|
||||
evidence.checks.formatting.duration_ms +
|
||||
evidence.checks.clippy_warnings_denied.duration_ms +
|
||||
evidence.checks.private_hosted_policy_locked_tests.duration_ms,
|
||||
};
|
||||
evidence.status = "passed";
|
||||
evidence.transcript = transcriptPath;
|
||||
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
|
||||
} finally {
|
||||
fs.closeSync(transcript);
|
||||
}
|
||||
console.log(`Release quality gates passed: ${evidencePath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error.stack || error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -5,16 +5,20 @@ const os = require("os");
|
|||
const path = require("path");
|
||||
const assert = require("assert");
|
||||
|
||||
const extension = require("../vscode-extension/extension");
|
||||
const packageJson = require("../vscode-extension/package.json");
|
||||
const extensionRoot = path.resolve(
|
||||
process.env.CLUSTERFLUX_VSCODE_EXTENSION_ROOT ||
|
||||
path.join(__dirname, "../vscode-extension")
|
||||
);
|
||||
const extension = require(path.join(extensionRoot, "extension.js"));
|
||||
const packageJson = require(path.join(extensionRoot, "package.json"));
|
||||
const extensionSource = fs.readFileSync(
|
||||
path.join(__dirname, "../vscode-extension/extension.js"),
|
||||
path.join(extensionRoot, "extension.js"),
|
||||
"utf8"
|
||||
);
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
|
||||
assert.strictEqual(packageJson.main, "./extension.js");
|
||||
assert(fs.existsSync(path.join(__dirname, "../vscode-extension", packageJson.main)));
|
||||
assert(fs.existsSync(path.join(extensionRoot, packageJson.main)));
|
||||
assert.deepStrictEqual(packageJson.dependencies || {}, {});
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clusterflux-vscode-"));
|
||||
fs.mkdirSync(path.join(root, "envs/linux"), { recursive: true });
|
||||
|
|
@ -157,7 +161,7 @@ assert(
|
|||
"package.json must contribute a Clusterflux activity-bar container"
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(path.join(__dirname, "../vscode-extension/resources/clusterflux.svg")),
|
||||
fs.existsSync(path.join(extensionRoot, "resources/clusterflux.svg")),
|
||||
"Clusterflux activity-bar icon must exist"
|
||||
);
|
||||
const packageViewIds = packageJson.contributes.views.clusterflux.map((view) => view.id).sort();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue