release-admin-speed.mjs 3.8 KB

12345678910111213141516171819202122232425262728293031
  1. #!/usr/bin/env node
  2. // --prepare is read-only. Deploy/rollback only after the user's release decision.
  3. import {readFileSync,writeFileSync,mkdirSync,existsSync} from 'node:fs';
  4. import {createHash} from 'node:crypto';
  5. import {homedir} from 'node:os';
  6. import {join} from 'node:path';
  7. import {patchAdminSpeed,speedHelpers} from './admin-speed-patch.mjs';
  8. const mode=process.argv.includes('--deploy')?'deploy':process.argv.includes('--rollback')?'rollback':'prepare';
  9. const dir=process.env.XIAOSHU_SPEED_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-11-10-admin-speed');
  10. const base=process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse',app=process.env.XIAOSHU_PARSE_APP_ID,master=process.env.XIAOSHU_MASTER_KEY,token=process.env.XIAOSHU_SYNC_OPERATOR_TOKEN;
  11. if(!app||!master||!token)throw Error('Missing authorized release configuration');
  12. const hash=s=>createHash('sha256').update(s).digest('hex');
  13. async function parse(path,method='GET',body){const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(30000)});if(!r.ok)throw Error('Release API HTTP '+r.status);const d=await r.json();if(d.error)throw Error('Release API rejected request');return d;}
  14. mkdirSync(dir,{recursive:true,mode:0o700});
  15. if(mode==='prepare'){
  16. if(existsSync(join(dir,'deploy-receipt.json')))throw Error('Release already deployed; choose a new directory to prepare another release');
  17. const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({path:'xiaoshu/ops/gateway-v3'}),limit:'2'}))).results;if(rows.length!==1)throw Error('Ambiguous gateway');
  18. const original=rows[0],candidate=patchAdminSpeed(original.code),manifest={preparedAt:new Date().toISOString(),objectId:original.objectId,path:original.path,beforeHash:hash(original.code),afterHash:hash(candidate),helpers:speedHelpers,newHelpers:['lessonHistoryLabelColumns','lessonHistoryLabelJoins']};
  19. writeFileSync(join(dir,'original.json'),JSON.stringify(original),{mode:0o600});writeFileSync(join(dir,'candidate.js'),candidate,{mode:0o600});writeFileSync(join(dir,'manifest.json'),JSON.stringify(manifest,null,2),{mode:0o600});console.log(JSON.stringify({mode,dir,...manifest}));
  20. }else{
  21. const manifest=JSON.parse(readFileSync(join(dir,'manifest.json'),'utf8')),original=JSON.parse(readFileSync(join(dir,'original.json'),'utf8')),candidate=readFileSync(join(dir,'candidate.js'),'utf8');
  22. if(hash(original.code)!==manifest.beforeHash||hash(candidate)!==manifest.afterHash)throw Error('Prepared artifacts changed');
  23. const current=await parse('/classes/Function/'+manifest.objectId),expected=mode==='deploy'?manifest.beforeHash:manifest.afterHash,target=mode==='deploy'?candidate:original.code;
  24. if(hash(current.code)===hash(target)){console.log(JSON.stringify({mode,unchanged:true}));process.exit(0);}
  25. if(hash(current.code)!==expected)throw Error('Online gateway changed; reprepare and revalidate instead of overwriting later work');
  26. const r=await fetch(base.replace(/\/parse\/?$/,'/api/functions/xiaoshu/sync/direct-v1'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,params:{action:'status'}}),signal:AbortSignal.timeout(30000)}),s=await r.json();
  27. if(!r.ok||s.success===false||!Array.isArray(s.data?.runs)||s.data.runs.some(run=>run.status==='running'))throw Error('Cannot verify synchronization is idle');
  28. await parse('/classes/Function/'+manifest.objectId,'PUT',{code:target});
  29. const verified=await parse('/classes/Function/'+manifest.objectId);if(hash(verified.code)!==hash(target))throw Error('Release readback mismatch');
  30. const receipt={at:new Date().toISOString(),hash:hash(target)};writeFileSync(join(dir,mode+'-receipt.json'),JSON.stringify(receipt),{mode:0o600});console.log(JSON.stringify({mode,verified:true,...receipt}));
  31. }