release.mjs 6.0 KB

1234567891011121314151617181920212223242526272829
  1. import fs from 'node:fs';import {createHash} from 'node:crypto';import {withGateway,parse,base,token} from './server-util.mjs';import {patchRound2} from '../../scripts/admin-speed-round2-patch.mjs';
  2. const dir='/Users/mac/.local/state/xiaoshu/cloud-releases/2026-11-10-admin-speed-round2',mode=process.argv[2]||'prepare',hash=x=>createHash('sha256').update(x).digest('hex');
  3. const original=JSON.parse(fs.readFileSync(dir+'/original-gateway.json')),rollbackSql=fs.readFileSync('scripts/sql/payroll-read-rollback.sql','utf8'),expectedOriginals=JSON.parse(fs.readFileSync(dir+'/original-definitions.json'));
  4. const originalSql="SELECT proname,pg_get_functiondef(p.oid) AS definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname IN ('xs_payroll_sources','xs_payroll_source_version','xs_payroll_cutoff_snapshot','xs_payroll_cutoff_conflicts') ORDER BY proname";
  5. const helpersSql="SELECT proname,pg_get_functiondef(p.oid) AS definition FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname='public' AND proname IN ('xs_payroll_preview_cache_write','xs_payroll_preview_cache_read','xs_payroll_snapshot_current','xs_payroll_preview_stamp','xs_payroll_cutoff_snapshot_fast','xs_payroll_source_version_fast','xs_payroll_source_version_stamp','xs_payroll_source_version_uncached_fast','xs_payroll_sources_cached','xs_payroll_source_stamp','xs_payroll_sources_current','xs_payroll_sources_uncached','xs_payroll_data_version','xs_payroll_digest_ready','xs_payroll_digest_row','xs_payroll_digest_truncate','xs_payroll_digest_signature','xs_payroll_authority_ready') ORDER BY proname";
  6. if(mode==='prepare'){
  7. const candidate=patchRound2(original.code),live=await parse('/classes/Function/'+original.objectId);if(hash(live.code)!==hash(original.code))throw Error('Gateway changed since baseline');
  8. const helpers=await withGateway('async function run(){return Psql.query('+JSON.stringify(helpersSql)+');}',call=>call({}));if(helpers.length!==18)throw Error('Incomplete acceleration helpers');
  9. fs.writeFileSync(dir+'/candidate.js',candidate,{mode:0o600});fs.writeFileSync(dir+'/helpers.json',JSON.stringify(helpers),{mode:0o600});fs.writeFileSync(dir+'/manifest.json',JSON.stringify({beforeHash:hash(original.code),afterHash:hash(candidate),helperHash:hash(JSON.stringify(helpers)),rollbackHash:hash(rollbackSql)},null,2),{mode:0o600});console.log(JSON.stringify({prepared:true,afterHash:hash(candidate),helpers:helpers.length}));
  10. }else{
  11. const manifest=JSON.parse(fs.readFileSync(dir+'/manifest.json')),expectedHelpers=JSON.parse(fs.readFileSync(dir+'/helpers.json')),candidate=fs.readFileSync(dir+'/candidate.js','utf8');if(hash(candidate)!==manifest.afterHash||hash(rollbackSql)!==manifest.rollbackHash)throw Error('Prepared release changed');
  12. async function run(p){
  13. if(p.action==='check'){if(JSON.stringify(await Psql.query(originalSql))!==JSON.stringify(expectedOriginals)||JSON.stringify(await Psql.query(helpersSql))!==JSON.stringify(expectedHelpers))throw Error('Database code drift');return Psql.one('SELECT xs_payroll_digest_ready() AS digest,xs_payroll_authority_ready() AS authority');}
  14. if(!['rehearse','rollback'].includes(p.action))throw Error('Unknown database action');
  15. try{return await Psql.tx(async tx=>{await tx.none("SET LOCAL lock_timeout='5s';SET LOCAL statement_timeout='60s';");if(JSON.stringify(await tx.query(originalSql))!==JSON.stringify(expectedOriginals)||JSON.stringify(await tx.query(helpersSql))!==JSON.stringify(expectedHelpers))throw Error('Database code drift');await tx.none(rollbackSql);if(JSON.stringify(await tx.query(originalSql))!==JSON.stringify(expectedOriginals))throw Error('Original financial functions changed');if(p.action==='rehearse')throw Error('ROLLBACK_REHEARSAL_OK');return{rolledBack:true};});}catch(e){if(p.action==='rehearse'&&e.message==='ROLLBACK_REHEARSAL_OK')return{rehearsed:true};throw e;}
  16. }
  17. const source='const originalSql='+JSON.stringify(originalSql)+';const helpersSql='+JSON.stringify(helpersSql)+';const expectedOriginals='+JSON.stringify(expectedOriginals)+';const expectedHelpers='+JSON.stringify(expectedHelpers)+';const rollbackSql='+JSON.stringify(rollbackSql)+';'+run.toString();
  18. await withGateway(source,async call=>{
  19. if(mode!=='rollback'){const ready=await call({action:'check'});if(!ready.digest||!ready.authority)throw Error('Fast path is not healthy');}
  20. if(mode==='rehearse'){const r=await call({action:'rehearse'}),after=await call({action:'check'});if(!after.digest||!after.authority)throw Error('Rehearsal did not restore infrastructure');fs.writeFileSync('perf/admin-speed-round2-2026-11-10/rollback-rehearsal.json',JSON.stringify({at:new Date().toISOString(),...r,restored:true,originalFinancialFunctionsUnchanged:true},null,2)+'\n');console.log(JSON.stringify(r));return;}
  21. if(!['deploy','rollback'].includes(mode))throw Error('Use prepare, rehearse, deploy or rollback');
  22. const status=await(await fetch(base+'/api/functions/xiaoshu/sync/direct-v1',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(20000)})).json();if(!Array.isArray(status.data?.runs)||status.data.runs.some(r=>r.status==='running'))throw Error('Synchronization must be idle');
  23. const live=await parse('/classes/Function/'+original.objectId),target=mode==='deploy'?candidate:original.code,expected=mode==='deploy'?manifest.beforeHash:manifest.afterHash;if(hash(live.code)!==expected&&hash(live.code)!==hash(target))throw Error('Gateway code drift');
  24. if(mode==='deploy'&&candidate!==patchRound2(original.code))throw Error('Working implementation differs from validated candidate');
  25. await parse('/classes/Function/'+original.objectId,'PUT',{code:target});if(hash((await parse('/classes/Function/'+original.objectId)).code)!==hash(target))throw Error('Gateway readback mismatch');
  26. if(mode==='rollback')await call({action:'rollback'});
  27. fs.writeFileSync(dir+'/'+mode+'-receipt.json',JSON.stringify({at:new Date().toISOString(),gatewayHash:hash(target),...manifest}),{mode:0o600});console.log(JSON.stringify({mode,verified:true,gatewayHash:hash(target)}));
  28. });
  29. }