Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions .github/workflows/deploy-strategy-switch-console.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1917,10 +1917,27 @@ jobs:
}
if (uploaded === prior || (separateWitness && uploaded === previousLatest)) fail();
stage = 'uploaded_configuration'; const next = resources(await request(`/versions/${uploaded}`), uploaded);
const comparable = value => ({ bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)),
script_runtime: value.script_runtime, handlers: value.script.handlers,
named_handlers: value.script.named_handlers === undefined ? null : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)),
placement_mode: value.script.placement_mode ?? null });
// A reviewed main release may add test-only handlers behind the __test
// export. Treat that export as code, not configuration: every other named
// handler stays strictly equal, and test handlers may only be added.
const allowTestHandlerAdditions = reviewedMain;
const namedHandlers = value => value.script.named_handlers === undefined ? null
: [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name));
const testExport = value => {
const named = namedHandlers(value);
if (!Array.isArray(named)) return { present: false, handlers: [], ambiguous: false };
const entries = named.filter(item => item.name === '__test');
const handlers = entries.length === 1 && Array.isArray(entries[0].handlers) ? [...entries[0].handlers].sort() : [];
return { present: entries.length > 0, handlers,
ambiguous: entries.length > 1 || (entries.length === 1 && entries[0].handlers !== undefined && !Array.isArray(entries[0].handlers)) };
};
const comparable = value => {
const named = namedHandlers(value);
return { bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)),
script_runtime: value.script_runtime, handlers: value.script.handlers,
named_handlers: allowTestHandlerAdditions && Array.isArray(named) ? named.filter(item => item.name !== '__test') : named,
Comment thread
Pigbibi marked this conversation as resolved.
Resolved
placement_mode: value.script.placement_mode ?? null };
};
const configHash = value => createHash('sha256').update(canonical(comparable(value))).digest('hex');
const desired = structuredClone(old);
if (flowMode) {
Expand All @@ -1932,6 +1949,14 @@ jobs:
const oldTag = old.script_runtime.migration_tag, newTag = next.script_runtime.migration_tag;
if (typeof oldTag !== 'string' || !oldTag || (newTag !== undefined && newTag !== oldTag)) fail('migration_tag_mismatch');
const normalized = structuredClone(next); normalized.script_runtime.migration_tag = oldTag;
if (reviewedMain) {
const before = testExport(desired), after = testExport(normalized);
if (before.ambiguous || after.ambiguous || before.present !== after.present) fail('test_export_shape_mismatch');
const retained = new Set(after.handlers), prior = new Set(before.handlers);
if (before.handlers.some(name => !retained.has(name))) fail('test_export_handlers_removed');
const added = after.handlers.filter(name => !prior.has(name));
if (added.length) console.log(JSON.stringify({ status: 'reviewed_main_test_export_handlers_added', added }));
}
if (configHash(normalized) !== configBefore || (!flowMode && next.script.etag === old.script.etag)) fail('configuration_hash_mismatch');
} else if (configUploaded !== configBefore || next.script.etag === old.script.etag) fail('configuration_hash_mismatch');
if (pr764 || betaRelease) {
Expand Down Expand Up @@ -1983,8 +2008,17 @@ jobs:
stage = 'readback'; await active(deploymentId, uploaded);
if (pr764 || integrated || reviewedMain || resume !== 'off') {
stage = 'post_release_configuration';
const configAfter = configHash(resources(await request(`/versions/${uploaded}`), uploaded));
const readback = resources(await request(`/versions/${uploaded}`), uploaded);
const configAfter = configHash(readback);
if (configAfter !== configBefore) fail('post_release_configuration_hash_mismatch');
// The reviewed-main hash excludes __test, so readback must still
// prove the exact validated test export survived activation.
if (reviewedMain) {
const verifiedTest = testExport(next), readbackTest = testExport(readback);
if (readbackTest.ambiguous || verifiedTest.present !== readbackTest.present
|| verifiedTest.handlers.length !== readbackTest.handlers.length
|| verifiedTest.handlers.some((name, index) => name !== readbackTest.handlers[index])) fail('post_release_test_export_mismatch');
}
// Version metadata hides secret values. Hash all comparable bindings/runtime/
// handlers/placement, never code etag or provider bookkeeping. No KV writes.
if (controlRuntimeCandidate) console.log(JSON.stringify({ status: 'integrated_configuration_verified', bindings_equal: true, runtime_equal: true, secret_values_read: false }));
Expand Down
15 changes: 11 additions & 4 deletions scripts/console_version_comparator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,17 @@ const canonical = value => JSON.stringify(value, function (_key, item) {
|| !value.bindings.some(b => b.name === 'STRATEGY_SWITCH_RUNTIME_INSTANCES' && b.type === 'durable_object_namespace')) fail();
return value;
}
const comparable = value => ({ bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)),
script_runtime: value.script_runtime, handlers: value.script.handlers,
named_handlers: value.script.named_handlers === undefined ? null : [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name)),
placement_mode: value.script.placement_mode ?? null });
// The comparator stays strict: test-only handler changes remain visible as a diff.
const allowTestHandlerAdditions = false;
const namedHandlers = value => value.script.named_handlers === undefined ? null
: [...value.script.named_handlers].sort((a,b) => a.name.localeCompare(b.name));
const comparable = value => {
const named = namedHandlers(value);
return { bindings: [...value.bindings].sort((a, b) => a.name.localeCompare(b.name)),
script_runtime: value.script_runtime, handlers: value.script.handlers,
named_handlers: allowTestHandlerAdditions && Array.isArray(named) ? named.filter(item => item.name !== '__test') : named,
placement_mode: value.script.placement_mode ?? null };
};
const configHash = value => createHash('sha256').update(canonical(comparable(value))).digest('hex');

const types = ['plain_text', 'json', 'secret_text', 'kv_namespace', 'durable_object_namespace'];
Expand Down
21 changes: 19 additions & 2 deletions tests/console_deploy_safety_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ async function releaseCase({pr764=false,ibkrDaily=false,activeOverride=null,inhe
releaseCases++;return {calls,stdout,stderr};
}
// Exact integrated candidate: explicit inheritance, verified module, one non-forced activation.
async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null } = {}, expected=true) {
async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c1b737d6e721eb9fe71a12c613fbb49d3fa51563", lineage=null, settings=false, daily=false, statusCandidate=false, cpu=false, active=null, latest=null, moduleContent=syntheticBundle.toString("base64"), uploadedTag=undefined, postTag=runtime.migration_tag, flow=false, oldFlag=null, drift=false, baselineItems=undefined, preUploadVersion=null, preDeployWitness=null, uploadId=null, ambiguousActive=false, expectFailure=null, testHandlers=null, deployed=false } = {}, expected=true) {
const calls=[], stdout=[], stderr=[], prior=settings?"7ffe9227-686a-48ad-baa9-72665969d918":daily?"14f0c9c2-09f9-40e9-bfbe-17165f499495":statusCandidate?"175ba768-484a-4fe6-9cea-e5a101bc1618":cpu?"90a0c8e9-b5ad-447a-998f-5d585ec28063":"600ec84d-a804-4ede-8d1d-0a34819ce46a";
active ??= prior; latest ??= active;
const separateWitness = reviewedMain && reviewedMainSource === inboxSource && !flow;
Expand All @@ -772,10 +772,16 @@ async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c
for (const i of [7, 10]) responses[i].result.resources.bindings.push({name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"});
if (drift) responses[7].result.resources.bindings[0].namespace_id="e".repeat(32);
}
if (testHandlers) {
if (testHandlers.active) responses[1].result.resources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.active]});
if (testHandlers.uploaded) responses[7].result.resources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.uploaded]});
if (testHandlers.runtimeUploaded) responses[7].result.resources.script.named_handlers.find(item=>item.name==="RuntimeInstances").handlers=[...testHandlers.runtimeUploaded];
}
const uploadedResources=responses[7].result.resources;
if (uploadedTag === undefined) delete uploadedResources.script_runtime.migration_tag;
else uploadedResources.script_runtime.migration_tag=uploadedTag;
const postResources=resource("new-code");
if (testHandlers?.post) postResources.script.named_handlers.push({name:"__test",handlers:[...testHandlers.post]});
if (flow) postResources.bindings.push({name:"CONSOLE_EXTERNAL_FLOW_ENABLED",type:"plain_text",text:"true"});
if (postTag === null) delete postResources.script_runtime.migration_tag;
else postResources.script_runtime.migration_tag=postTag;
Expand All @@ -801,9 +807,10 @@ async function integratedReleaseCase({ reviewedMain=false, reviewedMainSource="c
assert.equal(JSON.parse(stdout.at(-1)).prior_version,prior);
}
if (flow) assert.equal(JSON.parse(stdout.at(-1)).external_flow_enabled,true); else assert.equal(JSON.parse(stdout.at(-1)).version_id,newVersion);
} else {if (postTag !== null) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false);
} else {if (postTag !== null && !deployed) assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,0); else { assert.equal(posts.filter(x=>x.url.includes("/deployments?")).length,1); assert.equal(JSON.parse(stderr.at(-1)).outcome,"UNKNOWN_REQUIRES_READBACK"); } assert.equal(JSON.parse(stderr.at(-1)).retry,false);
if (expectFailure) { const error=JSON.parse(stderr.at(-1)); for (const [key,value] of Object.entries(expectFailure)) assert.equal(error[key],value,key); }}
assert.ok(!JSON.stringify({stdout,stderr}).includes(privateRelease));
return {stdout,stderr};
}
// Execute the actual publisher for the exact merged source, with fake provider responses.
await integratedReleaseCase({reviewedMain:true});
Expand All @@ -812,6 +819,16 @@ await integratedReleaseCase({reviewedMain:true,lineage:priorVersion},false);
await integratedReleaseCase({reviewedMain:true,moduleContent:Buffer.from("wrong").toString("base64")},false);
await integratedReleaseCase({reviewedMain:true,uploadedTag:"wrong-tag"},false);
await integratedReleaseCase({reviewedMain:true,postTag:null},false);
const configStop={retry:false,deploy_attempted:false,stage:"uploaded_configuration",upload_attempted:true,outcome:"UPLOAD_REQUIRES_READBACK_NOT_ACTIVATED"};
const addedTest=await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha","beta"]}});
assert.ok(addedTest.stdout.some(line=>JSON.parse(line).status==="reviewed_main_test_export_handlers_added"),"additive __test handlers are recorded");
await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha","beta"],uploaded:["alpha"],post:["alpha"]},expectFailure:{...configStop,checkpoint:"test_export_handlers_removed"}},false);
await integratedReleaseCase({reviewedMain:true,testHandlers:{uploaded:["alpha"],post:["alpha"]},expectFailure:{...configStop,checkpoint:"test_export_shape_mismatch"}},false);
await integratedReleaseCase({reviewedMain:true,testHandlers:{active:["alpha"],uploaded:["alpha"],post:["alpha"],runtimeUploaded:["other"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false);
await integratedReleaseCase({testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha","beta"]},expectFailure:{...configStop,checkpoint:"configuration_hash_mismatch"}},false);
const postTestStop={retry:false,deploy_attempted:true,stage:"post_release_configuration",upload_attempted:true,outcome:"UNKNOWN_REQUIRES_READBACK",checkpoint:"post_release_test_export_mismatch"};
await integratedReleaseCase({reviewedMain:true,deployed:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"],post:["alpha"]},expectFailure:postTestStop},false);
await integratedReleaseCase({reviewedMain:true,deployed:true,testHandlers:{active:["alpha"],uploaded:["alpha","beta"]},expectFailure:postTestStop},false);
const orphanWitness="00000000-0000-4000-8000-000000000021";
const driftedWitness="00000000-0000-4000-8000-000000000022";
const driftedActive="00000000-0000-4000-8000-000000000023";
Expand Down
Loading