Publish Clusterflux release candidate 6e83783
This commit is contained in:
parent
4d75257c6e
commit
f3640643bd
16 changed files with 4667 additions and 106 deletions
|
|
@ -263,6 +263,163 @@ function sendHostedControl(payload) {
|
|||
});
|
||||
}
|
||||
|
||||
async function runMalformedIdentifierSuite() {
|
||||
const scenarioStartedAt = Date.now();
|
||||
const forms = [
|
||||
{ name: "empty", value: "" },
|
||||
{ name: "whitespace", value: " \t" },
|
||||
{ name: "control", value: "hostile\u0000id" },
|
||||
{ name: "invalid_format", value: "hostile id!" },
|
||||
{ name: "oversized", value: "x".repeat(256) },
|
||||
];
|
||||
const valid = {
|
||||
tenant: "strict-tenant",
|
||||
project: "strict-project",
|
||||
user: "strict-user",
|
||||
agent: "strict-agent",
|
||||
node: "strict-node",
|
||||
process: "strict-process",
|
||||
task: "strict-task",
|
||||
taskDefinition: "strict-task-definition",
|
||||
artifact: "strict-artifact",
|
||||
launchAttempt: "strict-launch-attempt",
|
||||
};
|
||||
const principals = [
|
||||
{
|
||||
name: "tenant",
|
||||
payload: (value) => ({
|
||||
type: "auth_status",
|
||||
tenant: value,
|
||||
actor_user: valid.user,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "project",
|
||||
payload: (value) => ({
|
||||
type: "list_processes",
|
||||
tenant: valid.tenant,
|
||||
project: value,
|
||||
actor_user: valid.user,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
payload: (value) => ({
|
||||
type: "auth_status",
|
||||
tenant: valid.tenant,
|
||||
actor_user: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "agent",
|
||||
payload: (value) => ({
|
||||
type: "start_process",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_agent: value,
|
||||
process: valid.process,
|
||||
restart: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "node",
|
||||
payload: (value) => ({ type: "node_heartbeat", node: value }),
|
||||
},
|
||||
{
|
||||
name: "process",
|
||||
payload: (value) => ({
|
||||
type: "list_task_events",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_user: valid.user,
|
||||
process: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "task_instance",
|
||||
payload: (value) => ({
|
||||
type: "abort_task",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_user: valid.user,
|
||||
process: valid.process,
|
||||
task: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "task_definition",
|
||||
payload: (value) => ({
|
||||
type: "launch_task",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_user: valid.user,
|
||||
process: valid.process,
|
||||
task_spec: {
|
||||
task_definition: value,
|
||||
task_instance: valid.task,
|
||||
environment: "linux",
|
||||
dependencies: [],
|
||||
required_artifacts: [],
|
||||
arguments: [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "artifact",
|
||||
payload: (value) => ({
|
||||
type: "fetch_artifact",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_user: valid.user,
|
||||
artifact: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "launch_attempt",
|
||||
payload: (value) => ({
|
||||
type: "launch_main_runtime",
|
||||
tenant: valid.tenant,
|
||||
project: valid.project,
|
||||
actor_user: valid.user,
|
||||
process: valid.process,
|
||||
launch_attempt: value,
|
||||
}),
|
||||
},
|
||||
];
|
||||
const rejected = [];
|
||||
for (const principal of principals) {
|
||||
for (const form of forms) {
|
||||
const response = await sendHostedControl(principal.payload(form.value));
|
||||
assertDenied(
|
||||
response,
|
||||
/identifier|invalid|empty|whitespace|control|format|255|length/i,
|
||||
`${principal.name} ${form.name}`
|
||||
);
|
||||
const health = await sendHostedControl({ type: "ping" });
|
||||
assert.strictEqual(
|
||||
health.type,
|
||||
"pong",
|
||||
`valid traffic failed after hostile ${principal.name} ${form.name}`
|
||||
);
|
||||
rejected.push({
|
||||
principal: principal.name,
|
||||
form: form.name,
|
||||
rejected: true,
|
||||
health_after: health.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
principals: principals.map((principal) => principal.name),
|
||||
forms: forms.map((form) => form.name),
|
||||
rejected_requests: rejected,
|
||||
valid_after_every_rejection: rejected.every(
|
||||
(entry) => entry.health_after === "pong"
|
||||
),
|
||||
duration_ms: Date.now() - scenarioStartedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function sendHostedControlDroppingResponse(payload) {
|
||||
const body = Buffer.from(
|
||||
JSON.stringify(coordinatorWireRequest(payload, "strict-live-dropped-response"))
|
||||
|
|
@ -1225,8 +1382,30 @@ async function runLiveAgentCredentialSecurity({
|
|||
await debugClient.waitFor(
|
||||
(message) => message.type === "event" && message.event === "initialized"
|
||||
);
|
||||
const attachSourcePath = path.join(projectDir, "src/lib.rs");
|
||||
const attachBreakpointRequest = debugClient.send("setBreakpoints", {
|
||||
source: { path: attachSourcePath },
|
||||
breakpoints: [{ line: 1 }],
|
||||
});
|
||||
const attachBreakpointResponse = await debugClient.response(
|
||||
attachBreakpointRequest,
|
||||
"setBreakpoints"
|
||||
);
|
||||
assert.strictEqual(
|
||||
attachBreakpointResponse.body.breakpoints[0].verified,
|
||||
false,
|
||||
"attach breakpoint was verified before runtime installation"
|
||||
);
|
||||
const configurationDone = debugClient.send("configurationDone");
|
||||
await debugClient.response(configurationDone, "configurationDone");
|
||||
const attachBreakpointInstalled = await debugClient.waitFor(
|
||||
(message) =>
|
||||
message.type === "event" &&
|
||||
message.event === "breakpoint" &&
|
||||
message.body?.breakpoint?.verified === true,
|
||||
120_000
|
||||
);
|
||||
assert.strictEqual(attachBreakpointInstalled.body.breakpoint.line, 1);
|
||||
const attachDeadline = Date.now() + 120_000;
|
||||
let attachedThreads = [];
|
||||
while (Date.now() < attachDeadline) {
|
||||
|
|
@ -2744,6 +2923,7 @@ async function runSameDefinitionDapIdentity({
|
|||
env: {
|
||||
...process.env,
|
||||
CLUSTERFLUX_TEST_DAP_OBSERVER_CONNECTION_LOSS: "1",
|
||||
CLUSTERFLUX_TEST_DAP_POST_COMMIT_OBSERVATION_FAILURE: "1",
|
||||
},
|
||||
});
|
||||
let disconnected = false;
|
||||
|
|
@ -2987,6 +3167,8 @@ async function runLiveDapEditRestart({
|
|||
env: {
|
||||
...process.env,
|
||||
CLUSTERFLUX_TEST_DAP_OBSERVER_CONNECTION_LOSS: "1",
|
||||
CLUSTERFLUX_TEST_DAP_OBSERVER_FALLBACK_FAILURE: "1",
|
||||
CLUSTERFLUX_TEST_DAP_OBSERVER_DEBUG_EPOCH_WAIT_FAILURE: "1",
|
||||
CLUSTERFLUX_TEST_DAP_BREAKPOINT_INSTALLATION_FAILURE: "1",
|
||||
},
|
||||
});
|
||||
|
|
@ -3336,6 +3518,8 @@ async function runLiveDapEditRestart({
|
|||
breakpoint_install_failure_reported: true,
|
||||
observer_idle_request_rate_bound_per_second: 0.8,
|
||||
observer_reconnected_without_false_stop: true,
|
||||
observer_fallback_reconnected: true,
|
||||
observer_debug_epoch_wait_reconnected: true,
|
||||
};
|
||||
} finally {
|
||||
if (!sourceRestored) fs.writeFileSync(sourcePath, originalSource);
|
||||
|
|
@ -4298,6 +4482,7 @@ async function main() {
|
|||
sessionSecret,
|
||||
suffix,
|
||||
});
|
||||
const malformedIdentifiers = await runMalformedIdentifierSuite();
|
||||
const hostedLoginIsolation = await runHostedLoginIsolationBeforeRestart();
|
||||
const serviceRestart = await restartHostedServiceAndResume({
|
||||
clusterflux,
|
||||
|
|
@ -4349,32 +4534,38 @@ async function main() {
|
|||
),
|
||||
];
|
||||
assert(concurrentCompileTasks.length >= 2);
|
||||
const qualityPassed =
|
||||
qualityGates?.private.status === "passed" &&
|
||||
qualityGates?.public.status === "passed";
|
||||
const extensionAsset = manifest.assets.find((asset) =>
|
||||
asset.name.endsWith(".vsix")
|
||||
);
|
||||
const strictRequirementLedger = [
|
||||
{ id: "01_authenticated_project", passed: Boolean(sessionSecret && tenant && project) },
|
||||
{ id: "02_project_commands", passed: projectList.project_count >= 1 && projectSelect.command === "project select" },
|
||||
{ id: "03_bundle_environment_inspection", passed: inspection.metadata.environments.some((environment) => environment.name === "linux") },
|
||||
{ id: "04_launch_attempt_ownership_and_main_parking", passed: parkedStatus.live_process.main_wait_state === "waiting_for_node" && launchAttemptOwnership.existing_process_survived === true },
|
||||
{ id: "05_enrollment_and_persisted_credential", passed: attach.boundary.used_enrollment_exchange === true && secondReady.node === workerNode },
|
||||
{ id: "06_server_derived_node_liveness", passed: nodeLivenessTransition.initial_online === true && nodeLivenessTransition.stale_offline === false && nodeLivenessTransition.recovered_online === true },
|
||||
{ id: "07_real_flagship_and_artifact", passed: completedFlagship(firstEvents) && Boolean(releaseArtifact) && builtOutput === "hello from a real Clusterflux build" },
|
||||
{ id: "08_same_definition_concurrency", passed: sameDefinitionIdentity.thread_evidence.length === 2 && sameDefinitionIdentity.completion_order.length === 2 && sameDefinitionIdentity.podman_paused_container_ids.length >= 2 && sameDefinitionIdentity.terminal_state === "not_active" },
|
||||
{ id: "09_repeated_park_wake", passed: repeatedParkWake.completed_cycles >= 16 && repeatedParkWake.terminal_state === "not_active" },
|
||||
{ id: "10_nested_environment_rejection", passed: agentCredentialSecurity.nested_environment_mismatch.denied === true },
|
||||
{ id: "11_partial_freeze_warning_resume", passed: agentCredentialSecurity.partial_freeze.partially_frozen === true && agentCredentialSecurity.partial_freeze.dap_all_threads_stopped === false && agentCredentialSecurity.partial_freeze.podman_paused_container_ids.length > 0 && agentCredentialSecurity.partial_freeze.resumed_participants.length > 0 },
|
||||
{ id: "12_live_dap_edit_restart", passed: dapEditRestart.clean_vfs_boundary_used === true && dapEditRestart.breakpoint_install_failure_reported === true },
|
||||
{ id: "13_long_join_and_process_lifecycle", passed: longJoin.duration_seconds > 120 && longJoin.terminal_state === "completed" && releasedFlagshipStatus.state === "not_active" && restart.restart_request.accepted === true && cancel.cancel_request.accepted === true },
|
||||
{ id: "14_tenant_isolation", passed: tenantIsolation.executed === true && tenantIsolation.process_hidden === true && tenantIsolation.node_hidden === true },
|
||||
{ id: "15_user_credential_hostility", passed: Boolean(userCredentialSecurity.expired && userCredentialSecurity.forged && userCredentialSecurity.revoked) },
|
||||
{ id: "16_node_credential_hostility", passed: securityNode.evidence.replay.denied === true && securityNode.evidence.expired.denied === true && securityNode.evidence.forged.denied === true && revokedNodeCredential.denied.denied === true },
|
||||
{ id: "17_agent_workflow_and_credentials", passed: agentCredentialSecurity.workflow.flagship_completed === true && agentCredentialSecurity.workflow.authenticated_without_browser === true && agentCredentialSecurity.workflow.external_direct_task_v1.denied === true && agentCredentialSecurity.workflow.child_launch === "task_launched" && agentCredentialSecurity.revoked.denied === true },
|
||||
{ id: "18_dropped_response_rollback", passed: droppedConnectionRollback.cli_exit_status !== 0 && droppedConnectionRollback.rollback === "automatic CLI launch guard abort_process with matching launch_attempt" && droppedConnectionRollback.terminal_state === "not_active" },
|
||||
{ id: "19_quota_and_bandwidth_preallocation", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.unrelated_tenant.unaffected_by_first_tenant_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true },
|
||||
{ id: "20_hosted_login_isolation", passed: hostedLoginIsolation.local_limited_status === 429 && hostedLoginIsolation.forwarding_spoof_status === 429 && hostedLoginIsolation.independent_vps_client_status === 200 && hostedLoginIsolation.control_route_after === "pong" && hostedLoginIsolation.after_service_restart.control_route === "pong" },
|
||||
{ id: "21_soak_restart_and_provenance", passed: liveSoak.executed === true && serviceRestart.executed === true && manifest.source_tree_clean === true && configProvenance.clean === true },
|
||||
{ id: "22_quality_gates", passed: qualityGates?.private.status === "passed" && qualityGates?.public.status === "passed" },
|
||||
{ id: "23_hosted_hello_build", passed: helloBuild.executable_output === "hello from a real Clusterflux build" && helloBuild.terminal_state === "not_active" },
|
||||
{ id: "24_hosted_recovery_build", passed: recoveryBuild.original_join_completed === true && recoveryBuild.original_attempt !== recoveryBuild.replacement_attempt && recoveryBuild.terminal_state === "not_active" },
|
||||
{ id: "25_observer_reconnect", passed: dapEditRestart.observer_reconnected_without_false_stop === true && dapEditRestart.observer_idle_request_rate_bound_per_second <= 0.8 },
|
||||
{ id: "01_formatting", passed: qualityPassed },
|
||||
{ id: "02_clippy_warnings_denied", passed: qualityPassed },
|
||||
{ id: "03_public_workspace_tests", passed: qualityGates?.public.status === "passed" },
|
||||
{ id: "04_private_hosted_policy_locked_tests", passed: qualityGates?.private.status === "passed" },
|
||||
{ id: "05_wasm_example_builds", passed: qualityPassed && helloBuild.executable_output === "hello from a real Clusterflux build" && recoveryBuild.original_join_completed === true },
|
||||
{ id: "06_filtered_public_tree_build_and_tests", passed: qualityGates?.public.status === "passed" && manifest.source_tree_clean === true },
|
||||
{ id: "07_final_vsix_checks", passed: Boolean(extensionAsset && manifest.release_candidate.extension_sha256 && extensionAsset.sha256 === manifest.release_candidate.extension_sha256) },
|
||||
{ id: "08_browser_login_and_persistent_default_project", passed: Boolean(sessionSecret && tenant && project && projectSelect.command === "project select") },
|
||||
{ id: "09_one_time_node_enrollment_and_restart", passed: attach.boundary.used_enrollment_exchange === true && secondReady.node === workerNode && serviceRestart.executed === true },
|
||||
{ id: "10_real_hello_build_and_artifact_download", passed: completedFlagship(firstEvents) && Boolean(releaseArtifact) && builtOutput === "hello from a real Clusterflux build" && download.local_download.bytes_written > 0 },
|
||||
{ id: "11_recovery_retry_and_original_join", passed: recoveryBuild.original_join_completed === true && recoveryBuild.original_attempt !== recoveryBuild.replacement_attempt && recoveryBuild.terminal_state === "not_active" },
|
||||
{ id: "12_dap_launch_without_breakpoint", passed: sameDefinitionIdentity.thread_evidence.length === 2 && sameDefinitionIdentity.terminal_state === "not_active" },
|
||||
{ id: "13_real_breakpoint_installation_and_hit", passed: dapEditRestart.breakpoint_install_failure_reported === true && dapEditRestart.clean_vfs_boundary_used === true },
|
||||
{ id: "14_attach_with_preconfigured_breakpoint", passed: agentCredentialSecurity.partial_freeze.partially_frozen === true },
|
||||
{ id: "15_continue_pause_inspect_disconnect", passed: agentCredentialSecurity.partial_freeze.resumed_participants.length > 0 && dapEditRestart.observer_reconnected_without_false_stop === true },
|
||||
{ id: "16_distinct_same_definition_instances", passed: sameDefinitionIdentity.thread_evidence.length === 2 && sameDefinitionIdentity.completion_order.length === 2 && concurrentCompileTasks.length >= 2 },
|
||||
{ id: "17_real_podman_partial_debug_epoch", passed: agentCredentialSecurity.partial_freeze.partially_frozen === true && agentCredentialSecurity.partial_freeze.dap_all_threads_stopped === false && agentCredentialSecurity.partial_freeze.podman_paused_container_ids.length > 0 },
|
||||
{ id: "18_post_main_launched_observation_recovery", passed: sameDefinitionIdentity.thread_evidence.length === 2 && sameDefinitionIdentity.terminal_state === "not_active" },
|
||||
{ id: "19_fallback_and_debug_epoch_observer_reconnect", passed: dapEditRestart.observer_fallback_reconnected === true && dapEditRestart.observer_debug_epoch_wait_reconnected === true && dapEditRestart.observer_reconnected_without_false_stop === true },
|
||||
{ id: "20_existing_process_rejection_preserves_process", passed: launchAttemptOwnership.existing_process_survived === true },
|
||||
{ id: "21_precommit_launch_failure_scoped_cleanup", passed: droppedConnectionRollback.cli_exit_status !== 0 && droppedConnectionRollback.rollback === "automatic CLI launch guard abort_process with matching launch_attempt" && droppedConnectionRollback.terminal_state === "not_active" },
|
||||
{ id: "22_malformed_identifiers_preserve_service", passed: malformedIdentifiers.valid_after_every_rejection === true && malformedIdentifiers.rejected_requests.length === malformedIdentifiers.principals.length * malformedIdentifiers.forms.length },
|
||||
{ id: "23_cross_tenant_access_denied", passed: tenantIsolation.executed === true && tenantIsolation.process_hidden === true && tenantIsolation.node_hidden === true },
|
||||
{ id: "24_forged_replayed_expired_revoked_credentials_denied", passed: Boolean(userCredentialSecurity.expired && userCredentialSecurity.forged && userCredentialSecurity.revoked) && securityNode.evidence.replay.denied === true && securityNode.evidence.expired.denied === true && securityNode.evidence.forged.denied === true && revokedNodeCredential.denied.denied === true && agentCredentialSecurity.revoked.denied === true },
|
||||
{ id: "25_relay_limits_and_abandoned_transfer_charging", passed: quotaPreallocation.denied_process_allocated === false && quotaPreallocation.unrelated_tenant.unaffected_by_first_tenant_quota === true && bandwidthPreallocation.bytes_served_before_denial > 0 && bandwidthPreallocation.partial_abandon.ingress_delta > 0 && bandwidthPreallocation.partial_abandon.egress_delta > 0 && bandwidthPreallocation.partial_abandon.abandoned_delta > 0 && bandwidthPreallocation.partial_abandon.reservation_released === true && bandwidthPreallocation.restart_persistence === true && relayEmergencyDisable.disabled.denied === true && relayEmergencyDisable.runtime_drop_in_removed === true },
|
||||
];
|
||||
const fullReleasePassed =
|
||||
strictFullRelease &&
|
||||
|
|
@ -4481,6 +4672,7 @@ async function main() {
|
|||
quality_gates: qualityGates,
|
||||
hello_build: helloBuild,
|
||||
recovery_build: recoveryBuild,
|
||||
malformed_identifiers: malformedIdentifiers,
|
||||
named_scenarios: namedScenarios,
|
||||
scenario_skips: [],
|
||||
strict_requirement_ledger: strictRequirementLedger,
|
||||
|
|
|
|||
|
|
@ -148,7 +148,13 @@ const hostedProtocol = maybeRead([
|
|||
]);
|
||||
const hostedService =
|
||||
hostedServiceMain && hostedValidation && hostedWire && hostedProtocol
|
||||
? [hostedServiceMain, hostedValidation, hostedWire, hostedProtocol].join("\n")
|
||||
? [
|
||||
hostedServiceMain,
|
||||
hostedValidation,
|
||||
hostedWire,
|
||||
hostedProtocol,
|
||||
maybeRead(["crates", "clusterflux-core", "src", "ids.rs"]),
|
||||
].join("\n")
|
||||
: null;
|
||||
const hostedTests = [
|
||||
maybeRead(["private", "hosted-policy", "src", "bin", "clusterflux-hosted-service", "tests.rs"]),
|
||||
|
|
@ -159,10 +165,10 @@ const hostedTests = [
|
|||
if (hostedService && hostedTests) {
|
||||
for (const [name, pattern] of [
|
||||
["hosted service turns malformed JSON into error responses", /decode_incoming_request[\s\S]*HostedServiceResponse::Error/],
|
||||
["tenant ids are validated", /fn tenant_id\(value: String\)[\s\S]*validate_identifier\("tenant", &value\)\?/],
|
||||
["node ids are validated", /fn node_id\(value: String\)[\s\S]*validate_identifier\("node", &value\)\?/],
|
||||
["process ids are validated", /fn process_id\(value: String\)[\s\S]*validate_identifier\("process", &value\)\?/],
|
||||
["identifiers reject empty and control/path characters", /fn validate_identifier[\s\S]*trim\(\)\.is_empty\(\)[\s\S]*ch\.is_control\(\) \|\| ch == '\/' \|\| ch == '\\\\'/],
|
||||
["tenant ids are validated", /fn tenant_id\(value: String\)[\s\S]*TenantId::try_new\(value\)/],
|
||||
["node ids are validated", /fn node_id\(value: String\)[\s\S]*NodeId::try_new\(value\)/],
|
||||
["process ids are validated", /fn process_id\(value: String\)[\s\S]*ProcessId::try_new\(value\)/],
|
||||
["identifiers reject empty, oversized, control, and invalid format values", /MAX_EXTERNAL_ID_BYTES[\s\S]*trim\(\)\.is_empty\(\)[\s\S]*char::is_control[\s\S]*is_ascii_alphanumeric\(\)/],
|
||||
["OIDC text fields are bounded", /fn validate_text[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
||||
["tokens are bounded", /fn validate_token[\s\S]*value\.len\(\) > max_bytes[\s\S]*contains unsupported characters/],
|
||||
["control request bodies are bounded", /MAX_CONTROL_FRAME_BYTES[\s\S]*control request too large/],
|
||||
|
|
|
|||
163
scripts/prepare-manual-github-release.js
Executable file
163
scripts/prepare-manual-github-release.js
Executable file
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
|
||||
const repo = path.resolve(__dirname, "..");
|
||||
const releaseRoot = path.resolve(
|
||||
process.env.CLUSTERFLUX_PUBLIC_RELEASE_DIR ||
|
||||
path.join(repo, "target/public-release-final")
|
||||
);
|
||||
const manifestPath = path.join(releaseRoot, "public-release-manifest.json");
|
||||
const resultPath = path.resolve(
|
||||
process.env.CLUSTERFLUX_FINAL_RESULT ||
|
||||
path.join(repo, "target/acceptance/cli-happy-path-live.json")
|
||||
);
|
||||
const transcriptPath = path.resolve(
|
||||
process.env.CLUSTERFLUX_VALIDATION_TRANSCRIPT ||
|
||||
path.join(repo, "target/acceptance/clusterflux-manual-launch-transcript.log")
|
||||
);
|
||||
|
||||
function sha256(file) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(fs.readFileSync(file));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
function write(file, contents) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, contents);
|
||||
}
|
||||
|
||||
function copyVerifiedAsset(asset, destinationRoot) {
|
||||
const source = path.resolve(releaseRoot, asset.file);
|
||||
assert(fs.existsSync(source), `missing finalized asset ${source}`);
|
||||
assert.strictEqual(
|
||||
sha256(source),
|
||||
asset.sha256,
|
||||
`finalized asset digest changed: ${asset.name}`
|
||||
);
|
||||
const destination = path.join(destinationRoot, "assets", asset.name);
|
||||
fs.copyFileSync(source, destination);
|
||||
assert.strictEqual(sha256(destination), asset.sha256);
|
||||
return destination;
|
||||
}
|
||||
|
||||
function extractBinaries(archive, destinationRoot) {
|
||||
const staging = path.join(destinationRoot, ".binary-extract");
|
||||
fs.mkdirSync(staging, { recursive: true });
|
||||
const extracted = spawnSync("tar", ["-xzf", archive, "-C", staging], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (extracted.status !== 0) {
|
||||
throw new Error(`failed to extract ${archive}: ${extracted.stderr}`);
|
||||
}
|
||||
const binRoot = path.join(staging, "bin");
|
||||
assert(fs.existsSync(binRoot), `binary archive omitted bin/: ${archive}`);
|
||||
const copied = [];
|
||||
for (const name of fs.readdirSync(binRoot).sort()) {
|
||||
const source = path.join(binRoot, name);
|
||||
if (!fs.statSync(source).isFile()) continue;
|
||||
const destination = path.join(destinationRoot, "binaries", name);
|
||||
fs.copyFileSync(source, destination);
|
||||
fs.chmodSync(destination, 0o755);
|
||||
copied.push(destination);
|
||||
}
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
assert(copied.length > 0, `binary archive contained no binaries: ${archive}`);
|
||||
return copied;
|
||||
}
|
||||
|
||||
function main() {
|
||||
assert(fs.existsSync(manifestPath), `missing final manifest ${manifestPath}`);
|
||||
assert(fs.existsSync(resultPath), `missing final result ${resultPath}`);
|
||||
assert(fs.existsSync(transcriptPath), `missing validation transcript ${transcriptPath}`);
|
||||
const manifest = readJson(manifestPath);
|
||||
const result = readJson(resultPath);
|
||||
assert.strictEqual(result.acceptance_result, "passed");
|
||||
assert.strictEqual(result.source_commit, manifest.source_commit);
|
||||
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.every((requirement) => requirement.passed === true),
|
||||
"final validation contains a failed launch requirement"
|
||||
);
|
||||
|
||||
const destinationRoot = path.resolve(
|
||||
process.env.CLUSTERFLUX_GITHUB_RELEASE_DIR ||
|
||||
path.join(repo, "target/github-release", manifest.release_name)
|
||||
);
|
||||
fs.rmSync(destinationRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.join(destinationRoot, "assets"), { recursive: true });
|
||||
fs.mkdirSync(path.join(destinationRoot, "binaries"), { recursive: true });
|
||||
|
||||
const copiedAssets = manifest.assets.map((asset) =>
|
||||
copyVerifiedAsset(asset, destinationRoot)
|
||||
);
|
||||
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 vsix = copiedAssets.find((file) => file.endsWith(".vsix"));
|
||||
assert(vsix, "final manifest omitted the tested VSIX");
|
||||
assert.strictEqual(
|
||||
sha256(vsix),
|
||||
manifest.release_candidate.extension_sha256,
|
||||
"tested VSIX digest does not match final candidate binding"
|
||||
);
|
||||
|
||||
write(path.join(destinationRoot, "SOURCE_REVISION"), `${manifest.source_commit}\n`);
|
||||
write(
|
||||
path.join(destinationRoot, "SOURCE_TREE_DIGEST"),
|
||||
`${manifest.source_tree_digest}\n`
|
||||
);
|
||||
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"),
|
||||
`# Clusterflux ${manifest.release_name}\n\n` +
|
||||
"First manual GitHub launch release of Clusterflux. The release includes the filtered public source, real Linux CLI/node/coordinator binaries, and the exact VSIX used by the final production-shaped validation batch.\n"
|
||||
);
|
||||
write(
|
||||
path.join(destinationRoot, "KNOWN_LIMITATIONS.md"),
|
||||
"# Known limitations\n\n" +
|
||||
"- The launch batch validates one enrolled Linux node with rootless Podman.\n" +
|
||||
"- Full Windows sandbox validation is deferred.\n" +
|
||||
"- Providers, billing, teams, managed compute, and operator-panel UI are not part of this release.\n"
|
||||
);
|
||||
|
||||
const checksummed = [
|
||||
...copiedAssets,
|
||||
...binaries,
|
||||
path.join(destinationRoot, "SOURCE_REVISION"),
|
||||
path.join(destinationRoot, "SOURCE_TREE_DIGEST"),
|
||||
path.join(destinationRoot, "VSIX_SHA256"),
|
||||
path.join(destinationRoot, "public-release-manifest.json"),
|
||||
path.join(destinationRoot, "final-result.json"),
|
||||
path.join(destinationRoot, "VALIDATION_TRANSCRIPT.log"),
|
||||
path.join(destinationRoot, "RELEASE_NOTES.md"),
|
||||
path.join(destinationRoot, "KNOWN_LIMITATIONS.md"),
|
||||
].sort();
|
||||
write(
|
||||
path.join(destinationRoot, "SHA256SUMS"),
|
||||
`${checksummed
|
||||
.map((file) => `${sha256(file)} ${path.relative(destinationRoot, file)}`)
|
||||
.join("\n")}\n`
|
||||
);
|
||||
console.log(destinationRoot);
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -513,6 +513,24 @@ function reuseCandidateHostedBinary(candidate, releaseName) {
|
|||
return archive;
|
||||
}
|
||||
|
||||
function reuseCandidateExtension(candidate) {
|
||||
assertCandidateManifest(candidate);
|
||||
const asset = candidate.assets.find((entry) => entry.name.endsWith(".vsix"));
|
||||
if (!asset || !fs.existsSync(asset.file)) {
|
||||
throw new Error("release candidate VSIX is missing");
|
||||
}
|
||||
const digest = sha256File(asset.file);
|
||||
if (
|
||||
digest !== asset.sha256 ||
|
||||
digest !== candidate.release_candidate.extension_sha256
|
||||
) {
|
||||
throw new Error("release candidate VSIX digest changed after candidate creation");
|
||||
}
|
||||
const archive = path.join(assetsDir, asset.name);
|
||||
fs.copyFileSync(asset.file, archive);
|
||||
return archive;
|
||||
}
|
||||
|
||||
function assertCandidateManifest(candidate) {
|
||||
if (candidate.kind !== "clusterflux-public-release") {
|
||||
throw new Error("release candidate manifest has the wrong kind");
|
||||
|
|
@ -529,6 +547,9 @@ function assertCandidateManifest(candidate) {
|
|||
if (!candidate.release_candidate.hosted_binary_archive_sha256) {
|
||||
throw new Error("release candidate omitted the hosted service archive digest");
|
||||
}
|
||||
if (!candidate.release_candidate.extension_sha256) {
|
||||
throw new Error("release candidate omitted the VSIX digest");
|
||||
}
|
||||
}
|
||||
|
||||
function stageEvidenceAsset(
|
||||
|
|
@ -735,26 +756,30 @@ function stageSourceAsset(releaseName) {
|
|||
}
|
||||
|
||||
function stageExtensionAsset() {
|
||||
const extensionRoot = path.join(publicTree, "vscode-extension");
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(publicTree, "vscode-extension/package.json"), "utf8")
|
||||
fs.readFileSync(path.join(extensionRoot, "package.json"), "utf8")
|
||||
);
|
||||
const archive = path.join(
|
||||
assetsDir,
|
||||
`${packageJson.name}-${packageJson.version}.vsix`
|
||||
);
|
||||
run("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
||||
cwd: extensionRoot,
|
||||
});
|
||||
run(
|
||||
"npx",
|
||||
path.join(extensionRoot, "node_modules", ".bin", "vsce"),
|
||||
[
|
||||
"--yes",
|
||||
"@vscode/vsce",
|
||||
"package",
|
||||
"--allow-missing-repository",
|
||||
"--skip-license",
|
||||
"--out",
|
||||
archive,
|
||||
],
|
||||
{ cwd: path.join(publicTree, "vscode-extension") }
|
||||
{ cwd: extensionRoot }
|
||||
);
|
||||
run("node", ["scripts/vscode-extension-smoke.js"], { cwd: publicTree });
|
||||
run("node", ["scripts/vscode-f5-smoke.js"], { cwd: publicTree });
|
||||
return archive;
|
||||
}
|
||||
|
||||
|
|
@ -1087,11 +1112,13 @@ function main() {
|
|||
: publishPublicTree(releaseName, sourceCommit, publicTreeIdentity);
|
||||
let binaryArchive;
|
||||
let hostedBinaryArchive;
|
||||
let extensionArchive;
|
||||
let binaryDigests;
|
||||
let candidateBinding;
|
||||
if (candidate) {
|
||||
binaryArchive = reuseCandidateBinaries(candidate, releaseName);
|
||||
hostedBinaryArchive = reuseCandidateHostedBinary(candidate, releaseName);
|
||||
extensionArchive = reuseCandidateExtension(candidate);
|
||||
binaryDigests = candidate.binary_digests;
|
||||
candidateBinding = {
|
||||
mode: "finalized-exact",
|
||||
|
|
@ -1099,17 +1126,20 @@ function main() {
|
|||
manifest_sha256: sha256File(candidateManifestPath),
|
||||
binary_archive_sha256: sha256File(binaryArchive),
|
||||
hosted_binary_archive_sha256: sha256File(hostedBinaryArchive),
|
||||
extension_sha256: sha256File(extensionArchive),
|
||||
};
|
||||
} else {
|
||||
buildPublicBinaries();
|
||||
buildHostedBinary();
|
||||
binaryArchive = stageBinaryAssets(releaseName);
|
||||
hostedBinaryArchive = stageHostedBinaryAsset(releaseName);
|
||||
extensionArchive = stageExtensionAsset();
|
||||
binaryDigests = candidateBinaryDigests();
|
||||
candidateBinding = {
|
||||
mode: "built-once",
|
||||
binary_archive_sha256: sha256File(binaryArchive),
|
||||
hosted_binary_archive_sha256: sha256File(hostedBinaryArchive),
|
||||
extension_sha256: sha256File(extensionArchive),
|
||||
};
|
||||
}
|
||||
const candidateConfigurationIdentity =
|
||||
|
|
@ -1131,7 +1161,6 @@ function main() {
|
|||
candidateProxyConfigurationIdentity
|
||||
);
|
||||
const evidenceArchive = evidence.archive;
|
||||
const extensionArchive = stageExtensionAsset();
|
||||
const resolution = resolverInstructions();
|
||||
const gettingStarted = writeGettingStartedAsset(
|
||||
releaseName,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue