diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index a68b2eb..eef2c56 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -112,38 +112,39 @@ jobs: - name: List images run: docker images - - name: Apply DB migrations - # 数据库自动升级: 按 prisma/migrations/ 下的迁移顺序应用 (生产库由 master 部署时统一升级) - # migrate deploy 幂等, 无新迁移时为 no-op - # 位置关键: 在 "Remove old container" 之前 —— 建表/改表成功后才切换流量, - # 迁移失败则整个 workflow 中断, 不会用新镜像顶掉仍在运行的旧容器 - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - run: | - set -euo pipefail - npx prisma migrate status || true - npx prisma migrate deploy - - - name: Remove old container - run: docker rm -vf iboard || true - - - name: Prune dangling images - # 只清理本项目的悬空镜像 (旧 iboard:latest 被同名新镜像顶掉后变 dangling), - # 不再用 `docker system prune --volumes` —— 那会清光宿主机 build cache, - # 导致每次部署都全量重装 npm 依赖、重跑构建 - run: docker image prune -f - - - name: Run container - # DATABASE_URL 通过 gitea Actions secret 注入, 不进镜像层 - # OTA_PACKAGE_DIR 已在 Dockerfile 设为 /app/ota-package - # 米家凭证 (auth.txt) 不进 git: 运维放宿主机 /app/iboard/cache, bind-mount 进容器 (MIHOME_CACHE_DIR=/app/cache) - run: | - docker run -d --name iboard -p 3001:3001 \ - -e DATABASE_URL="${{ secrets.DATABASE_URL }}" \ - -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ - -e MIHOME_CACHE_DIR=/app/cache \ - -v /app/iboard/cache:/app/cache \ - iboard:latest - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} + - name: Deploy 0.2.1 production stack + # 0.2.1 起生产为单端口架构: app(3001 对外) + pg(内网) + powersync(内网), + # 客户端同步经 app 的 /sync 反代进入同步服务 (无独立同步端口)。 + # 部署逻辑在服务器的 /app/iboard-prod/scripts/deploy/prod.sh (幂等), 这里只负责: + # 1) 把刚构建好的镜像交给该脚本 + # 2) 未配置 PG 相关 secrets 或脚本缺失时跳过部署 (发布不失败, 仅提示) + env: + PG_PASSWORD: ${{ secrets.PG_PASSWORD }} + PS_STORAGE_PASSWORD: ${{ secrets.PS_STORAGE_PASSWORD }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + POWERSYNC_JWT_SECRET: ${{ secrets.POWERSYNC_JWT_SECRET }} + POWERSYNC_JWT_KEY_B64: ${{ secrets.POWERSYNC_JWT_KEY_B64 }} + run: | + set -euo pipefail + HOST_DIR=/app/iboard-prod + if [ -z "${PG_PASSWORD:-}" ]; then + echo "::warning::未配置 PG_PASSWORD 等 secrets, 跳过自动部署 (镜像已构建完成)" + echo "手工部署: ssh baihu 'bash $HOST_DIR/scripts/deploy/prod.sh'" + exit 0 + fi + if ! docker run --rm -v "$HOST_DIR:/w" alpine:3.20 test -f /w/scripts/deploy/prod.sh; then + echo "::warning::$HOST_DIR/scripts/deploy/prod.sh 不存在, 跳过部署" + exit 0 + fi + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$HOST_DIR:$HOST_DIR" \ + -w "$HOST_DIR" \ + -e PG_PASSWORD="$PG_PASSWORD" \ + -e PS_STORAGE_PASSWORD="$PS_STORAGE_PASSWORD" \ + -e JWT_SECRET="$JWT_SECRET" \ + -e POWERSYNC_JWT_SECRET="$POWERSYNC_JWT_SECRET" \ + -e POWERSYNC_JWT_KEY_B64="$POWERSYNC_JWT_KEY_B64" \ + -e PUBLIC_URL=https://iboard.honor3.com \ + -e ROOT="$HOST_DIR" \ + docker:27-cli sh -c 'apk add -q bash curl >/dev/null 2>&1; bash /app/iboard-prod/scripts/deploy/prod.sh' diff --git a/scripts/deploy/prod.sh b/scripts/deploy/prod.sh new file mode 100644 index 0000000..9c739b5 --- /dev/null +++ b/scripts/deploy/prod.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# 0.2.1 生产部署 (master 分支 CI 调用; 也可在服务器手工执行) +# +# 架构: app(3001 对外) + pg(内网) + powersync(内网), 见 deploy/prod/ +# 幂等: 迁移 -> publication -> 起栈; 任一环节失败则退出, 不切换流量 +# +# 所需环境变量 (由 CI secrets 注入): +# PG_PASSWORD, PS_STORAGE_PASSWORD, JWT_SECRET, POWERSYNC_JWT_SECRET, +# POWERSYNC_JWT_KEY_B64, PUBLIC_URL(可选, 默认 https://iboard.honor3.com) +set -euo pipefail + +ROOT="${ROOT:-/app/iboard-prod}" +COMPOSE_FILE="$ROOT/deploy/prod/docker-compose.yml" +ENV_FILE="$ROOT/deploy/prod/.env" +PROJECT="iboard-prod" + +log() { echo "[deploy] $*"; } +die() { echo "[deploy][ERROR] $*" >&2; exit 1; } + +for v in PG_PASSWORD PS_STORAGE_PASSWORD JWT_SECRET POWERSYNC_JWT_SECRET POWERSYNC_JWT_KEY_B64; do + [ -n "${!v:-}" ] || die "缺少环境变量 $v (请在 Gitea Actions secrets 配置)" +done +[ -f "$COMPOSE_FILE" ] || die "找不到 $COMPOSE_FILE (源码需上传到 $ROOT)" + +log "写入 $ENV_FILE" +umask 077 +cat > "$ENV_FILE" </dev/null 2>&1; then + break + fi + sleep 2 +done + +log "执行数据库迁移" +docker run --rm --network "${PROJECT}_default" \ + -e DATABASE_URL="postgresql://iboard:${PG_PASSWORD}@pg:5432/iboard" \ + iboard:latest npx prisma migrate deploy + +log "确保逻辑复制 publication 存在" +docker compose --project-name "$PROJECT" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T pg \ + psql -U iboard -d iboard -v ON_ERROR_STOP=1 -f /dev/stdin < sql/publication.sql + +log "启动应用与同步服务" +docker compose --project-name "$PROJECT" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d app powersync + +log "等待健康检查" +for i in $(seq 1 30); do + if curl -fsS --max-time 3 http://127.0.0.1:3001/api/health >/dev/null 2>&1; then + log "健康检查通过" + exit 0 + fi + sleep 2 +done +die "应用未通过健康检查, 请查看 docker compose logs"