chore(release): initialize separated production env
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 14s
Deploy / Deploy Staging (push) Successful in 2m18s
Deploy / Deploy Production (push) Has been skipped

This commit is contained in:
Xiaoxia AI
2026-06-21 14:24:23 +08:00
parent 1f89f8edfe
commit f22af7ee4c
7 changed files with 146 additions and 5 deletions
+6
View File
@@ -70,6 +70,12 @@ jobs:
run: |
bandit -r apps packages -q
- name: Validate release scripts syntax
run: |
bash -n scripts/backup_postgres.sh
bash -n scripts/restore_postgres_plan.sh
bash -n scripts/init_production_env.sh
- name: Validate Alembic migrations
run: |
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas \
+6
View File
@@ -70,6 +70,12 @@ jobs:
run: |
bandit -r apps packages -q
- name: Validate release scripts syntax
run: |
bash -n scripts/backup_postgres.sh
bash -n scripts/restore_postgres_plan.sh
bash -n scripts/init_production_env.sh
- name: Validate Alembic migrations
run: |
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas \
+10 -3
View File
@@ -12,7 +12,14 @@
## 2. 生产环境检查
- `/var/lib/xiaoxia-saas-production/.env` 存在且权限正确。
- `/var/lib/xiaoxia-saas-production/.env` 存在且权限正确。首次初始化必须使用非部署脚本生成独立 production env
```bash
scripts/init_production_env.sh
```
脚本只创建目录和 `.env`,不会启动容器;生成后必须人工替换 `CHANGE_ME_PRODUCTION_DB_PASSWORD`,并确认生产 DB/Redis 使用 `xiaoxia-postgres-production` / `xiaoxia-redis-production`,不得复用 staging 的 `xiaoxia-postgres` / `xiaoxia-redis`
- `APP_ENV=production`
- `DEBUG=false`
- `AUTO_CREATE_SCHEMA=false`
@@ -21,8 +28,8 @@
- `REDIS_URL` 指向生产 Redis。
- OSS 配置已确认或明确保持本地 fallback。
- `GENERATED_FILES_HOST_DIR` 生产环境不得指向 staging 目录。
- 本地校验文件:`python scripts/validate_release_env.py /var/lib/xiaoxia-saas-production/.env --strict-external`
- 容器内校验已注入环境:`docker exec xiaoxia-api-production python /app/scripts/validate_release_env.py --from-environ --strict-external`
- 本地校验文件:`python scripts/validate_release_env.py /var/lib/xiaoxia-saas-production/.env --strict-external`SMTP 仅在 `ENABLE_EMAIL_DELIVERY=true` 时强制)
- 容器内校验已注入环境:`docker exec xiaoxia-api-production python /app/scripts/validate_release_env.py --from-environ --strict-external`SMTP 仅在启用邮件时强制)
- 外部服务 smoke`docker exec xiaoxia-api-production python /app/scripts/smoke_external_services.py --strict --skip-smtp`SMTP 未启用时)。
- SMTP 真发信 smoke(启用邮件前必须执行):`docker exec xiaoxia-api-production python /app/scripts/smoke_external_services.py --strict --send-email-to <测试邮箱>`
- 如开启邮件/session
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
STAGING_ENV=${STAGING_ENV:-/var/lib/xiaoxia-saas-staging/.env}
PRODUCTION_DIR=${PRODUCTION_DIR:-/var/lib/xiaoxia-saas-production}
PRODUCTION_ENV=${PRODUCTION_ENV:-$PRODUCTION_DIR/.env}
GENERATED_DIR=${GENERATED_DIR:-$PRODUCTION_DIR/generated}
FORCE=${FORCE:-false}
if [ -e "$PRODUCTION_ENV" ] && [ "$FORCE" != "true" ]; then
echo "ERROR: production env already exists: $PRODUCTION_ENV" >&2
echo "Set FORCE=true only after manually reviewing the existing file." >&2
exit 1
fi
mkdir -p "$PRODUCTION_DIR" "$GENERATED_DIR"
chmod 700 "$PRODUCTION_DIR"
get_staging_value() {
key="$1"
if [ -f "$STAGING_ENV" ]; then
grep -E "^${key}=" "$STAGING_ENV" | tail -1 | cut -d= -f2- || true
fi
}
random_secret() {
python3 - <<'PY'
import secrets
print(secrets.token_urlsafe(48))
PY
}
OSS_ENDPOINT=${OSS_ENDPOINT:-$(get_staging_value OSS_ENDPOINT)}
OSS_BUCKET_NAME=${OSS_BUCKET_NAME:-$(get_staging_value OSS_BUCKET_NAME)}
OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID:-$(get_staging_value OSS_ACCESS_KEY_ID)}
OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET:-$(get_staging_value OSS_ACCESS_KEY_SECRET)}
JWT_SECRET_KEY=${JWT_SECRET_KEY:-$(random_secret)}
cat > "$PRODUCTION_ENV" <<EOF
APP_NAME=xiaoxia-saas
APP_VERSION=0.1.0
APP_ENV=production
ENVIRONMENT=production
DEBUG=false
AUTO_CREATE_SCHEMA=false
USE_IN_MEMORY_DB=false
ENV=production
API_PORT=8001
WEB_PORT=3000
PUBLIC_API_BASE_URL=https://api.xiaoxiajianji.com
GENERATED_FILES_HOST_DIR=$GENERATED_DIR
DATABASE_URL=postgresql+psycopg://xiaoxia:CHANGE_ME_PRODUCTION_DB_PASSWORD@xiaoxia-postgres-production:5432/xiaoxia_saas
REDIS_URL=redis://xiaoxia-redis-production:6379/0
CELERY_BROKER_URL=redis://xiaoxia-redis-production:6379/0
CELERY_RESULT_BACKEND=redis://xiaoxia-redis-production:6379/1
ENABLE_REDIS_SESSIONS=true
JWT_SECRET_KEY=$JWT_SECRET_KEY
ENABLE_EMAIL_DELIVERY=false
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
SMTP_FROM_NAME=小虾 SaaS
SMTP_USE_TLS=true
OSS_ENDPOINT=$OSS_ENDPOINT
OSS_ACCESS_KEY_ID=$OSS_ACCESS_KEY_ID
OSS_ACCESS_KEY_SECRET=$OSS_ACCESS_KEY_SECRET
OSS_BUCKET_NAME=$OSS_BUCKET_NAME
LOG_LEVEL=INFO
CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://api.xiaoxiajianji.com
EOF
chmod 600 "$PRODUCTION_ENV"
echo "OK production env initialized: $PRODUCTION_ENV"
echo "NEXT: replace CHANGE_ME_PRODUCTION_DB_PASSWORD and provision xiaoxia-postgres-production/xiaoxia-redis-production before deployment."
+2 -2
View File
@@ -83,7 +83,7 @@ def validate(values: dict[str, str], strict_external: bool) -> list[str]:
errors.append("DEBUG must be false in production")
external_requirements = []
if strict_external or is_enabled(values.get("ENABLE_EMAIL_DELIVERY")):
if is_enabled(values.get("ENABLE_EMAIL_DELIVERY")):
external_requirements.extend(REQUIRED_SMTP)
if strict_external:
external_requirements.extend(REQUIRED_OSS)
@@ -117,7 +117,7 @@ def main() -> int:
parser.add_argument(
"--strict-external",
action="store_true",
help="Require SMTP, Redis sessions, and OSS credentials for production readiness.",
help="Require Redis sessions and OSS credentials for production readiness. SMTP is required only when ENABLE_EMAIL_DELIVERY=true.",
)
args = parser.parse_args()
+13
View File
@@ -10,6 +10,19 @@ def test_restore_postgres_plan_is_non_destructive():
assert "pg_restore" not in executable_prefix
def test_init_production_env_is_non_deploying_and_separated():
script = Path("scripts/init_production_env.sh").read_text(encoding="utf-8")
assert "APP_ENV=production" in script
assert "ENVIRONMENT=production" in script
assert "xiaoxia-postgres-production" in script
assert "xiaoxia-redis-production" in script
assert "GENERATED_FILES_HOST_DIR=$GENERATED_DIR" in script
assert "ENABLE_EMAIL_DELIVERY=false" in script
assert "docker compose" not in script
assert "docker run" not in script
def test_backup_postgres_writes_manifest_and_version():
script = Path("scripts/backup_postgres.sh").read_text(encoding="utf-8")
+26
View File
@@ -35,6 +35,32 @@ def test_validate_release_env_accepts_strict_production(tmp_path: Path):
assert validate(parse_env_file(env_file), strict_external=True) == []
def test_validate_release_env_allows_disabled_smtp_with_strict_external(tmp_path: Path):
env_file = tmp_path / ".env.production"
env_file.write_text(
"\n".join(
[
"APP_ENV=production",
"DATABASE_URL=postgresql+psycopg://user:pass@db:5432/app",
"JWT_SECRET_KEY=abcdefghijklmnopqrstuvwxyz123456",
"REDIS_URL=redis://redis:6379/0",
"ENABLE_EMAIL_DELIVERY=false",
"ENABLE_REDIS_SESSIONS=true",
"OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com",
"OSS_ACCESS_KEY_ID=ak-real",
"OSS_ACCESS_KEY_SECRET=sk-real-secret",
"OSS_BUCKET_NAME=xiaoxia-prod",
"GENERATED_FILES_HOST_DIR=/var/lib/xiaoxia-saas-production/generated",
"DEBUG=false",
"AUTO_CREATE_SCHEMA=false",
]
),
encoding="utf-8",
)
assert validate(parse_env_file(env_file), strict_external=True) == []
def test_validate_release_env_rejects_placeholders_and_staging_paths(tmp_path: Path):
env_file = tmp_path / ".env.production"
env_file.write_text(