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
58 changes: 58 additions & 0 deletions .github/workflows/deploy-strategy-switch-console.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,64 @@ jobs:
}
JS

- name: Read fixed frontend baseline schema categories
if: inputs.release_frontend_assets
run: |
node --input-type=module <<'JS'
// Read-only diagnostic: keys, types and counts only. Never binding names,
// values, raw errors, credentials or response bodies. One fixed GET.
const fail = () => { throw new Error('frontend_schema_readback_failed'); };
let httpStatus = null;
try {
const account = process.env.CLOUDFLARE_ACCOUNT_ID, token = process.env.CLOUDFLARE_API_TOKEN;
if (process.env.GITHUB_REF !== 'refs/heads/main' || !token || !/^[0-9a-f]{32}$/i.test(account ?? '')) fail();
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${account}/workers/scripts/qsl-strategy-switch-console/versions/2beea000-cce5-43a5-9255-9724f7318e83`, {
method: 'GET', redirect: 'error', signal: AbortSignal.timeout(15000),
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', 'Accept-Encoding': 'identity' },
});
httpStatus = response.status;
if (!response.ok) { await response.body?.cancel(); fail(); }
if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? '') || !['', 'identity'].includes(response.headers.get('content-encoding') ?? '')) fail();
const length = response.headers.get('content-length');
if (length !== null && (!/^\d+$/.test(length) || Number(length) > 524288)) fail();
const reader = response.body?.getReader(); if (!reader) fail();
let size = 0, text = ''; const decoder = new TextDecoder('utf-8', { fatal: true });
try {
for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.byteLength; if (size > 524288) fail(); text += decoder.decode(value, { stream: true }); }
text += decoder.decode();
} finally { await reader.cancel(); }
if (length !== null && Number(length) !== size) fail();
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 shape = x => {
if (!x || kind(x) !== 'object') return { kind: kind(x) };
const entries = Object.entries(x); if (entries.length > 128) fail();
return { kind: 'object', fields: Object.fromEntries(entries.filter(([k]) => keys.includes(k)).map(([k,v]) => [k,kind(v)])), unknown_fields: entries.filter(([k]) => !keys.includes(k)).length };
};
const resources = body.result?.resources, bindings = resources?.bindings;
const categories = [];
const types = ['plain_text','json','secret_text','kv_namespace','durable_object_namespace','service','r2_bucket','d1','ai','browser','analytics_engine','version_metadata','secret_key','secrets_store_secret','queue','workflow','assets'];
if (Array.isArray(bindings)) {
if (bindings.length > 128) fail();
for (const binding of bindings) {
const type = types.includes(binding?.type) ? binding.type : 'OTHER';
const fields = shape(binding); const signature = JSON.stringify({ type, shape: fields });
const found = categories.find(item => item.signature === signature);
if (found) found.count++; else categories.push({ signature, type, shape: fields, count: 1 });
}
}
console.log(JSON.stringify({ status: 'frontend_schema_categories', version_id: '2beea000-cce5-43a5-9255-9724f7318e83', result_id_matches: body.result?.id === '2beea000-cce5-43a5-9255-9724f7318e83', result: shape(body.result), resources: shape(resources),
binding_container: kind(bindings), binding_count: Array.isArray(bindings) ? bindings.length : null,
binding_categories: categories.map(({ signature, ...item }) => item), script: shape(resources?.script), runtime: shape(resources?.script_runtime),
writes: 0, retry: false }));
} catch {
console.error(JSON.stringify({ error: 'frontend_schema_readback_failed', http_status: httpStatus, writes: 0, retry: false })); process.exitCode = 1;
}
JS


deploy:
if: github.event_name == 'workflow_dispatch' && !inputs.read_only_preflight && !inputs.rollback_to_pre_release
runs-on: ubuntu-latest
Expand Down
27 changes: 26 additions & 1 deletion tests/console_deploy_safety_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ assert.match(preflight, /environment: runtime-strategy-switch/);
assert.doesNotMatch(preflight, /secret put|sync-strategy-profiles|wrangler.*deploy|npm|npx|uses: actions\/checkout|SYNC_TOKEN|WRANGLER_CONFIG/);
assert.deepEqual([...preflight.matchAll(/^ ([A-Z_]+):/gm)].map(match => match[1]), ["CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"]);

// The schema diagnostic is read-only, fixed to the same historical version,
// and is gated inside the existing preflight job. Its projection never emits values.
const schemaStep = steps.find(s => s.startsWith("Read fixed frontend baseline schema categories\n"));
assert.ok(schemaStep && preflight.includes("Read fixed frontend baseline schema categories"));
assert.match(schemaStep, /^ if: inputs\.release_frontend_assets$/m);
const schemaScript = schemaStep.match(/node --input-type=module <<'JS'\n([\s\S]*?)\n JS/)[1].split("\n").map(l=>l.slice(10)).join("\n");
const runSchema = new (Object.getPrototypeOf(async function(){}).constructor)("process","fetch","console",schemaScript);
let schemaCases = 0;
async function schemaCase(payload, expected=true, status=200, env={}) {
const stdout=[],stderr=[],calls=[]; const secret="PRIVATE_SCHEMA_SENTINEL";
const process={env:{GITHUB_REF:"refs/heads/main",CLOUDFLARE_ACCOUNT_ID:"a".repeat(32),CLOUDFLARE_API_TOKEN:secret,...env},exitCode:0};
await runSchema(process,async(url,options)=>{calls.push({url,options});return new Response(typeof payload==="string"?payload:JSON.stringify(payload),{status,headers:{"content-type":"application/json"}});},{log:x=>stdout.push(x),error:x=>stderr.push(x)});
assert.equal(process.exitCode===0,expected);assert.equal(calls.length,env.GITHUB_REF?0:1);
if(calls.length){assert.equal(calls[0].options.method,"GET");assert.equal(calls[0].options.redirect,"error");assert.equal(calls[0].url,`https://api.cloudflare.com/client/v4/accounts/${"a".repeat(32)}/workers/scripts/qsl-strategy-switch-console/versions/2beea000-cce5-43a5-9255-9724f7318e83`);}
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 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);
await schemaCase({...schemaFixture,success:false},false);await schemaCase("{",false);await schemaCase("x".repeat(524289),false);
await schemaCase("PRIVATE_SCHEMA_SENTINEL",false,403);await schemaCase(schemaFixture,false,200,{GITHUB_REF:"refs/heads/other"});

// Execute the exact selection shell with a fake Git boundary, never checkout the
// test repository or evaluate any selected Worker. Real object identities are
// separately frozen by the release evidence and the required ancestry check.
Expand Down Expand Up @@ -512,4 +537,4 @@ const zeroTotal = await failure([{ success: true, result: { deployments: [{ ...d
{ version_id: versionId, percentage: 0 }, { version_id: deploymentId, percentage: 0 },
] }] } }]);
assert.equal(zeroTotal.checkpoint, "deployment_percentage_total", "all-zero traffic is still invalid");
console.log(`console_deploy_safety_validation: PASS (manual-only; 16 original write gates; exclusive preflight/deploy/restore; 2 exact Binance gates; ${cases} offline reader cases; ${sourceCases} source cases; ${restoreCases} restore cases; ${overlayCases} overlay cases; ${configCases} config cases; ${releaseCases} isolated release cases)`);
console.log(`console_deploy_safety_validation: PASS (manual-only; 16 original write gates; exclusive preflight/deploy/restore; 2 exact Binance gates; ${cases} offline reader cases; ${sourceCases} source cases; ${restoreCases} restore cases; ${overlayCases} overlay cases; ${configCases} config cases; ${releaseCases} isolated release cases; ${schemaCases} schema readback cases)`);
Loading