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
22 changes: 19 additions & 3 deletions .github/workflows/deploy-strategy-switch-console.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ jobs:
const body = JSON.parse(text);
if (body?.success !== true || (body.errors !== undefined && (!Array.isArray(body.errors) || body.errors.length))) fail();
const kind = x => x === null ? 'null' : Array.isArray(x) ? 'array' : typeof x;
const keys = ['resources','metadata','number','annotations','startup_time_ms','bindings','script','script_runtime','etag','handlers','last_deployed_from','placement_mode','compatibility_date','compatibility_flags','usage_model','limits','migration_tag','exports','name','type','namespace_id','class_name','script_name','environment','text','json','dispatch_namespace','id','raw','tag','version_id','secret_name','store_id'];
const keys = ['resources','metadata','number','annotations','startup_time_ms','bindings','script','script_runtime','etag','handlers','named_handlers','last_deployed_from','placement_mode','compatibility_date','compatibility_flags','usage_model','limits','migration_tag','exports','name','type','namespace_id','class_name','script_name','environment','text','json','dispatch_namespace','id','raw','tag','version_id','secret_name','store_id'];
const shape = x => {
if (!x || kind(x) !== 'object') return { kind: kind(x) };
const entries = Object.entries(x); if (entries.length > 128) fail();
Expand Down Expand Up @@ -730,12 +730,26 @@ jobs:
if (!value || Object.keys(value).some(key => !['bindings', 'script', 'script_runtime'].includes(key))
|| !Array.isArray(value.bindings) || !value.bindings.length || value.bindings.length > 128 || !runtime || !script
|| Object.keys(runtime).some(key => !['compatibility_date', 'compatibility_flags', 'usage_model', 'limits', 'migration_tag', 'exports'].includes(key))
|| Object.keys(script).some(key => !['etag', 'handlers', 'last_deployed_from', 'placement_mode'].includes(key))
|| Object.keys(script).some(key => !['etag', 'handlers', 'named_handlers', 'last_deployed_from', 'placement_mode'].includes(key))
|| typeof script.etag !== 'string' || !script.etag || script.etag.length > 256
|| !(script.handlers === null || (Array.isArray(script.handlers) && script.handlers.every(x => typeof x === 'string')))
|| !/^\d{4}-\d{2}-\d{2}$/.test(runtime.compatibility_date ?? '')
|| (runtime.compatibility_flags !== undefined && (!Array.isArray(runtime.compatibility_flags) || runtime.compatibility_flags.some(x => typeof x !== 'string')))
|| !['standard', 'bundled', 'unbound'].includes(runtime.usage_model)) fail();
// Current API also reports named exports (including DO classes).
// Validate their identity/handler shape and compare them below; do
// not discard this field merely to accept a newer response schema.
if (script.named_handlers !== undefined) {
if (!Array.isArray(script.named_handlers) || script.named_handlers.length > 128) fail();
const exports = new Set();
for (const item of script.named_handlers) {
if (!item || Object.keys(item).some(key => !['name', 'handlers'].includes(key))
|| typeof item.name !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(item.name) || exports.has(item.name)
|| (item.handlers !== undefined && (!Array.isArray(item.handlers) || item.handlers.length > 32
|| item.handlers.some(handler => typeof handler !== 'string' || !/^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/.test(handler))))) fail();
exports.add(item.name);
}
}
const names = new Set();
for (const binding of value.bindings) {
if (!binding || !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(binding.name ?? '') || names.has(binding.name)
Expand Down Expand Up @@ -776,7 +790,9 @@ jobs:
if (uploaded === prior) 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, placement_mode: value.script.placement_mode ?? null });
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 });
if (canonical(comparable(next)) !== canonical(comparable(old)) || next.script.etag === old.script.etag) fail();
// Same-Worker workflow concurrency serializes our release controls. These fresh
// GETs detect observed external drift; Cloudflare offers no compare-and-swap
Expand Down
14 changes: 10 additions & 4 deletions tests/console_deploy_safety_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ async function schemaCase(payload, expected=true, status=200, env={}) {
assert.ok(!JSON.stringify({stdout,stderr}).includes(secret));assert.ok(!JSON.stringify({stdout,stderr}).includes("a".repeat(32)));schemaCases++;
return stdout.length?JSON.parse(stdout[0]):JSON.parse(stderr[0]);
}
const schemaFixture={success:true,result:{id:"2beea000-cce5-43a5-9255-9724f7318e83",resources:{bindings:[{name:"PRIVATE_SCHEMA_SENTINEL",type:"kv_namespace",namespace_id:"PRIVATE_SCHEMA_SENTINEL"},{name:"OTHER",type:"secret_text",text:"PRIVATE_SCHEMA_SENTINEL"}],script:{etag:"PRIVATE_SCHEMA_SENTINEL",handlers:["fetch"]},script_runtime:{compatibility_date:"2026-06-08",usage_model:"standard"}}}};
const projected=await schemaCase(schemaFixture);assert.equal(projected.binding_count,2);assert.equal(projected.binding_categories[0].shape.fields.namespace_id,"string");
const schemaFixture={success:true,result:{id:"2beea000-cce5-43a5-9255-9724f7318e83",resources:{bindings:[{name:"PRIVATE_SCHEMA_SENTINEL",type:"kv_namespace",namespace_id:"PRIVATE_SCHEMA_SENTINEL"},{name:"OTHER",type:"secret_text",text:"PRIVATE_SCHEMA_SENTINEL"}],script:{etag:"PRIVATE_SCHEMA_SENTINEL",handlers:["fetch"],named_handlers:[{name:"PRIVATE_SCHEMA_SENTINEL",handlers:[]}]},script_runtime:{compatibility_date:"2026-06-08",usage_model:"standard"}}}};
const projected=await schemaCase(schemaFixture);assert.equal(projected.script.fields.named_handlers,"array");assert.equal(projected.binding_count,2);assert.equal(projected.binding_categories[0].shape.fields.namespace_id,"string");
const wrongId=structuredClone(schemaFixture);wrongId.result.id="PRIVATE_SCHEMA_SENTINEL";assert.equal((await schemaCase(wrongId)).result_id_matches,false);
const unknownSchema=structuredClone(schemaFixture);unknownSchema.result.resources.PRIVATE_SCHEMA_SENTINEL="PRIVATE_SCHEMA_SENTINEL";unknownSchema.result.resources.bindings[0].type="PRIVATE_SCHEMA_SENTINEL";
assert.equal((await schemaCase(unknownSchema)).resources.unknown_fields,1);
Expand Down Expand Up @@ -272,7 +272,7 @@ const remoteBindings=[{name:"STRATEGY_SWITCH_CONFIG",type:"kv_namespace",namespa
{name:"STRATEGY_SWITCH_RUNTIME_INSTANCES",type:"durable_object_namespace",namespace_id:"c".repeat(32),class_name:"RuntimeInstances"},
{name:"FLAG",type:"plain_text",text:privateRelease},{name:"DATA",type:"json",json:{private:privateRelease}},{name:"SECRET",type:"secret_text"}];
const runtime={compatibility_date:"2026-06-08",compatibility_flags:[],usage_model:"standard",limits:{},migration_tag:"runtime-instances-v1",exports:{RuntimeInstances:{type:"durable-object",storage:"sqlite"}}};
const resource=etag=>({bindings:structuredClone(remoteBindings),script_runtime:structuredClone(runtime),script:{etag,handlers:["fetch","scheduled"],last_deployed_from:"api"}});
const resource=etag=>({bindings:structuredClone(remoteBindings),script_runtime:structuredClone(runtime),script:{etag,handlers:["fetch","scheduled"],named_handlers:[{name:"RuntimeInstances",handlers:[]}],last_deployed_from:"api"}});
const wrap=result=>({success:true,errors:[],result}), dep=(id,v)=>({id,strategy:"percentage",versions:[{version_id:v,percentage:100}]});
const responseSequence=()=>[wrap({deployments:[dep(priorDeployment,priorVersion)]}),wrap({id:priorVersion,resources:resource("old-code")}),
wrap({items:[{id:priorVersion}]}),wrap({id:newVersion}),wrap({id:newVersion,resources:resource("new-code")}),
Expand Down Expand Up @@ -311,11 +311,17 @@ for(const [at,change] of [
[1,x=>{x.result.resources.bindings[0].namespace_id={private:privateRelease};}],
[1,x=>{delete x.result.resources.bindings[2].text;}],[1,x=>{delete x.result.resources.bindings[3].json;}],
[1,x=>{x.result.resources.bindings[4].text=privateRelease;}],
[1,x=>{x.result.resources.script.named_handlers={};}],
[1,x=>{delete x.result.resources.script.named_handlers[0].name;}],
[1,x=>{x.result.resources.script.named_handlers[0].handlers="PRIVATE_RELEASE_SENTINEL";}],
[1,x=>{x.result.resources.script.named_handlers.push(x.result.resources.script.named_handlers[0]);}],
[1,x=>{x.result.resources.script_runtime.unknown=privateRelease;}],[1,x=>{x.result.resources.bindings[0].type="service";}],[1,x=>{x.result.resources.assets={};}],
[2,x=>{x.result.items[0].id=newVersion;}],[3,x=>{x.result.id="untrusted";}],
[4,x=>{x.result.resources.bindings[0].namespace_id="e".repeat(32);}],[4,x=>{x.result.resources.bindings.pop();}],[4,x=>{x.result.resources.bindings[2].text="different";}],
[4,x=>{x.result.resources.script_runtime.compatibility_date="2026-06-09";}],[4,x=>{x.result.resources.script_runtime.limits={cpu_ms:1};}],
[4,x=>{x.result.resources.script_runtime.migration_tag="changed";}],[4,x=>{x.result.resources.script.handlers=["fetch"];}],[4,x=>{x.result.resources.script.etag="old-code";}],
[4,x=>{x.result.resources.script_runtime.migration_tag="changed";}],[4,x=>{x.result.resources.script.named_handlers[0].name="ChangedExport";}],
[4,x=>{x.result.resources.script.named_handlers[0].handlers=["alarm"];}],
[4,x=>{x.result.resources.script.handlers=["fetch"];}],[4,x=>{x.result.resources.script.etag="old-code";}],
[5,x=>{x.result.deployments[0].versions[0].version_id=newVersion;}],[6,x=>{x.result.items[0].id=priorVersion;}],[7,x=>{x.result.versions[0].percentage=99;}],[8,x=>{x.result.deployments[0].id=priorDeployment;}]
])await releaseCase({mutate:r=>change(r[at])},false,at+1);
for (const corrupt of [b=>{delete b.namespace_id;},b=>{b.namespace_id={private:privateRelease};}]) {
Expand Down
Loading