Commit 0165bc11 by luoqi

fix(deploy): 机器形态自检 —— 该托管却漏 COMPOSE_MANAGED=1 直接退出

## 事故(2026-07-26,生产 friday / 47.99.62.30)

在生产机漏了 `COMPOSE_MANAGED=1` 跑 deploy-prod.sh。不叠加 managed override 时,
docker-compose.prod.yml 把 DATABASE_URL 硬编码成 `postgres:5432`(容器内自带 PG),于是:

  22:35:33  起了 pac-postgres-1 / pac-redis-1 两个空容器(volume 同刻创建)
  22:35~36  pac-migrate 对着这个**全新空库**跑完 29 条迁移,建出整套 schema
            ← 在这里被打断
  22:38:29  正确那次(COMPOSE_MANAGED=1)把两条新迁移写进托管库

托管生产库**未被写坏**,逐项核对:patients 425,874 / facts 25,022,720 /
在池 236,553 / assigned 36 / personas 409,368 / plan_executions 7 —— 与事故前一致
(patients、facts 的 +2/+206 是正常增量摄入)。空库那侧唯一有行的表是 _prisma_migrations(29 行)。

## 真正的后遗症(已清理)

机器上留下一个「schema 齐全的空库」。危害不在占资源,在于:
**下次再漏 COMPOSE_MANAGED,service 会顺利连上它、health 200、脚本三项验证全绿,
生产静默服务空库且零告警。** 原本"空 PG 没 schema 会崩"这张天然安全网就此失效。

已在生产执行:docker rm -f pac-postgres-1 pac-redis-1
              docker volume rm pac_postgres_data pac_redis_data
清理前核过:两 volume 无其他容器挂载 / pac-service 连的是 RDS+托管 Redis /
pac-web 无 DB 环境变量 / 本地库业务表全 0 / 全机再无其他 volume。清理后 health+web+私网 200。

## 防线

不依赖人记得加环境变量,而是问机器自己 —— 读 `.env` 的 DATABASE_URL 指向哪儿:
  · postgres / localhost / 127.0.0.1 → 自带 DB 形态,放行(测试机)
  · 其他主机(RDS 等)               → 托管形态,没设 COMPOSE_MANAGED=1 就 die

闸放在 main() 最前,早于任何 git/docker 动作 —— 拦下时机器状态零变更。
报错只打主机名不打整串 URL(不泄露 .env 口令)。

## 验证

- 新增 tests/deploy-managed-guard.spec.ts 5 例,真起 bash 跑脚本:
  托管+漏设→退出且给出正确命令 / 报错不含口令 / 托管+设了→放行 /
  自带 DB→放行(不误伤测试机) / localhost 与 127.0.0.1 同样放行
