| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 |
- #!/usr/bin/env node
- // Read-only release preparation. Publishing is handled by the serialized operator release workflow.
- import fs from 'node:fs';
- import {createHash} from 'node:crypto';
- import {homedir} from 'node:os';
- import {join,resolve} from 'node:path';
- import {fileURLToPath} from 'node:url';
- import ts from 'typescript';
- function body(code) {
- const ast=ts.createSourceFile('gateway.js',code,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS);
- const nodes=ast.statements.filter(n=>ts.isFunctionDeclaration(n)&&n.name?.text==='contentPage');
- if(nodes.length!==1)throw Error('Expected exactly one contentPage function');
- const node=nodes[0];return{start:node.getStart(ast),end:node.end,text:code.slice(node.getStart(ast),node.end)};
- }
- export function patchWordClassroomRead(code) {
- const source=fs.readFileSync(new URL('./deploy-admin-functions.mjs',import.meta.url),'utf8');
- const local=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)?.[1];
- if(!local)throw Error('Missing local app gateway');
- const target=body(local).text,old=body(code);
- if(old.text===target)return code;
- // Only accept the known predecessor. Any independent online change requires review.
- const expected=target
- .replace(' const modelId = contentModel(input, nodeIds), vocabulary = modelId === 52;\n // Vocabulary IDs are numeric: preserve indexable joins in the high-traffic classroom reads.\n','')
- .replace("clauses.push(vocabulary ? 'c.\"nodeId\" = ANY($' + values.length + ')' : 'CAST(c.\"nodeId\" AS text) = ANY($' + values.length + '::text[])');", () => "clauses.push('CAST(c.\"nodeId\" AS text) = ANY($' + values.length + '::text[])');")
- .replace(' const config = CONTENT_ADDONS[modelId]', ' const modelId = contentModel(input, nodeIds); const config = CONTENT_ADDONS[modelId]')
- .replace("clauses.push((vocabulary ? 'c.\"modelId\" = $' : 'CAST(c.\"modelId\" AS text) = $') + values.length);", () => "clauses.push('CAST(c.\"modelId\" AS text) = $' + values.length);")
- .replace("a ON a.\"company\" = $1 AND ' + (vocabulary ? 'a.\"id\" = c.\"itemId\"' : 'CAST(a.\"id\" AS text) = CAST(c.\"itemId\" AS text)')", () => "a ON a.\"company\" = $1 AND CAST(a.\"id\" AS text) = CAST(c.\"itemId\" AS text)'");
- if(old.text!==expected)throw Error('Online contentPage differs from the reviewed predecessor; inspect drift before release');
- const patched=code.slice(0,old.start)+target+code.slice(old.end);new Function(patched);return patched;
- }
- async function main() {
- const mode='prepare';
- if(process.argv.some(arg=>['--deploy','--rollback'].includes(arg)))throw Error('Preparation only: use the serialized operator release workflow to publish or roll back');
- const master=process.env.XIAOSHU_MASTER_KEY;if(!master)throw Error('Missing XIAOSHU_MASTER_KEY; no production changes made');
- const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main';
- const dir=process.env.XIAOSHU_WORD_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-11-07-word-classroom-release');
- fs.mkdirSync(dir,{recursive:true,mode:0o700});
- if(['manifest.json','original.json','candidate.json'].some(name=>fs.existsSync(join(dir,name))))throw Error('Prepared release artifacts already exist; choose a new release directory, never overwrite the rollback baseline');
- const hash=x=>createHash('sha256').update(x).digest('hex'),save=(name,data)=>fs.writeFileSync(join(dir,name),JSON.stringify(data,null,2),{mode:0o600,flag:'wx'});
- const parse=async(path,method='GET',data)=>{
- const response=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:data?JSON.stringify(data):undefined,signal:AbortSignal.timeout(30000)});
- const result=await response.json();if(!response.ok||result.error)throw Error('Release API HTTP '+response.status);return result;
- };
- if(mode==='prepare') {
- const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({name:'xiaoshu.app.gateway'}),limit:'2'}))).results;
- if(rows?.length!==1)throw Error('Ambiguous app gateway');
- const original=rows[0],candidate=patchWordClassroomRead(original.code);
- save('original.json',original);save('candidate.json',{code:candidate});
- const manifest={objectId:original.objectId,beforeHash:hash(original.code),afterHash:hash(candidate),functions:['contentPage'],preparedAt:new Date().toISOString()};
- save('manifest.json',manifest);console.log(JSON.stringify({mode,dir,...manifest}));
- }
- }
- if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url))await main();
|