Files
a-share-analysis/tools/deployment/verify-source-deployment.mjs
T

141 lines
4.6 KiB
JavaScript

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');
}