Просмотр исходного кода

fix(cloud): prevent recursive reservation preview deployment

彭峰 3 недель назад
Родитель
Сommit
fdb28c278b

+ 2 - 1
scripts/deploy-payroll-integrity.mjs

@@ -1,6 +1,7 @@
 #!/usr/bin/env node
 // Patch only payroll entry points in the live shared gateway; preserve other releases.
 import fs from 'node:fs';import path from 'node:path';import {createHash} from 'node:crypto';
+import {normalizeHistoricalReservationPreviewBase} from './payroll-gateway-transform.mjs';
 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;
 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});
 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;}
@@ -13,9 +14,9 @@ for(const name of ['xiaoshu.ops.gateway','xiaoshu.ops.gateway.v3','xiaoshu.admin
   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);
  }else{
   if(code.includes(marker))code=code.slice(0,code.indexOf(marker));
+  code=normalizeHistoricalReservationPreviewBase(code);
   code=code.replace(/^function roleOf\(context\).*$/m,raw.match(/^function roleOf\(context\).*$/m)[0]);
   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']);");
-  code=code.replace('async function historicalReservationPreview(context,input){','async function historicalReservationPreviewLegacy(context,input){');
   code=code.replace("requireRole(context,['ops-manager','ops-staff','ops-auditor']);await releaseExpiredReservations(context,input);","requireRole(context,['ops-manager','ops-staff','ops-auditor']);");
   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]);}
   const extra=["ops/payroll/coaches","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');

+ 14 - 0
scripts/payroll-gateway-transform.mjs

@@ -0,0 +1,14 @@
+const brokenHistoricalReservationWrapper = /async function historicalReservationPreviewLegacy\(context,input\)\{const co=payrollTenant\(context,input\),data=await historicalReservationPreviewLegacy\(context,input\)[^\n]*\}\n?/g;
+
+export function normalizeHistoricalReservationPreviewBase(code) {
+  let normalized = code.replace(brokenHistoricalReservationWrapper, '');
+  if (!normalized.includes('async function historicalReservationPreviewLegacy(context,input){')) {
+    normalized = normalized.replace(
+      'async function historicalReservationPreview(context,input){',
+      'async function historicalReservationPreviewLegacy(context,input){',
+    );
+  }
+  const legacyCount = normalized.split('async function historicalReservationPreviewLegacy(context,input){').length - 1;
+  if (legacyCount !== 1) throw new Error('历史预约预览基础函数版本不兼容');
+  return normalized;
+}

+ 23 - 0
scripts/tests/payroll-gateway-transform.test.mjs

@@ -0,0 +1,23 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { normalizeHistoricalReservationPreviewBase } from '../payroll-gateway-transform.mjs';
+
+const original = "async function historicalReservationPreview(context,input){return 'legacy';}\n";
+const legacy = "async function historicalReservationPreviewLegacy(context,input){return 'legacy';}\n";
+const broken = "async function historicalReservationPreviewLegacy(context,input){const co=payrollTenant(context,input),data=await historicalReservationPreviewLegacy(context,input),rows=await Psql.query('SELECT 1');return{...data,items:data.items.map(i=>i)};}\n";
+
+test('首次增量发布会保留一个旧预览入口', () => {
+  const result = normalizeHistoricalReservationPreviewBase(original);
+  assert.equal(result.includes('historicalReservationPreview(context,input)'), false);
+  assert.equal(result.split('historicalReservationPreviewLegacy(context,input)').length - 1, 1);
+});
+
+test('重复增量发布会移除递归包装并保留真正的旧预览', () => {
+  const result = normalizeHistoricalReservationPreviewBase(legacy + broken);
+  assert.equal(result, legacy);
+  assert.equal(result.includes('data=await historicalReservationPreviewLegacy'), false);
+});
+
+test('缺少历史预约预览入口时拒绝发布', () => {
+  assert.throws(() => normalizeHistoricalReservationPreviewBase('function unrelated(){}'), /版本不兼容/);
+});