refactor(GOV-009): 彻底切换为仅源码部署
This commit is contained in:
@@ -1,15 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { access, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const repoRoot = new URL('../../', import.meta.url);
|
||||
const processNames = ['api', 'scheduler', 'worker', 'web'];
|
||||
const sensitiveKeyPattern = /(PASSWORD|API_KEY|TOKEN|SECRET)$/;
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
async function readRepoFile(relativePath) {
|
||||
return readFile(new URL(relativePath, repoRoot), 'utf8');
|
||||
@@ -115,13 +110,21 @@ test('生产模板禁止模拟数据并保持 MySQL 5.7 与 RocketMQ 配置契
|
||||
}
|
||||
});
|
||||
|
||||
test('本地 Compose 仅启动 Redis 并提供健康检查', async () => {
|
||||
const compose = await readRepoFile('deploy/compose/development.yml');
|
||||
|
||||
assert.match(compose, /services:\s*\n\s+redis:/);
|
||||
assert.match(compose, /healthcheck:/);
|
||||
assert.match(compose, /127\.0\.0\.1:/);
|
||||
assert.doesNotMatch(compose, /^\s{2}(mysql|rocketmq|api|worker|scheduler|web):/m);
|
||||
test('活动仓库不再包含 Docker 运行构建或部署入口', async () => {
|
||||
for (const relativePath of [
|
||||
'.dockerignore',
|
||||
'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',
|
||||
]) {
|
||||
await assert.rejects(access(new URL(relativePath, repoRoot)), { code: 'ENOENT' });
|
||||
}
|
||||
});
|
||||
|
||||
test('活动架构与部署文档只采用 RocketMQ 5.x', async () => {
|
||||
@@ -130,7 +133,6 @@ test('活动架构与部署文档只采用 RocketMQ 5.x', async () => {
|
||||
'docs/architecture/system-architecture.md',
|
||||
'docs/governance/code-development-standards.md',
|
||||
'docs/deployment/local-development.md',
|
||||
'docs/deployment/docker-compose-production.md',
|
||||
'docs/deployment/ubuntu-source-production.md',
|
||||
]) {
|
||||
const content = await readRepoFile(relativePath);
|
||||
@@ -153,73 +155,27 @@ test('生产源码部署使用独立 EnvironmentFile 并提供 Nginx 代理', as
|
||||
assert.match(nginx, /\/opt\/a-share-analysis\/current\/web\/dist/);
|
||||
});
|
||||
|
||||
test('部署文档覆盖本地、Compose 生产和 Ubuntu 源码生产', async () => {
|
||||
test('部署文档要求本机外部 Redis 和唯一 Ubuntu 源码生产路径', async () => {
|
||||
const local = await readRepoFile('docs/deployment/local-development.md');
|
||||
const compose = await readRepoFile('docs/deployment/docker-compose-production.md');
|
||||
const source = await readRepoFile('docs/deployment/ubuntu-source-production.md');
|
||||
|
||||
assert.match(local, /Redis/);
|
||||
assert.match(compose, /Docker Compose/);
|
||||
assert.match(local, /redis-cli -h 127\.0\.0\.1 -p 6379 ping/);
|
||||
assert.match(local, /外部服务|开发机/);
|
||||
assert.doesNotMatch(local, /docker compose/i);
|
||||
assert.match(source, /Ubuntu 22\.04 LTS/);
|
||||
assert.match(source, /Ubuntu 24\.04 LTS/);
|
||||
assert.match(source, /Node\.js 22 LTS/);
|
||||
assert.match(source, /pnpm 10/);
|
||||
assert.match(source, /Node\.js 22\.x/);
|
||||
assert.match(source, /pnpm 10\.25\.0/);
|
||||
assert.match(source, /pnpm verify:release/);
|
||||
assert.match(source, /\/opt\/a-share-analysis\/releases\//);
|
||||
assert.match(source, /\/opt\/a-share-analysis\/current/);
|
||||
assert.match(source, /不负责安装/);
|
||||
for (const document of [local, compose, source]) {
|
||||
for (const document of [local, source]) {
|
||||
assert.match(document, /MySQL 5\.7\.42/);
|
||||
}
|
||||
});
|
||||
|
||||
test('生产 Compose 只接受显式镜像并解析为真实健康检查', async () => {
|
||||
const composePath = new URL('deploy/compose/production.yml.example', repoRoot).pathname;
|
||||
const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'a-share-compose-'));
|
||||
const envFiles = {};
|
||||
for (const processName of processNames) {
|
||||
const envPath = path.join(temporaryDirectory, `${processName}.env`);
|
||||
await writeFile(envPath, 'APP_ENV=production\n');
|
||||
envFiles[processName] = envPath;
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
execFileAsync('docker', ['compose', '-f', composePath, 'config'], {
|
||||
env: { ...process.env },
|
||||
}),
|
||||
/必须提供不可变 (?:Web|Server) 镜像引用/,
|
||||
);
|
||||
|
||||
const imageDigest = `sha256:${'a'.repeat(64)}`;
|
||||
const { stdout } = await execFileAsync(
|
||||
'docker',
|
||||
['compose', '-f', composePath, 'config', '--format', 'json'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
A_SHARE_WEB_IMAGE: `registry.invalid/web@${imageDigest}`,
|
||||
A_SHARE_SERVER_IMAGE: `registry.invalid/server@${imageDigest}`,
|
||||
A_SHARE_WEB_ENV_FILE: envFiles.web,
|
||||
A_SHARE_API_ENV_FILE: envFiles.api,
|
||||
A_SHARE_SCHEDULER_ENV_FILE: envFiles.scheduler,
|
||||
A_SHARE_WORKER_ENV_FILE: envFiles.worker,
|
||||
},
|
||||
},
|
||||
);
|
||||
const config = JSON.parse(stdout);
|
||||
|
||||
assert.equal(config.services.web.image, `registry.invalid/web@${imageDigest}`);
|
||||
assert.equal(config.services.api.image, `registry.invalid/server@${imageDigest}`);
|
||||
assert.deepEqual(config.services.api.command, ['node', 'dist/apps/api/main.js']);
|
||||
assert.ok(config.services.web.healthcheck?.test);
|
||||
assert.ok(config.services.api.healthcheck?.test);
|
||||
});
|
||||
|
||||
test('容器构建文件存在且 systemd 启动入口对应真实构建产物', async () => {
|
||||
await access(new URL('web/Dockerfile', repoRoot));
|
||||
await access(new URL('web/nginx.conf', repoRoot));
|
||||
await access(new URL('server/Dockerfile', repoRoot));
|
||||
await execFileAsync('pnpm', ['--filter', '@a-share-analysis/server', 'build'], {
|
||||
cwd: repoRoot.pathname,
|
||||
});
|
||||
|
||||
test('systemd 启动入口对应源码构建产物', async () => {
|
||||
for (const processName of ['api', 'scheduler', 'worker']) {
|
||||
const unit = await readRepoFile(`deploy/systemd/a-share-${processName}.service.example`);
|
||||
const execStart = unit.match(
|
||||
@@ -230,20 +186,11 @@ test('容器构建文件存在且 systemd 启动入口对应真实构建产物',
|
||||
}
|
||||
});
|
||||
|
||||
test('Docker 构建上下文排除本地配置与派生产物', async () => {
|
||||
const dockerIgnore = await readRepoFile('.dockerignore');
|
||||
test('根级发布命令只保留源码发布门禁', async () => {
|
||||
const rootPackage = JSON.parse(await readRepoFile('package.json'));
|
||||
|
||||
for (const requiredPattern of [
|
||||
'deploy/env/**/*.local',
|
||||
'.git',
|
||||
'.gitnexus',
|
||||
'node_modules',
|
||||
'web/dist',
|
||||
'server/dist',
|
||||
]) {
|
||||
assert.ok(
|
||||
dockerIgnore.split(/\r?\n/).includes(requiredPattern),
|
||||
`.dockerignore 缺少 ${requiredPattern}`,
|
||||
);
|
||||
}
|
||||
assert.equal(rootPackage.scripts['verify:source'], 'node tools/deployment/verify-source-deployment.mjs');
|
||||
assert.equal(rootPackage.scripts['verify:release'], 'pnpm verify && pnpm verify:source');
|
||||
assert.equal(rootPackage.scripts['verify:docker'], undefined);
|
||||
assert.equal(rootPackage.scripts['check:images'], undefined);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user