feat: complete V1 phases 0-7

This commit is contained in:
2026-08-12 14:35:12 +08:00
commit c3dd9be674
311 changed files with 38750 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# 发布与运维
生产发布只允许不可变镜像标签,顺序固定为:staging 预检 → migration 预检与备份 → 冒烟测试 → UAT 签字 → 生产备份 → 部署 → `/health/live`、`/health/ready` 和公开内容冒烟 → 观察 → 必要时回滚到上一镜像标签。禁止使用 `latest`。
## 外部准备
- 私有镜像仓库、TLS 证书和合法域名;生产 MySQL、Redis、私有对象存储。
- 微信 AppID/AppSecret、六类订阅消息模板、合法 HTTPS 域名和真机授权。
- 告警接收渠道、异地备份目标、隐私政策/用户协议/主体资质与 UAT/发布审批人。
## 执行
1. 设置 `IMAGE_TAG`、`REGISTRY_IMAGE` 及 `.env.example` 中所有生产必填环境变量。
2. `./infra/deploy/preflight.ps1 staging`,部署 staging,执行 migration 和 `smoke.ps1`。
3. 完成 `docs/14_RELEASE_CHECKLIST.md` 中 staging/UAT 项并留存证据。
4. 使用 `infra/backup/backup-mysql.ps1` 备份并将 SQL、SHA-256 文件同步到异地加密存储。
5. `./infra/deploy/preflight.ps1 production`,部署固定镜像;执行冒烟并观察 5xx、p95、数据库、磁盘、通知死信和备份告警。
6. 异常时将 `IMAGE_TAG` 恢复到上一批准版本重新部署。若 migration 不向后兼容,停止发布并按已验证的数据库恢复步骤执行。
备份默认每日一次,保留 30 天;必须每季度在独立数据库做恢复演练并记录表数量、关键行数和耗时。脚本会校验 SHA-256,恢复目标数据库名必须显式提供。
+13
View File
@@ -0,0 +1,13 @@
services:
api:
image: ${REGISTRY_IMAGE:?REGISTRY_IMAGE required}:${IMAGE_TAG:?IMAGE_TAG required}
restart: always
environment:
APP_ENV: production
ADMIN_COOKIE_SECURE: 'true'
NOTIFICATION_WORKER_ENABLED: 'true'
read_only: true
tmpfs:
- /tmp:size=64m,noexec,nosuid
security_opt:
- no-new-privileges:true
+7
View File
@@ -0,0 +1,7 @@
services:
api:
image: ${REGISTRY_IMAGE:?REGISTRY_IMAGE required}:${IMAGE_TAG:?IMAGE_TAG required}
restart: unless-stopped
environment:
APP_ENV: staging
ADMIN_COOKIE_SECURE: 'true'
+9
View File
@@ -0,0 +1,9 @@
param([Parameter(Mandatory=$true)][ValidateSet('staging','production')][string]$Environment)
$ErrorActionPreference='Stop'
$required=@('IMAGE_TAG','DATABASE_URL','SESSION_TOKEN_PEPPER','AUDIT_IP_HASH_KEY','FIELD_ENCRYPTION_KEY','ADMIN_WEB_ORIGIN')
foreach($name in $required){if(-not [Environment]::GetEnvironmentVariable($name)){throw "Missing required environment variable: $name"}}
if($env:IMAGE_TAG -eq 'latest'){throw 'IMAGE_TAG must be immutable and cannot be latest'}
if($Environment -eq 'production' -and $env:ADMIN_COOKIE_SECURE -ne 'true'){throw 'production requires ADMIN_COOKIE_SECURE=true'}
docker compose -f compose.yaml -f "infra/deploy/compose.$Environment.yaml" config --quiet
if($LASTEXITCODE -ne 0){throw 'compose validation failed'}
Write-Output "Preflight passed for $Environment"
+6
View File
@@ -0,0 +1,6 @@
param([Parameter(Mandatory=$true)][string]$BaseUrl)
$ErrorActionPreference='Stop';$base=$BaseUrl.TrimEnd('/');$live=Invoke-RestMethod -Uri "$base/health/live" -TimeoutSec 10;$ready=Invoke-RestMethod -Uri "$base/health/ready" -TimeoutSec 10
if($live.status -ne 'ok' -or $ready.status -ne 'ready'){throw 'health smoke test failed'}
$content=Invoke-RestMethod -Uri "$base/api/v1/content?page=1&pageSize=1" -TimeoutSec 10
if($content.success -ne $true){throw 'public content smoke test failed'}
Write-Output 'Smoke tests passed'