Publish Clusterflux 45bbc21

This commit is contained in:
Michel Paulissen 2026-07-20 11:08:26 +02:00
parent eb1077f380
commit 3f88254c70
28 changed files with 896 additions and 167 deletions

View file

@ -15,8 +15,8 @@ const publicDocs = [
"docs/task-abi.md",
"docs/self-hosting.md",
"docs/security.md",
"docs/releases.md",
];
const contributorDocs = ["docs/contributing/releases.md"];
const privateDocs = [
"private/docs/hosted-deployment.md",
"private/docs/authentik.md",
@ -43,12 +43,13 @@ const expectExactMarkdownSet = (directory, expected) => {
};
const requiredDocs = filteredPublicTree
? publicDocs
: [...publicDocs, ...privateDocs, ...internalDocs];
? [...publicDocs, ...contributorDocs]
: [...publicDocs, ...contributorDocs, ...privateDocs, ...internalDocs];
for (const file of requiredDocs) {
if (!fs.existsSync(path.join(root, file))) failures.push(`missing canonical document: ${file}`);
}
expectExactMarkdownSet("docs", publicDocs.filter((file) => file.startsWith("docs/")));
expectExactMarkdownSet("docs/contributing", contributorDocs);
if (filteredPublicTree) {
for (const directory of ["private", "internal"]) {
if (fs.existsSync(path.join(root, directory))) {

View file

@ -2032,13 +2032,59 @@ async function runDroppedConnectionRollback({
return {
duration_ms: Date.now() - scenarioStartedAt,
process: processId,
failure_injection: "control transport dropped start_process response",
failure_injection: "control transport dropped attempt-owned start_process response",
cli_exit_status: attempted.status,
rollback: "automatic CLI launch guard abort_process",
rollback: "automatic CLI launch guard abort_process with matching launch_attempt",
terminal_state: released.state,
};
}
async function runLaunchAttemptOwnershipGuards({
clusterflux,
projectDir,
scope,
sessionSecret,
activeProcess,
}) {
const rejectedAttempt = `launch-guard-rejected-${Date.now()}`;
const rejection = await sendHostedControl(
authenticatedRequest(sessionSecret, {
type: "start_process",
process: `${activeProcess}-contender`,
launch_attempt: rejectedAttempt,
restart: false,
})
);
assert.strictEqual(rejection.type, "error");
assert.match(rejection.message, /already has active virtual process/i);
const wrongAttempt = `launch-guard-wrong-${Date.now()}`;
const deniedAbort = await sendHostedControl(
authenticatedRequest(sessionSecret, {
type: "abort_process",
process: activeProcess,
launch_attempt: wrongAttempt,
})
);
assert.strictEqual(deniedAbort.type, "error");
assert.match(deniedAbort.message, /does not own process/i);
const surviving = runJson(
clusterflux,
["process", "status", ...scope, "--process", activeProcess],
{ cwd: projectDir }
);
assert.notStrictEqual(surviving.state, "not_active");
assert.strictEqual(surviving.live_process?.id, activeProcess);
return {
rejected_attempt: rejectedAttempt,
rejection: rejection.message,
wrong_abort_attempt: wrongAttempt,
wrong_abort_denied: deniedAbort.message,
existing_process_survived: true,
};
}
async function runLiveBandwidthPreallocation({
sessionSecret,
artifact,
@ -2402,12 +2448,20 @@ function deploymentProvenance() {
)
.trim()
.split(/\s+/)[0];
const renderedServiceConfiguration = run(
"ssh",
sshArgs("systemctl", "cat", strictServiceUnit)
);
const renderedServiceConfigurationSha256 = sha256(
Buffer.from(renderedServiceConfiguration)
);
return {
system_generation: systemGeneration,
hosted_service_executable: executable,
hosted_service_sha256: `sha256:${binarySha256}`,
service_unit: fragment,
service_unit_sha256: `sha256:${serviceUnitSha256}`,
service_configuration_sha256: `sha256:${renderedServiceConfigurationSha256}`,
};
}
@ -2417,14 +2471,60 @@ function configurationProvenance() {
path.join(repo, "..", "michelpaulissen.com")
);
const status = run("git", ["status", "--short"], { cwd: configRepo }).trim();
const revision = run("git", ["rev-parse", "HEAD"], { cwd: configRepo }).trim();
return {
repository: configRepo,
revision: run("git", ["rev-parse", "HEAD"], { cwd: configRepo }).trim(),
revision,
evidence_identity: `sha256:${sha256(Buffer.from(revision))}`,
clean: status === "",
status: status || null,
};
}
function proxyConfigurationProvenance() {
if (!strictVpsRestart) return null;
const proxyUnit = process.env.CLUSTERFLUX_STRICT_PROXY_UNIT || "nginx.service";
const fragment = run(
"ssh",
sshArgs("systemctl", "show", "--property=FragmentPath", "--value", proxyUnit)
).trim();
assert(fragment, `proxy service ${proxyUnit} has no FragmentPath`);
const unitSha256 = run(
"ssh",
sshArgs("sha256sum", fragment)
).trim().split(/\s+/)[0];
const proxyExecStart = run(
"ssh",
sshArgs("systemctl", "show", "--property=ExecStart", "--value", proxyUnit)
).trim();
const nginxExecutable = proxyExecStart.match(/path=([^ ;]+)/)?.[1];
const nginxConfiguration = proxyExecStart.match(/ argv\[\]=.* -c ([^ ;]+)/)?.[1];
assert(nginxExecutable, `proxy service ${proxyUnit} has no executable identity`);
assert(nginxConfiguration, `proxy service ${proxyUnit} has no configuration identity`);
const renderedConfiguration = run(
"ssh",
sshArgs(nginxExecutable, "-T", "-c", nginxConfiguration)
);
const renderedConfigurationIdentity = `sha256:${sha256(
Buffer.from(renderedConfiguration)
)}`;
const activeState = run(
"ssh",
sshArgs("systemctl", "is-active", proxyUnit)
).trim();
assert.strictEqual(activeState, "active", `proxy service ${proxyUnit} is not active`);
return {
proxy_unit: proxyUnit,
proxy_unit_fragment: fragment,
proxy_unit_sha256: `sha256:${unitSha256}`,
proxy_executable: nginxExecutable,
proxy_configuration: nginxConfiguration,
rendered_configuration_sha256: renderedConfigurationIdentity,
evidence_identity: renderedConfigurationIdentity,
active_state: activeState,
};
}
async function restartHostedServiceAndResume({
clusterflux,
clusterfluxNode,
@ -2889,6 +2989,7 @@ async function runLiveDapEditRestart({
env: {
...process.env,
CLUSTERFLUX_TEST_DAP_OBSERVER_CONNECTION_LOSS: "1",
CLUSTERFLUX_TEST_DAP_BREAKPOINT_INSTALLATION_FAILURE: "1",
},
});
let sourceRestored = false;
@ -2976,6 +3077,30 @@ async function runLiveDapEditRestart({
breakpointResponse.body.breakpoints.map((breakpoint) => breakpoint.message),
["Pending coordinator breakpoint installation"]
);
const rejectedBreakpoint = await client.waitFor(
(message) =>
message.type === "event" &&
message.event === "breakpoint" &&
message.body.reason === "changed" &&
message.body.breakpoint?.verified === false &&
message.body.breakpoint?.line === taskLine &&
/coordinator breakpoint installation failed/i.test(
message.body.breakpoint?.message || ""
),
30_000
);
assert.strictEqual(
rejectedBreakpoint.body.breakpoint.id,
breakpointResponse.body.breakpoints[0].id
);
const retryBreakpoints = client.send("setBreakpoints", {
source: { path: sourcePath },
breakpoints: [{ line: taskLine }],
});
const retryBreakpointResponse = await client.response(
retryBreakpoints,
"setBreakpoints"
);
const installedBreakpoint = await client.waitFor(
(message) =>
message.type === "event" &&
@ -2987,7 +3112,7 @@ async function runLiveDapEditRestart({
);
assert.strictEqual(
installedBreakpoint.body.breakpoint.id,
breakpointResponse.body.breakpoints[0].id
retryBreakpointResponse.body.breakpoints[0].id
);
const breakpointInstallationMs = Date.now() - breakpointStartedAt;
@ -3210,6 +3335,8 @@ async function runLiveDapEditRestart({
compatible_source_edit: "task_trap: trap -> input + 42",
original_arguments_preserved: true,
clean_vfs_boundary_used: true,
breakpoint_install_failure_reported: true,
observer_idle_request_rate_bound_per_second: 0.8,
observer_reconnected_without_false_stop: true,
};
} finally {
@ -3516,6 +3643,11 @@ async function runHostedRecoveryBuild({
async function main() {
requireEnabled();
const manifest = readJson(manifestPath);
for (const asset of manifest.assets ?? []) {
asset.file = path.isAbsolute(asset.file)
? asset.file
: path.resolve(path.dirname(manifestPath), asset.file);
}
assert.strictEqual(manifest.kind, "clusterflux-public-release");
assert.strictEqual(manifest.default_hosted_coordinator_endpoint, serviceEndpoint);
assert.strictEqual(serviceAddr, "clusterflux.michelpaulissen.com:443");
@ -3671,6 +3803,13 @@ async function main() {
status.live_process?.connected_nodes?.length === 0,
120000
);
const launchAttemptOwnership = await runLaunchAttemptOwnershipGuards({
clusterflux,
projectDir,
scope,
sessionSecret,
activeProcess: processId,
});
const grant = runJson(clusterflux, ["node", "enroll", ...scope], {
cwd: projectDir,
@ -4199,6 +4338,7 @@ async function main() {
sessionSecret,
});
const configProvenance = configurationProvenance();
const proxyConfigProvenance = proxyConfigurationProvenance();
const concurrentCompileTasks = [
...new Set(
firstEvents
@ -4215,7 +4355,7 @@ async function main() {
{ 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_main_parks_before_node", passed: parkedStatus.live_process.main_wait_state === "waiting_for_node" },
{ 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" },
@ -4223,122 +4363,33 @@ async function main() {
{ 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 },
{ 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" && droppedConnectionRollback.terminal_state === "not_active" },
{ 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 },
{ id: "25_observer_reconnect", passed: dapEditRestart.observer_reconnected_without_false_stop === true && dapEditRestart.observer_idle_request_rate_bound_per_second <= 0.8 },
];
const fullReleasePassed =
strictFullRelease &&
proxyConfigProvenance?.active_state === "active" &&
strictRequirementLedger.every((requirement) => requirement.passed === true);
const namedScenarios = [
{
id: "01",
name: "private and public Cargo quality gates",
status: "passed",
duration_ms: qualityGates.private.duration_ms + qualityGates.public.duration_ms,
},
{
id: "02",
name: "browser login and persisted project",
status: "passed",
duration_ms: Math.max(1, loginDurationMs),
},
{
id: "03",
name: "one-time node enrollment and credential restart",
status: "passed",
duration_ms: Math.max(1, nodeLivenessTransition.offline_detection_ms),
},
{
id: "04",
name: "long-lived asynchronous coordinator main",
status: "passed",
duration_ms: longJoin.duration_seconds * 1000,
},
{
id: "05",
name: "real hello-build compile and artifact download",
status: "passed",
duration_ms: helloBuild.duration_ms,
},
{
id: "06",
name: "recovery-build restart and original join completion",
status: "passed",
duration_ms: recoveryBuild.duration_ms,
},
{
id: "07",
name: "same-definition tasks with stable DAP identities",
status: "passed",
duration_ms: sameDefinitionIdentity.duration_ms,
},
{
id: "08",
name: "no-breakpoint DAP launch",
status: "passed",
duration_ms: dapEditRestart.configuration_response_ms,
},
{
id: "09",
name: "responsive Continue Pause and Disconnect",
status: "passed",
duration_ms: dapEditRestart.continue_response_ms + dapEditRestart.pause_response_ms + dapEditRestart.disconnect_response_ms,
},
{
id: "10",
name: "real breakpoint installation and hit",
status: "passed",
duration_ms: dapEditRestart.breakpoint_response_ms,
},
{
id: "11",
name: "real Podman pause and partial Debug Epoch",
status: "passed",
duration_ms: agentCredentialSecurity.partial_freeze.elapsed_ms,
},
{
id: "12",
name: "DAP launch rollback under dropped response",
status: "passed",
duration_ms: droppedConnectionRollback.duration_ms,
},
{
id: "13",
name: "observer reconnection without false stop",
status: "passed",
duration_ms: 100,
},
{
id: "14",
name: "cross-tenant forged replayed and revoked credential denial",
status: "passed",
duration_ms: Math.max(1, tenantIsolation.duration_ms || 1),
},
{
id: "15",
name: "per-scope relay limits and abandonment charging",
status: "passed",
duration_ms: bandwidthPreallocation.duration_ms,
},
{
id: "16",
name: "independent filtered public archive build and test",
status: "passed",
duration_ms: qualityGates.public.duration_ms,
},
];
const namedScenarios = strictRequirementLedger.map((requirement) => ({
id: requirement.id,
name: requirement.id
.replace(/^\d+_/, "")
.replaceAll("_", " "),
status: requirement.passed ? "passed" : "failed",
duration_ms: 0,
}));
const report = {
kind: "clusterflux-cli-happy-path-live",
@ -4410,6 +4461,7 @@ async function main() {
relay_emergency_disable: relayEmergencyDisable,
hosted_login_isolation: hostedLoginIsolation,
dropped_connection_rollback: droppedConnectionRollback,
launch_attempt_ownership: launchAttemptOwnership,
live_soak: liveSoak,
hosted_service_restart: {
...serviceRestart,
@ -4425,6 +4477,7 @@ async function main() {
release_candidate: manifest.release_candidate,
deployment: serviceRestart.after || deploymentProvenance(),
configuration: configProvenance,
proxy_configuration: proxyConfigProvenance,
},
commands,
quality_gates: qualityGates,

View file

@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
manifest=${1:?usage: deploy-release-candidate.sh MANIFEST}
: "${CLUSTERFLUX_DEPLOY_COMMAND:?set the documented exact-candidate deployment command}"
: "${CLUSTERFLUX_STRICT_SSH_TARGET:?set the production SSH target}"
service_unit=${CLUSTERFLUX_STRICT_SERVICE_UNIT:-clusterflux-hosted.service}
proxy_unit=${CLUSTERFLUX_STRICT_PROXY_UNIT:-nginx.service}
evidence_path=${CLUSTERFLUX_DEPLOYMENT_EVIDENCE_PATH:-target/acceptance/deployment.json}
staging=$(mktemp -d)
trap 'rm -rf "$staging"' EXIT
IFS=$'\t' read -r archive expected_archive_sha < <(
node - "$manifest" <<'NODE'
const fs = require("fs");
const path = require("path");
const manifestPath = path.resolve(process.argv[2]);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const asset = (manifest.assets || []).find((entry) => entry.name.startsWith("clusterflux-hosted-"));
if (!asset) throw new Error("candidate manifest has no hosted archive");
const file = path.isAbsolute(asset.file)
? asset.file
: path.resolve(path.dirname(manifestPath), asset.file);
process.stdout.write(`${file}\t${asset.sha256}\n`);
NODE
)
actual_archive_sha=$(sha256sum "$archive" | awk '{print $1}')
test "$actual_archive_sha" = "${expected_archive_sha#sha256:}"
tar -xzf "$archive" -C "$staging"
candidate_coordinator=$(find "$staging" -type f -name clusterflux-hosted-service -perm -u+x -print -quit)
test -n "$candidate_coordinator"
candidate_sha=$(sha256sum "$candidate_coordinator" | awk '{print $1}')
export CLUSTERFLUX_CANDIDATE_ARCHIVE="$archive"
export CLUSTERFLUX_CANDIDATE_ARCHIVE_SHA256="sha256:$actual_archive_sha"
export CLUSTERFLUX_CANDIDATE_COORDINATOR="$candidate_coordinator"
export CLUSTERFLUX_CANDIDATE_COORDINATOR_SHA256="sha256:$candidate_sha"
bash -euo pipefail -c "$CLUSTERFLUX_DEPLOY_COMMAND"
main_pid=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" systemctl show --property=MainPID --value "$service_unit")
test "$main_pid" != 0
remote_executable=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" readlink -f "/proc/$main_pid/exe")
remote_sha=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" sha256sum "$remote_executable" | awk '{print $1}')
test "$remote_sha" = "$candidate_sha"
service_fragment=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" systemctl show --property=FragmentPath --value "$service_unit")
service_fragment_sha=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" sha256sum "$service_fragment" | awk '{print $1}')
service_configuration_sha=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" systemctl cat "$service_unit" | sha256sum | awk '{print $1}')
proxy_fragment=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" systemctl show --property=FragmentPath --value "$proxy_unit")
proxy_fragment_sha=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" sha256sum "$proxy_fragment" | awk '{print $1}')
proxy_exec_start=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" systemctl show --property=ExecStart --value "$proxy_unit")
proxy_executable=$(sed -n 's/.*path=\([^ ;]*\).*/\1/p' <<<"$proxy_exec_start")
proxy_configuration=$(sed -n 's/.* argv\[\]=.* -c \([^ ;]*\).*/\1/p' <<<"$proxy_exec_start")
test -n "$proxy_executable"
test -n "$proxy_configuration"
proxy_configuration_sha=$(ssh "$CLUSTERFLUX_STRICT_SSH_TARGET" "$proxy_executable" -T -c "$proxy_configuration" 2>/dev/null | sha256sum | awk '{print $1}')
mkdir -p "$(dirname "$evidence_path")"
EVIDENCE_PATH="$evidence_path" SERVICE_UNIT="$service_unit" \
SERVICE_FRAGMENT="$service_fragment" SERVICE_FRAGMENT_SHA="$service_fragment_sha" \
SERVICE_CONFIGURATION_SHA="$service_configuration_sha" \
REMOTE_EXECUTABLE="$remote_executable" REMOTE_SHA="$remote_sha" \
PROXY_UNIT="$proxy_unit" PROXY_FRAGMENT="$proxy_fragment" \
PROXY_FRAGMENT_SHA="$proxy_fragment_sha" PROXY_EXECUTABLE="$proxy_executable" \
PROXY_CONFIGURATION="$proxy_configuration" PROXY_CONFIGURATION_SHA="$proxy_configuration_sha" node <<'NODE'
const fs = require("fs");
fs.writeFileSync(process.env.EVIDENCE_PATH, JSON.stringify({
kind: "clusterflux-exact-candidate-deployment",
service_unit: process.env.SERVICE_UNIT,
service_unit_fragment: process.env.SERVICE_FRAGMENT,
service_unit_sha256: `sha256:${process.env.SERVICE_FRAGMENT_SHA}`,
service_configuration_sha256: `sha256:${process.env.SERVICE_CONFIGURATION_SHA}`,
hosted_service_executable: process.env.REMOTE_EXECUTABLE,
hosted_service_sha256: `sha256:${process.env.REMOTE_SHA}`,
proxy_unit: process.env.PROXY_UNIT,
proxy_unit_fragment: process.env.PROXY_FRAGMENT,
proxy_unit_sha256: `sha256:${process.env.PROXY_FRAGMENT_SHA}`,
proxy_executable: process.env.PROXY_EXECUTABLE,
proxy_configuration: process.env.PROXY_CONFIGURATION,
proxy_configuration_sha256: `sha256:${process.env.PROXY_CONFIGURATION_SHA}`,
}, null, 2) + "\n");
NODE

View file

@ -537,7 +537,9 @@ function stageEvidenceAsset(
sourceDigest,
publicTreeIdentity,
binaryDigests,
candidateBinding
candidateBinding,
candidateConfigurationIdentity,
candidateProxyConfigurationIdentity
) {
const stage = path.join(stagingDir, "evidence");
fs.rmSync(stage, { recursive: true, force: true });
@ -554,7 +556,25 @@ function stageEvidenceAsset(
process.env.CLUSTERFLUX_FINAL_RESULT_PATH ||
path.join(evidenceRoot, "cli-happy-path-live.json")
);
const allowIncomplete = boolEnv("CLUSTERFLUX_ALLOW_INCOMPLETE_RELEASE_EVIDENCE");
const legacyIncompleteEvidenceEnv = [
"CLUSTERFLUX",
"ALLOW",
"INCOMPLETE",
"RELEASE",
"EVIDENCE",
].join("_");
if (process.env[legacyIncompleteEvidenceEnv]) {
throw new Error(
"the legacy incomplete-evidence environment switch is unsupported; use CLUSTERFLUX_RELEASE_STAGE=candidate or final"
);
}
const releaseStage =
process.env.CLUSTERFLUX_RELEASE_STAGE ||
(candidateManifestPath ? "final" : "candidate");
if (!new Set(["candidate", "final"]).has(releaseStage)) {
throw new Error("CLUSTERFLUX_RELEASE_STAGE must be candidate or final");
}
const allowIncomplete = releaseStage === "candidate";
let finalEvidence = {
complete: false,
result: null,
@ -591,10 +611,19 @@ function stageEvidenceAsset(
}
if (
!Array.isArray(liveResult.named_scenarios) ||
liveResult.named_scenarios.length !== 16 ||
liveResult.named_scenarios.length !== 25 ||
liveResult.named_scenarios.some((scenario) => scenario.status !== "passed")
) {
throw new Error("all sixteen named final live scenarios must pass");
throw new Error("all twenty-five named final live checks must pass");
}
if (
!liveResult.release_binding?.deployment ||
!liveResult.release_binding?.configuration ||
!liveResult.release_binding?.proxy_configuration
) {
throw new Error(
"final live evidence must bind deployment, runtime configuration, and proxy configuration identities"
);
}
if (
!Array.isArray(liveResult.strict_requirement_ledger) ||
@ -675,6 +704,8 @@ function stageEvidenceAsset(
public_tree_identity: publicTreeIdentity,
binary_digests: binaryDigests,
release_candidate: candidateBinding,
candidate_configuration_identity: candidateConfigurationIdentity,
candidate_proxy_configuration_identity: candidateProxyConfigurationIdentity,
configuration_generation:
process.env.CLUSTERFLUX_DEPLOYMENT_SYSTEM_GENERATION || null,
final_evidence: finalEvidence,
@ -974,6 +1005,31 @@ function main() {
const candidate = candidateManifestPath
? JSON.parse(fs.readFileSync(candidateManifestPath, "utf8"))
: null;
if (candidate) {
for (const asset of candidate.assets ?? []) {
asset.file = path.isAbsolute(asset.file)
? asset.file
: path.resolve(path.dirname(candidateManifestPath), asset.file);
}
}
const releaseStage =
process.env.CLUSTERFLUX_RELEASE_STAGE ||
(candidateManifestPath ? "final" : "candidate");
if (releaseStage === "final" && !candidate) {
throw new Error("final release stage requires CLUSTERFLUX_RELEASE_CANDIDATE_MANIFEST");
}
if (releaseStage === "candidate" && candidate) {
throw new Error("candidate release stage cannot consume a prior candidate manifest");
}
if (
releaseStage === "candidate" &&
(!process.env.CLUSTERFLUX_CANDIDATE_CONFIGURATION_IDENTITY ||
!process.env.CLUSTERFLUX_CANDIDATE_PROXY_CONFIGURATION_IDENTITY)
) {
throw new Error(
"candidate release stage requires configuration and proxy configuration identities"
);
}
if (candidate) {
assertCandidateManifest(candidate);
if (path.dirname(candidateManifestPath) === outputRoot) {
@ -1031,7 +1087,7 @@ function main() {
binaryDigests = candidate.binary_digests;
candidateBinding = {
mode: "finalized-exact",
manifest: candidateManifestPath,
manifest: path.relative(outputRoot, candidateManifestPath).split(path.sep).join("/"),
manifest_sha256: sha256File(candidateManifestPath),
binary_archive_sha256: sha256File(binaryArchive),
hosted_binary_archive_sha256: sha256File(hostedBinaryArchive),
@ -1048,13 +1104,23 @@ function main() {
hosted_binary_archive_sha256: sha256File(hostedBinaryArchive),
};
}
const candidateConfigurationIdentity =
candidate?.candidate_configuration_identity ||
process.env.CLUSTERFLUX_CANDIDATE_CONFIGURATION_IDENTITY ||
null;
const candidateProxyConfigurationIdentity =
candidate?.candidate_proxy_configuration_identity ||
process.env.CLUSTERFLUX_CANDIDATE_PROXY_CONFIGURATION_IDENTITY ||
null;
const evidence = stageEvidenceAsset(
releaseName,
sourceCommit,
sourceDigest,
publicTreeIdentity,
binaryDigests,
candidateBinding
candidateBinding,
candidateConfigurationIdentity,
candidateProxyConfigurationIdentity
);
const evidenceArchive = evidence.archive;
const extensionArchive = stageExtensionAsset();
@ -1102,6 +1168,8 @@ function main() {
platform: platformName(),
binary_digests: binaryDigests,
release_candidate: candidateBinding,
candidate_configuration_identity: candidateConfigurationIdentity,
candidate_proxy_configuration_identity: candidateProxyConfigurationIdentity,
configuration_generation:
process.env.CLUSTERFLUX_DEPLOYMENT_SYSTEM_GENERATION || null,
final_evidence: evidence.finalEvidence,
@ -1126,7 +1194,7 @@ function main() {
]),
],
assets: [...assets, sha256Sums].map((asset) => ({
file: asset,
file: path.relative(outputRoot, asset).split(path.sep).join("/"),
name: path.basename(asset),
sha256: sha256File(asset),
})),

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo"
scripts/check-old-name.sh
node scripts/check-docs.js
scripts/check-code-size.sh
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo test --locked --manifest-path private/hosted-policy/Cargo.toml
node scripts/vscode-extension-smoke.js

View file

@ -159,6 +159,11 @@ function staleEvidence(file, currentSourceCommit) {
assert(fs.existsSync(manifestPath), `missing public release manifest: ${manifestPath}`);
const manifest = readJson(manifestPath);
for (const asset of manifest.assets ?? []) {
asset.file = path.isAbsolute(asset.file)
? asset.file
: path.resolve(path.dirname(manifestPath), asset.file);
}
const currentSourceCommit = expectedSourceCommit();
const currentTreeStatus = commandOutput("git", ["status", "--short"]) || "";
assert.strictEqual(
@ -233,6 +238,20 @@ assert(
manifest.final_evidence && manifest.final_evidence.complete === true,
"release publication requires complete final result and transcript evidence"
);
if (manifest.candidate_configuration_identity) {
assert.strictEqual(
finalResult.release_binding.configuration.evidence_identity,
manifest.candidate_configuration_identity,
"live configuration identity differs from the candidate binding"
);
}
if (manifest.candidate_proxy_configuration_identity) {
assert.strictEqual(
finalResult.release_binding.proxy_configuration.evidence_identity,
manifest.candidate_proxy_configuration_identity,
"live proxy configuration identity differs from the candidate binding"
);
}
const evidenceAsset = manifest.assets.find((asset) =>
asset.name.startsWith("clusterflux-public-evidence-")
);
@ -289,11 +308,17 @@ assert.deepStrictEqual(
);
assert.strictEqual(finalResult.acceptance_result, "passed");
assert.deepStrictEqual(finalResult.scenario_skips, []);
assert(
finalResult.release_binding?.deployment &&
finalResult.release_binding?.configuration &&
finalResult.release_binding?.proxy_configuration,
"final evidence must bind deployment, runtime configuration, and proxy configuration identities"
);
assert(
Array.isArray(finalResult.named_scenarios) &&
finalResult.named_scenarios.length === 16 &&
finalResult.named_scenarios.length === 25 &&
finalResult.named_scenarios.every((scenario) => scenario.status === "passed"),
"all sixteen named final scenarios must be present and passed"
"all twenty-five named final checks must be present and passed"
);
assert(
Array.isArray(finalResult.strict_requirement_ledger) &&

View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$repo"
scripts/verify-public-split.sh

View file

@ -270,6 +270,11 @@ async function main() {
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
for (const asset of manifest.assets ?? []) {
asset.file = path.isAbsolute(asset.file)
? asset.file
: path.resolve(path.dirname(manifestPath), asset.file);
}
resolveRepoIdentity(manifest);
if (manifest.kind !== "clusterflux-public-release") {
throw new Error(`unexpected public release manifest kind: ${manifest.kind}`);