refactor(GOV-009): 彻底切换为仅源码部署
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const defaultRepositoryRoot = path.resolve(import.meta.dirname, '../..');
|
||||
|
||||
export const FORBIDDEN_DOCKER_PATHS = Object.freeze([
|
||||
'.dockerignore',
|
||||
'Dockerfile',
|
||||
'web/Dockerfile',
|
||||
'web/nginx.conf',
|
||||
'server/Dockerfile',
|
||||
'deploy/compose/development.yml',
|
||||
'deploy/compose/production.yml.example',
|
||||
'docs/deployment/docker-compose-production.md',
|
||||
'tools/deployment/verify-docker.mjs',
|
||||
'tools/deployment/verify-image-references.mjs',
|
||||
'tools/deployment/verify-image-references.test.mjs',
|
||||
]);
|
||||
|
||||
const buildOutputs = Object.freeze([
|
||||
['Web', 'web/dist/index.html'],
|
||||
['API', 'server/dist/apps/api/main.js'],
|
||||
['Scheduler', 'server/dist/apps/scheduler/main.js'],
|
||||
['Worker', 'server/dist/apps/worker/main.js'],
|
||||
]);
|
||||
|
||||
const serverProcesses = Object.freeze([
|
||||
['api', 'API'],
|
||||
['scheduler', 'Scheduler'],
|
||||
['worker', 'Worker'],
|
||||
]);
|
||||
|
||||
function resolveRoot(repositoryRoot) {
|
||||
return repositoryRoot instanceof URL
|
||||
? fileURLToPath(repositoryRoot)
|
||||
: path.resolve(repositoryRoot);
|
||||
}
|
||||
|
||||
async function pathExists(absolutePath) {
|
||||
try {
|
||||
await access(absolutePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertDockerArtifactsAbsent(repositoryRoot) {
|
||||
for (const relativePath of FORBIDDEN_DOCKER_PATHS) {
|
||||
if (await pathExists(path.join(repositoryRoot, relativePath))) {
|
||||
throw new Error(`禁止存在 Docker 活动文件:${relativePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function assertBuildOutputsPresent(repositoryRoot) {
|
||||
for (const [displayName, relativePath] of buildOutputs) {
|
||||
if (!(await pathExists(path.join(repositoryRoot, relativePath)))) {
|
||||
throw new Error(`${displayName} 构建入口不存在:${relativePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertUnitValue(unit, displayName, key, expectedValue) {
|
||||
const pattern = new RegExp(
|
||||
`^${key}=${expectedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
|
||||
'm',
|
||||
);
|
||||
if (!pattern.test(unit)) {
|
||||
throw new Error(`${displayName} systemd 必须使用 ${key}=${expectedValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertSystemdUnits(repositoryRoot) {
|
||||
for (const [processName, displayName] of serverProcesses) {
|
||||
const relativePath = `deploy/systemd/a-share-${processName}.service.example`;
|
||||
let unit;
|
||||
try {
|
||||
unit = await readFile(path.join(repositoryRoot, relativePath), 'utf8');
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT')
|
||||
throw new Error(`${displayName} systemd 配置不存在:${relativePath}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const environmentFile = `/etc/a-share-analysis/${processName}.env`;
|
||||
assertUnitValue(unit, displayName, 'User', 'a-share');
|
||||
assertUnitValue(unit, displayName, 'Group', 'a-share');
|
||||
assertUnitValue(unit, displayName, 'WorkingDirectory', '/opt/a-share-analysis/current');
|
||||
assertUnitValue(unit, displayName, 'EnvironmentFile', environmentFile);
|
||||
assertUnitValue(unit, displayName, 'ExecStartPre', `/usr/bin/test -r ${environmentFile}`);
|
||||
assertUnitValue(
|
||||
unit,
|
||||
displayName,
|
||||
'ExecStart',
|
||||
`/usr/bin/node /opt/a-share-analysis/current/server/dist/apps/${processName}/main.js`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertNginxConfig(repositoryRoot) {
|
||||
const relativePath = 'deploy/nginx/a-share-analysis.conf.example';
|
||||
let nginx;
|
||||
try {
|
||||
nginx = await readFile(path.join(repositoryRoot, relativePath), 'utf8');
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') throw new Error(`Nginx 配置不存在:${relativePath}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const [description, pattern] of [
|
||||
['Web dist 根目录', /\broot\s+\/opt\/a-share-analysis\/current\/web\/dist\s*;/],
|
||||
['API 代理 location', /location\s+\/api\/\s*\{/],
|
||||
['API 代理目标', /proxy_pass\s+http:\/\/127\.0\.0\.1:3000\s*;/],
|
||||
]) {
|
||||
if (!pattern.test(nginx)) throw new Error(`Nginx 缺少${description}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 源码发布门禁只验证应用产物和进程配置,不连接或改变任何外部基础设施。
|
||||
*/
|
||||
export async function verifySourceDeployment(repositoryRoot = defaultRepositoryRoot) {
|
||||
const resolvedRoot = resolveRoot(repositoryRoot);
|
||||
await assertDockerArtifactsAbsent(resolvedRoot);
|
||||
await assertBuildOutputsPresent(resolvedRoot);
|
||||
await assertSystemdUnits(resolvedRoot);
|
||||
await assertNginxConfig(resolvedRoot);
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))
|
||||
) {
|
||||
await verifySourceDeployment();
|
||||
process.stdout.write('源码发布产物与 systemd/Nginx 配置验证通过\n');
|
||||
}
|
||||
Reference in New Issue
Block a user