deploy-payroll-integrity.mjs 3.8 KB

1234567891011121314151617181920212223242526272829
  1. #!/usr/bin/env node
  2. // Patch only payroll entry points in the live shared gateway; preserve other releases.
  3. import fs from 'node:fs';import path from 'node:path';import {createHash} from 'node:crypto';
  4. import {normalizeHistoricalReservationPreviewBase} from './payroll-gateway-transform.mjs';
  5. const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main',master=process.env.XIAOSHU_MASTER_KEY;
  6. if(!master)throw Error('缺少 XIAOSHU_MASTER_KEY');const out=process.env.XIAOSHU_PAYROLL_REPORT_DIR||path.resolve('artifacts/payroll-integrity');fs.mkdirSync(out,{recursive:true,mode:0o700});
  7. async function api(p,method='GET',body){const res=await fetch(base+p,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:body?JSON.stringify(body):undefined});const data=await res.json();if(!res.ok||data.error)throw Error(data.error||'HTTP '+res.status);return data;}
  8. const source=fs.readFileSync(new URL('./deploy-admin-functions.mjs',import.meta.url),'utf8');const raw=source.match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1];
  9. const marker='\n// PAYROLL_INTEGRITY_RELEASE\n';
  10. for(const name of ['xiaoshu.ops.gateway','xiaoshu.ops.gateway.v3','xiaoshu.admin.gateway']){
  11. const rows=(await api('/classes/Function?where='+encodeURIComponent(JSON.stringify({name})))).results;if(rows.length!==1)throw Error('云函数名称不唯一 '+name);const old=rows[0];
  12. fs.writeFileSync(path.join(out,name+'.before-'+Date.now()+'.json'),JSON.stringify(old,null,2),{mode:0o600});let code=old.code;
  13. if(name==='xiaoshu.admin.gateway'){
  14. const desired=source.match(/^function operationsRoleOf\(context\).*$/m)[0];if(!/^function operationsRoleOf\(context\).*$/m.test(code))throw Error('身份网关版本不兼容');code=code.replace(/^function operationsRoleOf\(context\).*$/m,desired);
  15. }else{
  16. if(code.includes(marker))code=code.slice(0,code.indexOf(marker));
  17. code=normalizeHistoricalReservationPreviewBase(code);
  18. code=code.replace(/^function roleOf\(context\).*$/m,raw.match(/^function roleOf\(context\).*$/m)[0]);
  19. if(!code.includes("context=await auth(request);requireRole(context,['ops-manager','ops-staff','ops-auditor']);"))code=code.replace('context=await auth(request);',"context=await auth(request);requireRole(context,['ops-manager','ops-staff','ops-auditor']);");
  20. code=code.replace("requireRole(context,['ops-manager','ops-staff','ops-auditor']);await releaseExpiredReservations(context,input);","requireRole(context,['ops-manager','ops-staff','ops-auditor']);");
  21. for(const op of ['export','exclude-line']){const re=new RegExp("^ if\\(operation==='ops/payroll/"+op+"'\\).*?$",'m');if(!re.test(code))throw Error('工资路由版本不兼容 '+op);code=code.replace(re,raw.match(re)[0]);}
  22. const extra=["ops/payroll/coaches","ops/payroll/review-rewards/confirm","ops/course-mappings/students","ops/course-mappings/student-commit"].filter(op=>!code.includes("operation==='"+op+"'" )).map(op=>raw.split('\n').find(line=>line.includes("operation==='"+op+"'"))).join('\n');
  23. if(extra)code=code.replace(" if(operation==='ops/payroll/overview')",extra+"\n if(operation==='ops/payroll/overview')");
  24. code+=marker+fs.readFileSync(new URL('./cloud/review-rewards.js',import.meta.url),'utf8')+fs.readFileSync(new URL('./cloud/payroll-integrity.js',import.meta.url),'utf8');
  25. }
  26. new Function(code);const current=await api('/classes/Function/'+old.objectId);if(current.updatedAt!==old.updatedAt)throw Error('云函数被其他发布更新,请重试 '+name);
  27. await api('/classes/Function/'+old.objectId,'PUT',{code});const checked=await api('/classes/Function/'+old.objectId);if(checked.code!==code)throw Error('发布后代码核对失败 '+name);
  28. console.log(JSON.stringify({name,sha256:createHash('sha256').update(code).digest('hex'),verified:true}));
  29. }