| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 |
- #!/usr/bin/env node
- // Prepare and verify static releases without reading or uploading server credentials.
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import { createHash } from 'node:crypto';
- import { execFileSync } from 'node:child_process';
- const repo=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
- const sha=data=>createHash('sha256').update(data).digest('hex');
- const run=(cmd,args,options={})=>execFileSync(cmd,args,{cwd:repo,stdio:'inherit',...options});
- export async function manifestFor(directory,releaseId,commit){
- const files=[];
- async function walk(relative=''){
- for(const entry of await fs.readdir(path.join(directory,relative),{withFileTypes:true})){
- const name=path.posix.join(relative,entry.name);
- if(entry.isSymbolicLink()||/[\r\n]/.test(name)||/(^|\/)\.(env|git)|\.(pem|key)$/i.test(name))throw Error('发布包包含不允许的文件: '+name);
- if(entry.isDirectory())await walk(name);
- else if(entry.isFile()&&name!=='deployment-version.json')files.push({path:name,sha256:sha(await fs.readFile(path.join(directory,name)))});
- }
- }
- await walk();
- if(!files.some(f=>f.path==='index.html')||!files.some(f=>/^main-.*\.js$/.test(f.path)))throw Error('缺少管理端生产入口或主脚本');
- return{application:'xiaoshu-admin',releaseId,commit,builtAt:new Date().toISOString(),files:files.sort((a,b)=>a.path.localeCompare(b.path))};
- }
- async function response(url){const result=await fetch(url,{signal:AbortSignal.timeout(30000),headers:{'Cache-Control':'no-cache'}});if(!result.ok)throw Error('HTTP '+result.status+' '+url);return result;}
- export async function verifyRelease(base,manifest){
- const url=new URL(base);if(url.protocol!=='https:'&&!['localhost','127.0.0.1'].includes(url.hostname))throw Error('公网发布地址必须使用 HTTPS');
- const live=await(await response(new URL('/deployment-version.json?release='+manifest.releaseId,url))).json();
- if(live.releaseId!==manifest.releaseId||live.commit!==manifest.commit)throw Error('公网版本与本次发布不一致');
- for(let i=0;i<manifest.files.length;i+=6)await Promise.all(manifest.files.slice(i,i+6).map(async file=>{
- const remote=new URL('/'+file.path,url);remote.searchParams.set('release',manifest.releaseId);
- if(sha(Buffer.from(await(await response(remote)).arrayBuffer()))!==file.sha256)throw Error('公网文件校验失败: '+file.path);
- }));
- const expected=manifest.files.find(f=>f.path==='index.html').sha256;
- for(const route of ['/admin/login','/admin/payroll']){
- const page=await response(new URL(route+'?release='+manifest.releaseId,url));
- if(!page.headers.get('content-type')?.includes('text/html')||sha(Buffer.from(await page.arrayBuffer()))!==expected)throw Error('Angular 深层路由未返回新入口: '+route);
- }
- return{releaseId:manifest.releaseId,commit:manifest.commit,filesVerified:manifest.files.length,url:url.origin};
- }
- async function main(){
- const mode=process.argv[2]||'--prepare';
- if(!['--prepare','--verify'].includes(mode))throw Error('使用 --prepare 或 --verify;上传前需另行配置现有站点的部署入口');
- const base=process.env.XIAOSHU_ADMIN_URL||'https://admin.xiaoshu.pro';
- const output=path.join(repo,'dist/admin-release'),bundle=path.join(output,'site');
- if(mode==='--verify'){const manifest=JSON.parse(await fs.readFile(path.join(bundle,'deployment-version.json'),'utf8'));console.log(JSON.stringify(await verifyRelease(base,manifest)));return;}
- const commit=run('git',['rev-parse','HEAD'],{encoding:'utf8',stdio:'pipe'}).trim();
- if(run('git',['status','--porcelain','--untracked-files=no'],{encoding:'utf8',stdio:'pipe'}).trim())throw Error('请先提交已跟踪文件的修改,再构建可追踪的发布版本');
- run('npm',['run','build:admin']);
- await fs.rm(output,{recursive:true,force:true});await fs.mkdir(output,{recursive:true});
- await fs.cp(path.join(repo,'dist/xiaoshu-admin/browser'),bundle,{recursive:true});
- const releaseId=commit.slice(0,12)+'-'+new Date().toISOString().replace(/[-:.]/g,'');
- const manifest=await manifestFor(bundle,releaseId,commit);
- await fs.writeFile(path.join(bundle,'deployment-version.json'),JSON.stringify(manifest,null,2));
- const archive=path.join(output,'xiaoshu-admin-'+releaseId+'.tar.gz');
- run('tar',['-czf',archive,'-C',bundle,'.']);
- await fs.writeFile(path.join(output,'SHA256SUMS'),sha(await fs.readFile(archive))+' '+path.basename(archive)+'\n');
- console.log(JSON.stringify({prepared:true,releaseId,archive,files:manifest.files.length}));
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url))main().catch(error=>{console.error(error.message);process.exitCode=1;});
|