- 482 单测通过(35 suites),service tsc 干净

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 13733519
Pipeline #3445 failed in 0 seconds
import { execFileSync } from 'node:child_process';
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
/**
* deploy-prod.sh 的「机器形态自检」回归 —— 锁住 2026-07-26 生产事故的防线。
*
* 事故:在 friday 生产机漏了 `COMPOSE_MANAGED=1` 跑 deploy-prod.sh。不叠加 managed override 时,
* docker-compose.prod.yml 把 DATABASE_URL 硬编码成 `postgres:5432`(容器内自带 PG),于是:
* · 当场起了 pac-postgres-1 / pac-redis-1 两个空容器
* · 29 条迁移建到那个**空库**上(托管库本身没被写坏,已逐项核对数据完好)
* 真正的后遗症是机器上从此留了个「schema 齐全的空库」—— 下次再漏 COMPOSE_MANAGED,
* service 会顺利连上它、health 200、脚本三项验证全绿,**生产静默服务空库且零告警**。
* 原本"空 PG 起不来会崩"这张天然安全网就此失效。
*
* 防线口径:不依赖人记得加环境变量,而是问机器自己 —— 读 `.env` 的 DATABASE_URL 指向哪儿。
* 指向 postgres/localhost/127.0.0.1 → 自带 DB 形态,放行
* 指向别的主机(RDS 等) → 托管形态,必须 COMPOSE_MANAGED=1,否则 die
*/
const SCRIPT = resolve(__dirname, '../../../deploy/deploy-prod.sh');
/** 造一棵最小目录树(deploy/ + 两个 .env),跑脚本并回收 stdout+stderr。 */
function runGuard(dbUrl: string, env: NodeJS.ProcessEnv): { code: number; out: string } {
const dir = mkdtempSync(join(tmpdir(), 'pac-deploy-guard-'));
try {
mkdirSync(join(dir, 'deploy'), { recursive: true });
mkdirSync(join(dir, 'apps/pac-service'), { recursive: true });
mkdirSync(join(dir, 'apps/pac-web'), { recursive: true });
cpSync(SCRIPT, join(dir, 'deploy/deploy-prod.sh'));
writeFileSync(join(dir, 'apps/pac-service/.env'), `DATABASE_URL="${dbUrl}"\n`);
writeFileSync(join(dir, 'apps/pac-web/.env'), '');
try {
const out = execFileSync('bash', ['deploy/deploy-prod.sh', '--no-pull'], {
cwd: dir,
// git/docker 在临时目录里必然失败 —— 无所谓,闸在它们之前,只看闸放不放行
env: { ...process.env, ...env },
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 30_000,
});
return { code: 0, out };
} catch (e) {
const err = e as { status?: number; stdout?: string; stderr?: string };
return { code: err.status ?? -1, out: `${err.stdout ?? ''}${err.stderr ?? ''}` };
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
const RDS = 'postgresql://pac:s3cr3t@fridaypac.pg.rds.aliyuncs.com:5432/pac?schema=public';
const BUNDLED = 'postgresql://pac:pac@postgres:5432/pac?schema=public';
describe('deploy-prod.sh 机器形态自检(COMPOSE_MANAGED 防线)', () => {
test('⭐ 托管 DB + 漏设 COMPOSE_MANAGED → 必须退出,且给出正确命令', () => {
const { code, out } = runGuard(RDS, { COMPOSE_MANAGED: '' });
expect(code).not.toBe(0);
expect(out).toContain('托管 DB 形态');
expect(out).toContain('COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh');
});
test('⭐ 报错信息只暴露主机名,不能把 .env 里的口令打出来', () => {
const { out } = runGuard(RDS, { COMPOSE_MANAGED: '' });
expect(out).toContain('fridaypac.pg.rds.aliyuncs.com');
expect(out).not.toContain('s3cr3t');
});
test('⭐ 托管 DB + COMPOSE_MANAGED=1 → 放行(闸不拦,后面失败是因为临时目录没 git/docker)', () => {
const { out } = runGuard(RDS, { COMPOSE_MANAGED: '1' });
expect(out).not.toContain('托管 DB 形态');
});
test('自带 DB(postgres:5432)+ 不设 COMPOSE_MANAGED → 放行(测试机形态,不能误伤)', () => {
const { out } = runGuard(BUNDLED, { COMPOSE_MANAGED: '' });
expect(out).not.toContain('托管 DB 形态');
});
test('localhost / 127.0.0.1 同样按自带 DB 放行', () => {
for (const host of ['localhost', '127.0.0.1']) {
const { out } = runGuard(`postgresql://pac:pac@${host}:5432/pac`, { COMPOSE_MANAGED: '' });
expect(out).not.toContain('托管 DB 形态');
}
});
});
......@@ -26,6 +26,33 @@ die() { printf '\033[1;31mFAIL: %s\033[0m\n' "$*" >&2; exit 1; }
main() {
cd "$(dirname "$0")/.."
# ⭐ 机器形态自检(2026-07-26 事故防线)——「该托管却没带 COMPOSE_MANAGED=1」直接退出。
#
# 事故经过:在 friday 生产机漏了 COMPOSE_MANAGED=1 跑本脚本 → 不叠加 managed override →
# docker-compose.prod.yml 里 DATABASE_URL 硬编码成 postgres:5432(容器内自带 PG),
# 于是当场起了 pac-postgres-1 / pac-redis-1 两个空容器,并把 29 条迁移建到那个**空库**上。
# 托管库本身没被写坏,但机器上从此留下一个「schema 齐全的空库」——
# 下次再漏 COMPOSE_MANAGED,service 会顺利连上它、health 200、三项验证全绿,
# **生产静默服务空库且无任何告警**。原本"空 PG 起不来会崩"的天然安全网就此失效。
#
# 判据不依赖人记得加环境变量,而是问机器自己:.env 的 DATABASE_URL 指向哪儿。
# 指向 postgres:5432 / localhost → 自带 DB 形态,COMPOSE_MANAGED 可不设
# 指向别的主机(RDS 等) → 托管形态,必须 COMPOSE_MANAGED=1,否则退出
local SVC_ENV="apps/pac-service/.env"
if [[ -f "$SVC_ENV" ]]; then
local dburl
dburl=$(grep -E '^\s*DATABASE_URL=' "$SVC_ENV" | tail -1 | cut -d= -f2- | tr -d '"'"'"' ')
if [[ -n "$dburl" && "$dburl" != *"@postgres:"* && "$dburl" != *"@localhost:"* && "$dburl" != *"@127.0.0.1:"* ]]; then
# 托管形态。注意只打印主机名,不打印含口令的整串。
local dbhost="${dburl#*@}"; dbhost="${dbhost%%/*}"
[[ "${COMPOSE_MANAGED:-0}" == "1" ]] || die \
"本机是【托管 DB 形态】(.env 的 DATABASE_URL 指向 $dbhost),但没设 COMPOSE_MANAGED=1。
这样跑会起自带 PG/Redis 容器、把迁移建到空库,并跳过私网可达性验证。
正确命令: COMPOSE_MANAGED=1 bash deploy/deploy-prod.sh
(若确实想用自带 DB,请改 .env 的 DATABASE_URL 指向 postgres:5432)"
fi
fi
# 托管 DB/Redis:COMPOSE_MANAGED=1 叠加 override(不启自带 PG/Redis,URL 走 .env 的托管端点)
local COMPOSE=(docker compose -f docker-compose.prod.yml)
[[ "${COMPOSE_MANAGED:-0}" == "1" ]] && COMPOSE+=(-f docker-compose.managed.yml)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment