release: freeze V2 production candidate
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 6m47s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 42m10s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m59s

This commit is contained in:
2026-07-20 18:39:27 +08:00
parent 57fc8d350b
commit 7bc74f06f2
66 changed files with 5633 additions and 279 deletions
+5
View File
@@ -34,6 +34,11 @@ CONTRACT_API_READ_CACHE_TTL_SECONDS=2
CONTRACT_API_REVIEW_WORKER_NAME=contract-review-worker-1
CONTRACT_API_REVIEW_WORKER_POLL_INTERVAL_SECONDS=5
CONTRACT_API_REVIEW_WORKER_BATCH_LIMIT=5
CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE=development
CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED=true
CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED=false
CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL=http://localhost:3000
CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET=local-only-identity-rate-secret-change-me
CONTRACT_REVIEW_WORKER_SEED_DEMO_DATA=false
CONTRACT_WEB_SESSION_SECRET=local-only-session-secret-change-me-32
CONTRACT_WEB_SESSION_SECURE=false
+65 -3
View File
@@ -1,9 +1,48 @@
# Copy to .env.production and replace every CHANGE_ME value before deployment.
COMPOSE_PROJECT_NAME=yuqei-v2-production
RELEASE_ID=CHANGE_ME_IMMUTABLE_RC_TAG
CONTRACT_API_IMAGE=registry.example.com/yuqei/contract-api@sha256:CHANGE_ME_64_HEX_DIGEST
AI_PLATFORM_API_IMAGE=registry.example.com/yuqei/ai-platform-api@sha256:CHANGE_ME_64_HEX_DIGEST
AI_WORKER_IMAGE=registry.example.com/yuqei/ai-worker@sha256:CHANGE_ME_64_HEX_DIGEST
POSTGRES_IMAGE=pgvector/pgvector@sha256:CHANGE_ME_64_HEX_DIGEST
POSTGRES_CLIENT_IMAGE=postgres@sha256:CHANGE_ME_64_HEX_DIGEST
REDIS_IMAGE=redis@sha256:CHANGE_ME_64_HEX_DIGEST
MINIO_IMAGE=minio/minio@sha256:CHANGE_ME_64_HEX_DIGEST
MINIO_MC_IMAGE=minio/mc@sha256:CHANGE_ME_64_HEX_DIGEST
NGINX_IMAGE=nginx@sha256:CHANGE_ME_64_HEX_DIGEST
BACKUP_UTILITY_IMAGE=alpine@sha256:CHANGE_ME_64_HEX_DIGEST
BACKEND_SERVER_NAME=api.contracts.example.com
BACKEND_PUBLIC_BASE_URL=https://api.contracts.example.com
BACKEND_TLS_CERT_DIR=/etc/yuqei/tls
BACKEND_HTTP_BIND_ADDRESS=0.0.0.0
BACKEND_HTTP_PORT=80
BACKEND_HTTPS_BIND_ADDRESS=0.0.0.0
BACKEND_HTTPS_PORT=443
POSTGRES_BIND_ADDRESS=127.0.0.1
REDIS_BIND_ADDRESS=127.0.0.1
MINIO_API_BIND_ADDRESS=127.0.0.1
MINIO_CONSOLE_BIND_ADDRESS=127.0.0.1
CONTRACT_API_BIND_ADDRESS=127.0.0.1
AI_PLATFORM_API_BIND_ADDRESS=127.0.0.1
CONTRACT_WEB_BIND_ADDRESS=127.0.0.1
POSTGRES_VOLUME_NAME=yuqei-v2-production-postgres
MINIO_VOLUME_NAME=yuqei-v2-production-minio
CONTRACT_FILES_VOLUME_NAME=yuqei-v2-production-contract-files
BACKUP_HOST_DIR=/mnt/yuqei-v2-backups
BACKUP_REQUIRE_SEPARATE_MOUNT=true
BACKUP_MAX_AGE_HOURS=30
# The file is mounted as /run/secrets/ai_provider_api_key. Configure the default
# database provider with api_key_secret_ref=file:///run/secrets/ai_provider_api_key.
AI_PROVIDER_API_KEY_FILE=/etc/yuqei/secrets/ai-provider-api-key
POSTGRES_USER=CHANGE_ME
POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_DB=postgres
POSTGRES_IMAGE=pgvector/pgvector:pg16
PYPI_INDEX_URL=https://pypi.org/simple
NPM_REGISTRY=https://registry.npmjs.org
CONTRACT_POSTGRES_DB=yuqei_contract
@@ -16,8 +55,6 @@ MINIO_CONSOLE_HOST_PORT=9001
CONTRACT_API_HOST_PORT=8100
AI_PLATFORM_API_HOST_PORT=8101
CONTRACT_WEB_HOST_PORT=3000
MINIO_IMAGE=minio/minio:RELEASE.2023-06-23T20-26-00Z
MINIO_MC_IMAGE=minio/mc:latest
MINIO_ROOT_USER=CHANGE_ME
MINIO_ROOT_PASSWORD=CHANGE_ME
AI_PLATFORM_OBJECT_STORAGE_BUCKET=ai-platform-uploads
@@ -36,6 +73,31 @@ CONTRACT_API_READ_CACHE_TTL_SECONDS=2
CONTRACT_API_REVIEW_WORKER_NAME=contract-review-worker-1
CONTRACT_API_REVIEW_WORKER_POLL_INTERVAL_SECONDS=5
CONTRACT_API_REVIEW_WORKER_BATCH_LIMIT=5
CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE=disabled
CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED=false
CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED=true
# SMTP settings are retained for a future switch to CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE=smtp.
CONTRACT_API_IDENTITY_SMTP_HOST=smtp.example.com
CONTRACT_API_IDENTITY_SMTP_PORT=587
CONTRACT_API_IDENTITY_SMTP_USERNAME=mailer@example.com
CONTRACT_API_IDENTITY_SMTP_PASSWORD=CHANGE_ME_LOAD_FROM_PROTECTED_ENV
CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL=mailer@example.com
CONTRACT_API_IDENTITY_SMTP_FROM_NAME=AI 合同平台
CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS=true
CONTRACT_API_IDENTITY_SMTP_USE_SSL=false
CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS=10
CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL=https://contracts.example.com
CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET=CHANGE_ME_USE_A_RANDOM_32_BYTE_SECRET
CONTRACT_API_IDENTITY_CHALLENGE_RESEND_COOLDOWN_SECONDS=60
CONTRACT_API_IDENTITY_CHALLENGE_RATE_WINDOW_SECONDS=3600
CONTRACT_API_IDENTITY_CHALLENGE_EMAIL_LIMIT=5
CONTRACT_API_IDENTITY_CHALLENGE_IP_LIMIT=20
CONTRACT_API_IDENTITY_CHALLENGE_TENANT_LIMIT=100
CONTRACT_API_IDENTITY_LOGIN_RATE_WINDOW_SECONDS=900
CONTRACT_API_IDENTITY_LOGIN_EMAIL_LIMIT=10
CONTRACT_API_IDENTITY_LOGIN_IP_LIMIT=50
CONTRACT_API_IDENTITY_ANTI_ENUMERATION_FLOOR_MS=120
CONTRACT_API_IDENTITY_ANTI_ENUMERATION_JITTER_MS=30
CONTRACT_REVIEW_WORKER_SEED_DEMO_DATA=false
CONTRACT_WEB_SESSION_SECRET=CHANGE_ME_USE_A_DIFFERENT_RANDOM_32_BYTE_SECRET
CONTRACT_WEB_SESSION_SECURE=true
@@ -0,0 +1,133 @@
services:
postgres:
image: ${POSTGRES_IMAGE:?POSTGRES_IMAGE must reference an immutable production image}
volumes:
- postgres-data:/var/lib/postgresql/data
postgres-init-databases:
image: ${POSTGRES_CLIENT_IMAGE:?POSTGRES_CLIENT_IMAGE must reference an immutable production image}
redis:
image: ${REDIS_IMAGE:?REDIS_IMAGE must reference an immutable production image}
command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
minio:
image: ${MINIO_IMAGE:?MINIO_IMAGE must reference an immutable production image}
volumes:
- minio-data:/data
contract-api-migrate:
image: ${CONTRACT_API_IMAGE:?CONTRACT_API_IMAGE must reference an immutable production image}
pull_policy: always
contract-api:
image: ${CONTRACT_API_IMAGE:?CONTRACT_API_IMAGE must reference an immutable production image}
pull_policy: always
volumes:
- contract-files:/data/contract-files
contract-review-worker:
image: ${CONTRACT_API_IMAGE:?CONTRACT_API_IMAGE must reference an immutable production image}
pull_policy: always
volumes:
- contract-files:/data/contract-files
ai-platform-api-migrate:
image: ${AI_PLATFORM_API_IMAGE:?AI_PLATFORM_API_IMAGE must reference an immutable production image}
pull_policy: always
ai-platform-prompt-assets-import:
image: ${AI_PLATFORM_API_IMAGE:?AI_PLATFORM_API_IMAGE must reference an immutable production image}
pull_policy: always
ai-platform-api:
image: ${AI_PLATFORM_API_IMAGE:?AI_PLATFORM_API_IMAGE must reference an immutable production image}
pull_policy: always
environment:
AI_PLATFORM_SECRET_FILE_ROOT: /run/secrets
secrets:
- source: ai_provider_api_key
target: ai_provider_api_key
worker:
image: ${AI_WORKER_IMAGE:?AI_WORKER_IMAGE must reference an immutable production image}
pull_policy: always
contract-web:
profiles: ["backend-hosted-web"]
edge:
image: ${NGINX_IMAGE:?NGINX_IMAGE must reference an immutable production image}
restart: unless-stopped
environment:
BACKEND_SERVER_NAME: ${BACKEND_SERVER_NAME:?BACKEND_SERVER_NAME is required}
NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
ports:
- "${BACKEND_HTTP_BIND_ADDRESS:-0.0.0.0}:${BACKEND_HTTP_PORT:-80}:80"
- "${BACKEND_HTTPS_BIND_ADDRESS:-0.0.0.0}:${BACKEND_HTTPS_PORT:-443}:443"
volumes:
- ../nginx/templates:/etc/nginx/templates:ro
- ${BACKEND_TLS_CERT_DIR:?BACKEND_TLS_CERT_DIR is required}:/etc/nginx/tls:ro
read_only: true
tmpfs:
- /var/cache/nginx
- /var/run
- /etc/nginx/conf.d
- /tmp
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "wget", "--no-check-certificate", "-qO-", "https://127.0.0.1/healthz"]
interval: 15s
timeout: 5s
retries: 6
depends_on:
contract-api:
condition: service_healthy
ai-platform-api:
condition: service_healthy
contract-files-backup:
image: ${BACKUP_UTILITY_IMAGE:?BACKUP_UTILITY_IMAGE must reference an immutable production image}
profiles: ["ops"]
entrypoint: ["/bin/sh", "-c"]
command: ["exit 0"]
volumes:
- contract-files:/source:ro
- ${BACKUP_HOST_DIR:?BACKUP_HOST_DIR is required}:/backup
read_only: true
security_opt:
- no-new-privileges:true
minio-backup:
image: ${MINIO_MC_IMAGE:?MINIO_MC_IMAGE must reference an immutable production image}
profiles: ["ops"]
entrypoint: ["/bin/sh", "-c"]
command: ["exit 0"]
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
AI_PLATFORM_OBJECT_STORAGE_BUCKET: ${AI_PLATFORM_OBJECT_STORAGE_BUCKET:-ai-platform-uploads}
MC_CONFIG_DIR: /tmp/.mc
volumes:
- ${BACKUP_HOST_DIR:?BACKUP_HOST_DIR is required}:/backup
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
depends_on:
minio:
condition: service_healthy
volumes:
postgres-data:
name: ${POSTGRES_VOLUME_NAME:-yuqei-v2-production-postgres}
minio-data:
name: ${MINIO_VOLUME_NAME:-yuqei-v2-production-minio}
contract-files:
name: ${CONTRACT_FILES_VOLUME_NAME:-yuqei-v2-production-contract-files}
secrets:
ai_provider_api_key:
file: ${AI_PROVIDER_API_KEY_FILE:?AI_PROVIDER_API_KEY_FILE must point to a protected provider key file}
+34 -9
View File
@@ -9,7 +9,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-yuqei-local}
POSTGRES_DB: ${POSTGRES_DB:-postgres}
ports:
- "${POSTGRES_HOST_PORT:-5432}:5432"
- "${POSTGRES_BIND_ADDRESS:-0.0.0.0}:${POSTGRES_HOST_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
@@ -19,7 +19,7 @@ services:
retries: 5
postgres-init-databases:
image: postgres:16-alpine
image: ${POSTGRES_CLIENT_IMAGE:-postgres:16-alpine}
depends_on:
postgres:
condition: service_healthy
@@ -41,10 +41,10 @@ services:
restart: "no"
redis:
image: redis:7-alpine
image: ${REDIS_IMAGE:-redis:7-alpine}
restart: unless-stopped
ports:
- "${REDIS_HOST_PORT:-6379}:6379"
- "${REDIS_BIND_ADDRESS:-0.0.0.0}:${REDIS_HOST_PORT:-6379}:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
@@ -59,8 +59,8 @@ services:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-yuqei-minio}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-yuqei-minio-local}
ports:
- "${MINIO_API_HOST_PORT:-9000}:9000"
- "${MINIO_CONSOLE_HOST_PORT:-9001}:9001"
- "${MINIO_API_BIND_ADDRESS:-0.0.0.0}:${MINIO_API_HOST_PORT:-9000}:9000"
- "${MINIO_CONSOLE_BIND_ADDRESS:-0.0.0.0}:${MINIO_CONSOLE_HOST_PORT:-9001}:9001"
volumes:
- minio-data:/data
healthcheck:
@@ -121,8 +121,33 @@ services:
CONTRACT_API_DB_POOL_TIMEOUT_SECONDS: ${CONTRACT_API_DB_POOL_TIMEOUT_SECONDS:-30}
CONTRACT_API_DB_POOL_RECYCLE_SECONDS: ${CONTRACT_API_DB_POOL_RECYCLE_SECONDS:-1800}
CONTRACT_API_READ_CACHE_TTL_SECONDS: ${CONTRACT_API_READ_CACHE_TTL_SECONDS:-2}
CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE: ${CONTRACT_API_IDENTITY_EMAIL_DELIVERY_MODE:-development}
CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED: ${CONTRACT_API_IDENTITY_SELF_SERVICE_ENABLED:-true}
CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED: ${CONTRACT_API_IDENTITY_MANUAL_INVITATION_ENABLED:-false}
CONTRACT_API_IDENTITY_SMTP_HOST: ${CONTRACT_API_IDENTITY_SMTP_HOST:-}
CONTRACT_API_IDENTITY_SMTP_PORT: ${CONTRACT_API_IDENTITY_SMTP_PORT:-587}
CONTRACT_API_IDENTITY_SMTP_USERNAME: ${CONTRACT_API_IDENTITY_SMTP_USERNAME:-}
CONTRACT_API_IDENTITY_SMTP_PASSWORD: ${CONTRACT_API_IDENTITY_SMTP_PASSWORD:-}
CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE: ${CONTRACT_API_IDENTITY_SMTP_PASSWORD_FILE:-}
CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL: ${CONTRACT_API_IDENTITY_SMTP_FROM_EMAIL:-}
CONTRACT_API_IDENTITY_SMTP_FROM_NAME: ${CONTRACT_API_IDENTITY_SMTP_FROM_NAME:-AI 合同平台}
CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS: ${CONTRACT_API_IDENTITY_SMTP_USE_STARTTLS:-true}
CONTRACT_API_IDENTITY_SMTP_USE_SSL: ${CONTRACT_API_IDENTITY_SMTP_USE_SSL:-false}
CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS: ${CONTRACT_API_IDENTITY_SMTP_TIMEOUT_SECONDS:-10}
CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL: ${CONTRACT_API_IDENTITY_PUBLIC_WEB_BASE_URL:-http://localhost:3000}
CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET: ${CONTRACT_API_IDENTITY_RATE_LIMIT_SECRET:-local-only-identity-rate-secret-change-me}
CONTRACT_API_IDENTITY_CHALLENGE_RESEND_COOLDOWN_SECONDS: ${CONTRACT_API_IDENTITY_CHALLENGE_RESEND_COOLDOWN_SECONDS:-60}
CONTRACT_API_IDENTITY_CHALLENGE_RATE_WINDOW_SECONDS: ${CONTRACT_API_IDENTITY_CHALLENGE_RATE_WINDOW_SECONDS:-3600}
CONTRACT_API_IDENTITY_CHALLENGE_EMAIL_LIMIT: ${CONTRACT_API_IDENTITY_CHALLENGE_EMAIL_LIMIT:-5}
CONTRACT_API_IDENTITY_CHALLENGE_IP_LIMIT: ${CONTRACT_API_IDENTITY_CHALLENGE_IP_LIMIT:-20}
CONTRACT_API_IDENTITY_CHALLENGE_TENANT_LIMIT: ${CONTRACT_API_IDENTITY_CHALLENGE_TENANT_LIMIT:-100}
CONTRACT_API_IDENTITY_LOGIN_RATE_WINDOW_SECONDS: ${CONTRACT_API_IDENTITY_LOGIN_RATE_WINDOW_SECONDS:-900}
CONTRACT_API_IDENTITY_LOGIN_EMAIL_LIMIT: ${CONTRACT_API_IDENTITY_LOGIN_EMAIL_LIMIT:-10}
CONTRACT_API_IDENTITY_LOGIN_IP_LIMIT: ${CONTRACT_API_IDENTITY_LOGIN_IP_LIMIT:-50}
CONTRACT_API_IDENTITY_ANTI_ENUMERATION_FLOOR_MS: ${CONTRACT_API_IDENTITY_ANTI_ENUMERATION_FLOOR_MS:-120}
CONTRACT_API_IDENTITY_ANTI_ENUMERATION_JITTER_MS: ${CONTRACT_API_IDENTITY_ANTI_ENUMERATION_JITTER_MS:-30}
ports:
- "${CONTRACT_API_HOST_PORT:-8100}:8100"
- "${CONTRACT_API_BIND_ADDRESS:-0.0.0.0}:${CONTRACT_API_HOST_PORT:-8100}:8100"
volumes:
- contract-files:/data/contract-files
healthcheck:
@@ -250,7 +275,7 @@ services:
AI_PLATFORM_OBJECT_STORAGE_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-yuqei-minio-local}
AI_PLATFORM_OBJECT_STORAGE_REGION_NAME: us-east-1
ports:
- "${AI_PLATFORM_API_HOST_PORT:-8101}:8101"
- "${AI_PLATFORM_API_BIND_ADDRESS:-0.0.0.0}:${AI_PLATFORM_API_HOST_PORT:-8101}:8101"
healthcheck:
test: ["CMD", "python", "-c", "import json, urllib.request; data=json.load(urllib.request.urlopen('http://127.0.0.1:8101/health', timeout=5)); assert data['status'] == 'ok'"]
interval: 10s
@@ -331,7 +356,7 @@ services:
AI_PLATFORM_PROXY_SECRET: ${AI_PLATFORM_SERVICE_SHARED_SECRET:-local-only-ai-service-secret-change-me-32}
NEXT_PUBLIC_AI_PLATFORM_API_BASE_URL: http://localhost:8101
ports:
- "${CONTRACT_WEB_HOST_PORT:-3000}:3000"
- "${CONTRACT_WEB_BIND_ADDRESS:-0.0.0.0}:${CONTRACT_WEB_HOST_PORT:-3000}:3000"
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1));"]
interval: 10s