141 lines
4.6 KiB
JavaScript
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');
|
|
}
|