devops: 备案通过回切 https 域名 + 流水线 Ali 单轨

- nginx-jiu-ali.conf:443 ssl+http2 正式入口(HSTS/XFO/nosniff 安全头、
  ACME webroot 续期通道、80→443 跳转);8443 明文过渡口拆除
- 客户端构建 URL 全量回切 https://jiu.51yanmei.com(compile×5/local_test/
  release-client/notify)
- 流水线去 EC2:deploy-client/site 单轨 Ali、manual 回滚与每日备份切 ali、
  seed/reset/debug-db 改容器内取密码(SEC-003,退役 DB_PASSWORD secret)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
wangjia
2026-07-03 09:58:30 +08:00
parent 6238b86dcb
commit 824992fe6e
13 changed files with 108 additions and 92 deletions
+4 -3
View File
@@ -18,9 +18,10 @@ jobs:
- name: Dump MySQL to local backup dir
env:
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
# 2026-07-03 割接后备份源=阿里机主库
DEPLOY_SSH_KEY: ${{ secrets.ALI_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.ALI_HOST }}
DEPLOY_USER: ${{ secrets.ALI_USER }}
run: sh scripts/ci/backup-db.sh
- name: Notify (Telegram)
+9 -10
View File
@@ -10,15 +10,14 @@ jobs:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
printf '%s' "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
printf '%s' "${{ secrets.ALI_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
chmod 600 ~/.ssh/ec2_deploy.pem
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts
ssh-keyscan -H ${{ secrets.ALI_HOST }} >> ~/.ssh/known_hosts
- name: Query DB
env:
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
EC2_HOST: ${{ secrets.ALI_HOST }}
EC2_USER: ${{ secrets.ALI_USER }}
run: |
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} bash << EOF
echo "=== Docker containers ==="
@@ -26,20 +25,20 @@ jobs:
echo ""
echo "=== Databases ==="
docker exec jiu_mysql mysql -uroot -p${DB_PASSWORD} -e "SHOW DATABASES;" 2>/dev/null \
|| echo "jiu_mysql container not found or DB_PASSWORD wrong"
docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD -e "SHOW DATABASES;"' 2>/dev/null \
|| echo "jiu_mysql container not found or MYSQL_ROOT_PASSWORD wrong"
echo ""
echo "=== Tables in jiu_db ==="
docker exec jiu_mysql mysql -uroot -p${DB_PASSWORD} jiu_db -e "SHOW TABLES;" 2>/dev/null
docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD jiu_db -e "SHOW TABLES;"' 2>/dev/null
echo ""
echo "=== shops ==="
docker exec jiu_mysql mysql -uroot -p${DB_PASSWORD} jiu_db -e "SELECT id, name, code FROM shops;" 2>/dev/null
docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD jiu_db -e "SELECT id, name, code FROM shops;"' 2>/dev/null
echo ""
echo "=== users ==="
docker exec jiu_mysql mysql -uroot -p${DB_PASSWORD} jiu_db -e "SELECT id, shop_id, username, role, is_active FROM users;" 2>/dev/null
docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD jiu_db -e "SELECT id, shop_id, username, role, is_active FROM users;"' 2>/dev/null
EOF
- name: Cleanup SSH key
+3 -14
View File
@@ -180,20 +180,9 @@ jobs:
GITEA_REPOSITORY: ${{ gitea.repository }}
run: sh scripts/ci/release-client.sh "${{ gitea.ref_name }}"
- name: Deploy → EC2
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
GITEA_REPOSITORY: ${{ gitea.repository }}
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
run: sh scripts/ci/deploy-client.sh "${{ gitea.ref_name }}"
# 迁移期影子目标:同步 Flutter web + version.yaml + 安装包到阿里机。失败不挡 EC2。
- name: Deploy → Ali (migration shadow)
if: always()
continue-on-error: true
# 2026-07-03 割接后阿里机即唯一发布目标(EC2 轨移除;EC2 仅保留
# jiu.51yanmei.com 反代桥接到 ali,见 baize 台账)。
- name: Deploy → Ali
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
+3 -14
View File
@@ -32,20 +32,9 @@ jobs:
GITEA_REPOSITORY: ${{ gitea.repository }}
run: sh scripts/ci/release-site.sh "${{ gitea.ref_name }}"
- name: Deploy → EC2
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
GITEA_REPOSITORY: ${{ gitea.repository }}
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
run: sh scripts/ci/deploy-site.sh "${{ gitea.ref_name }}"
# 迁移期影子目标:同步官网到阿里机。失败不挡 EC2。
- name: Deploy → Ali (migration shadow)
if: always()
continue-on-error: true
# 2026-07-03 割接后阿里机即唯一发布目标(EC2 轨移除;EC2 仅保留
# jiu.51yanmei.com 反代桥接到 ali,见 baize 台账)。
- name: Deploy → Ali
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
+10 -3
View File
@@ -28,9 +28,16 @@ jobs:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
GITEA_REPOSITORY: ${{ gitea.repository }}
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
# 2026-07-03 割接后回滚目标统一为阿里机
DEPLOY_TARGET: ali
DEPLOY_SSH_KEY: ${{ secrets.ALI_SSH_KEY }}
DEPLOY_HOST: ${{ secrets.ALI_HOST }}
DEPLOY_USER: ${{ secrets.ALI_USER }}
# 兼容:回滚割接前旧 tag 时,旧脚本直接引用 EC2_*(set -u 下缺失即炸)——
# 同值指向阿里,旧脚本也会部署到阿里而不是复活 EC2
EC2_SSH_KEY: ${{ secrets.ALI_SSH_KEY }}
EC2_HOST: ${{ secrets.ALI_HOST }}
EC2_USER: ${{ secrets.ALI_USER }}
run: |
TAG="${{ inputs.version }}"
case "$TAG" in
+7 -8
View File
@@ -17,20 +17,19 @@ jobs:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
printf '%s' "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
printf '%s' "${{ secrets.ALI_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
chmod 600 ~/.ssh/ec2_deploy.pem
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts
ssh-keyscan -H ${{ secrets.ALI_HOST }} >> ~/.ssh/known_hosts
- name: Reset database & reseed
env:
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
EC2_HOST: ${{ secrets.ALI_HOST }}
EC2_USER: ${{ secrets.ALI_USER }}
SHOPS: ${{ github.event.inputs.shops }}
run: |
# 1. 清库重建
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} \
"docker exec jiu_mysql mysql -uroot -p${DB_PASSWORD} -e 'DROP DATABASE IF EXISTS jiu_db; CREATE DATABASE jiu_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"
"docker exec jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD -e \"DROP DATABASE IF EXISTS jiu_db; CREATE DATABASE jiu_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\"'"
echo "✓ Database reset"
# 2. 重启 jiu-serverAutoMigrate 重建表结构
@@ -57,13 +56,13 @@ jobs:
echo "→ Seeding $SHOP ..."
if [ "$FIRST" = "true" ]; then
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} \
"docker exec -i jiu_mysql mysql -uroot -p${DB_PASSWORD} --default-character-set=utf8mb4 jiu_db" \
"docker exec -i jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD --default-character-set=utf8mb4 jiu_db'" \
< "$FILE"
FIRST=false
else
grep -v "^TRUNCATE" "$FILE" | \
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} \
"docker exec -i jiu_mysql mysql -uroot -p${DB_PASSWORD} --default-character-set=utf8mb4 jiu_db"
"docker exec -i jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD --default-character-set=utf8mb4 jiu_db'"
fi
echo "✓ $SHOP done"
done
+6 -7
View File
@@ -21,15 +21,14 @@ jobs:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
printf '%s' "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
printf '%s' "${{ secrets.ALI_SSH_KEY }}" > ~/.ssh/ec2_deploy.pem
chmod 600 ~/.ssh/ec2_deploy.pem
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts
ssh-keyscan -H ${{ secrets.ALI_HOST }} >> ~/.ssh/known_hosts
- name: Seed databases
env:
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
EC2_HOST: ${{ secrets.ALI_HOST }}
EC2_USER: ${{ secrets.ALI_USER }}
SHOPS: ${{ github.event.inputs.shops }}
CLEAR: ${{ github.event.inputs.clear }}
run: |
@@ -41,7 +40,7 @@ jobs:
echo " 清空 ${SHOP}..."
printf "SET @shop_code='%s';\n" "$SHOP" | cat - backend/seeds/clear_shop.sql | \
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} \
"docker exec -i jiu_mysql mysql -uroot -p${DB_PASSWORD} --default-character-set=utf8mb4 jiu_db"
"docker exec -i jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD --default-character-set=utf8mb4 jiu_db'"
done
echo "✓ 清空完成"
fi
@@ -56,7 +55,7 @@ jobs:
fi
echo "→ Seeding $SHOP ..."
ssh -i ~/.ssh/ec2_deploy.pem ${EC2_USER}@${EC2_HOST} \
"docker exec -i jiu_mysql mysql -uroot -p${DB_PASSWORD} --default-character-set=utf8mb4 jiu_db" \
"docker exec -i jiu_mysql sh -c 'exec mysql -uroot -p\$MYSQL_ROOT_PASSWORD --default-character-set=utf8mb4 jiu_db'" \
< "$FILE"
echo "✓ $SHOP done"
done
+46 -19
View File
@@ -2,34 +2,51 @@
# 与 nginx-jiu.confEC2 pangolin 容器版,listen 127.0.0.1:8445)等价。
# CI deploy-server.sh 的 ali 分支落到 /etc/nginx/conf.d/jiu.conf。
#
# 过渡期入口 = 8443 明文 HTTP,裸 IP 直连 http://182.92.213.171:8443
#(备案未通过前 80/443/8080 被阿里云拦截,走非标端口;不建过渡子域,
# 证书签给域名对不上 IP,故过渡期不做 TLS——登录口令/JWT 明文传输,仅限过渡;
# 备案通过后改回 listen 443 ssl + 域名 + 80→443 跳转块)。
# 2026-07-03 备案通过回切:443 ssljiu.51yanmei.com)为唯一正式入口 + 80→443 跳转。
# 8443 明文过渡口已拆(用户拍板:裸 IP 版 client 从未发过,存量客户端都是 https
# 域名 BASE_URLDNS 回切后自动恢复,8443 无人使用)。
# 证书:/etc/nginx/ssl/jiu.51yanmei.com/2026-07-03 从 EC2 letsencrypt 同步,
# 2026-08-28 到期;续期方案=DNS 已指 ali 后 certbot HTTP-01 webroot,见 baize)。
# 后端上游 = 127.0.0.1:8081(非 8080ali 上 8080 已被 pay 项目 payd 占用;
# 对应 /opt/jiu/config/production.env 的 SERVER_PORT=8081)。
limit_req_zone $binary_remote_addr zone=jiu_pub:10m rate=10r/s;
limit_req_status 429;
server {
listen 8443 default_server;
server_name _;
# ali nginx 1.24 无 `http2 on;`1.25.1+),用 listen 参数旧语法
listen 443 ssl http2 default_server; # 正式入口(备案后回切 2026-07-03
server_name jiu.51yanmei.com _;
# 备案通过切回 443 时恢复(证书路径仍有效,jiu.51yanmei.com2026-09-21 到期)
# listen 443 ssl http2;
# server_name jiu.51yanmei.com;
# ssl_certificate /etc/letsencrypt/live/jiu.51yanmei.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/jiu.51yanmei.com/privkey.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers HIGH:!aNULL:!MD5;
# 证书现为 EC2 同步副本(2026-08-28 到期)。ACME 通道已就绪:待批准后在 ali 跑
# `certbot certonly --webroot -w /var/www/acme -d jiu.51yanmei.com` 签新证,
# 然后把这两行换成 /etc/letsencrypt/live/jiu.51yanmei.com/ 并配 reload 部署钩子。
ssl_certificate /etc/nginx/ssl/jiu.51yanmei.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/jiu.51yanmei.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 安全响应头(SEC-P02)。注意 nginx add_header 继承规则:location 内有自己的
# add_header 时会整体覆盖 server 级——故 /images、/app/index.html、/downloads
# 三处各自补了 HSTS。
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
client_max_body_size 20m;
# certbot HTTP-01 续期通道(webroot 独立目录,不放 marketing——
# deploy-site 的 rsync --delete 会清掉站点目录里的挑战文件)
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
}
# 商品图片静态文件
location ^~ /images/ {
alias /opt/jiu/images/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Strict-Transport-Security "max-age=31536000" always;
}
# 文件导入接口(超时更长)
@@ -68,6 +85,7 @@ server {
alias /opt/jiu/web/index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Strict-Transport-Security "max-age=31536000" always;
expires 0;
}
@@ -95,6 +113,7 @@ server {
alias /opt/jiu/downloads/;
add_header Content-Disposition "attachment";
add_header Cache-Control "no-cache";
add_header Strict-Transport-Security "max-age=31536000" always;
autoindex off;
}
@@ -105,9 +124,17 @@ server {
}
}
# 备案通过、入口切回 443 后再恢复此跳转块(80 未备案期间被拦截,挂了也无意义):
# server {
# listen 80;
# server_name jiu.51yanmei.com;
# return 301 https://$host$request_uri;
# }
# 80 → 443 跳转(仅 jiu 域名;51yanmei.com 门户 80 在 51yanmei.conf 自管)。
# ACME 挑战必须在跳转前放行(HTTP-01 走 80)。
server {
listen 80;
server_name jiu.51yanmei.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
+14 -9
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env bash
# backup-db.sh — dump the production MySQL (jiu_db) from EC2 and keep a gzip
# snapshot on the mac (host) runner's local disk (~/jiu-db-backups), retaining
# the last 30 days. The DB password is read on EC2 from production.env's
# DATABASE_DSN, so no DB-password secret is needed.
# backup-db.sh — dump the production MySQL (jiu_db) from the production host
# and keep a gzip snapshot on the mac (host) runner's local disk
# (~/jiu-db-backups), retaining the last 30 days. The DB password is read on
# the host from production.env's DATABASE_DSN, so no DB-password secret is
# needed.
#
# Requires env (same as deploy-server): EC2_SSH_KEY, EC2_HOST, EC2_USER.
# Target host (post-cutover 2026-07-03: Ali is the primary): pass
# DEPLOY_SSH_KEY / DEPLOY_HOST / DEPLOY_USER (falls back to EC2_* legacy vars).
set -euo pipefail
# shellcheck source=scripts/ci/lib-forgejo.sh
@@ -15,15 +17,18 @@ mkdir -p "${BACKUP_DIR}"
FILENAME="jiu_db_$(date +%Y%m%d_%H%M%S).sql.gz"
DEST="${BACKUP_DIR}/${FILENAME}"
TARGET_HOST="${DEPLOY_HOST:-$EC2_HOST}"
TARGET_USER="${DEPLOY_USER:-$EC2_USER}"
setup_ssh
trap teardown_ssh EXIT
echo "==> backup-db: dumping jiu_db from ${EC2_HOST}"
# On EC2: parse the DSN password from production.env, then mysqldump the
echo "==> backup-db: dumping jiu_db from ${TARGET_HOST}"
# On the host: parse the DSN password from production.env, then mysqldump the
# container to stdout; stream back over ssh and gzip locally. The heredoc is
# single-quoted, so it runs verbatim on EC2 (no local expansion). ${SSH} carries
# single-quoted, so it runs verbatim on the host (no local expansion). ${SSH} carries
# no -t, keeping stdout a clean dump stream.
${SSH} "${EC2_USER}@${EC2_HOST}" 'bash -s' <<'ENDSSH' | gzip > "${DEST}"
${SSH} "${TARGET_USER}@${TARGET_HOST}" 'bash -s' <<'ENDSSH' | gzip > "${DEST}"
set -euo pipefail
PW=$(python3 -c 'import re;e=open("/opt/jiu/config/production.env").read();d=re.search(r"DATABASE_DSN=(.*)",e).group(1).strip().strip(chr(34)).strip(chr(39));print(re.match(r"[^:]+:([^@]+)@",d).group(1))')
exec docker exec -e MYSQL_PWD="${PW}" jiu_mysql mysqldump -uroot --single-transaction --no-tablespaces jiu_db
+2 -2
View File
@@ -8,7 +8,7 @@
# is decommissioned — deploying there would resurrect a backend against a stale
# database, so the workflow no longer calls the ec2 branch):
# - ali: stop jiu → swap binary → start jiu → health check (:8081) →
# reload the host nginx (transition listen-8443 config).
# reload the host nginx (443 ssl jiu.51yanmei.com, 备案回切 2026-07-03).
# - ec2 (legacy default, kept for manual rollback of pre-cutover tags only):
# stop jiu → swap binary → start jiu → health check → reload nginx
# inside the pangolin-edge container.
@@ -56,7 +56,7 @@ fi
if [ "$DEPLOY_TARGET" = "ali" ]; then
# --- Alibaba Cloud (live primary since the 2026-07-02 cutover) ---
# Full swap-and-restart: jiu.service runs against the promoted local primary
# DB on :8081; nginx serves the transition listen-8443 config.
# DB on :8081; nginx serves 443 ssl (jiu.51yanmei.com, 备案回切 2026-07-03).
${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH'
set -e
+1 -1
View File
@@ -14,7 +14,7 @@ else
fi
MSG="${ICON} 岩美 ${TAG} ${LABEL}
https://jiu.51yanmei.com"
https://jiu.51yanmei.com" # 2026-07-03 备案通过,正式域名入口
curl -f -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
+1 -1
View File
@@ -28,7 +28,7 @@ ver = sys.argv[1]
notes = sys.argv[2]
changelog_file = sys.argv[3]
base = "https://jiu.51yanmei.com/downloads"
base = "https://jiu.51yanmei.com/downloads" # 2026-07-03 备案通过回切 https 域名
mac_url = f"{base}/jiu-macos-x64.zip"
win_url = f"{base}/jiu-windows-x64-setup.exe"
android_url = f"{base}/jiu-android.apk"
+2 -1
View File
@@ -5,7 +5,8 @@
# sh scripts/local_test.sh # 编译 + 启动
# sh scripts/local_test.sh --no-open # 只编译不启动
#
# 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile-macos.sh 一致)。
# 后端固定指向生产 https://jiu.51yanmei.com(与 CI compile-macos.sh 一致
# 2026-07-03 备案通过已回切 https 域名)。
set -euo pipefail
export PATH="/opt/homebrew/bin:$PATH"