| 12345678910111213141516171819202122232425262728293031 |
- #!/usr/bin/env node
- // --prepare is read-only. Deploy/rollback only after the user's release decision.
- import {readFileSync,writeFileSync,mkdirSync,existsSync} from 'node:fs';
- import {createHash} from 'node:crypto';
- import {homedir} from 'node:os';
- import {join} from 'node:path';
- import {patchAdminSpeed,speedHelpers} from './admin-speed-patch.mjs';
- const mode=process.argv.includes('--deploy')?'deploy':process.argv.includes('--rollback')?'rollback':'prepare';
- const dir=process.env.XIAOSHU_SPEED_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-11-10-admin-speed');
- 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;
- if(!app||!master||!token)throw Error('Missing authorized release configuration');
- const hash=s=>createHash('sha256').update(s).digest('hex');
- 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;}
- mkdirSync(dir,{recursive:true,mode:0o700});
- if(mode==='prepare'){
- if(existsSync(join(dir,'deploy-receipt.json')))throw Error('Release already deployed; choose a new directory to prepare another release');
- 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');
- 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']};
- 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}));
- }else{
- 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');
- if(hash(original.code)!==manifest.beforeHash||hash(candidate)!==manifest.afterHash)throw Error('Prepared artifacts changed');
- const current=await parse('/classes/Function/'+manifest.objectId),expected=mode==='deploy'?manifest.beforeHash:manifest.afterHash,target=mode==='deploy'?candidate:original.code;
- if(hash(current.code)===hash(target)){console.log(JSON.stringify({mode,unchanged:true}));process.exit(0);}
- if(hash(current.code)!==expected)throw Error('Online gateway changed; reprepare and revalidate instead of overwriting later work');
- 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();
- 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');
- await parse('/classes/Function/'+manifest.objectId,'PUT',{code:target});
- const verified=await parse('/classes/Function/'+manifest.objectId);if(hash(verified.code)!==hash(target))throw Error('Release readback mismatch');
- 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}));
- }
|