Files
iboard/.gitea/workflows/docker-build.yml
T
2026-10-27 08:09:00 +08:00

169 lines
8.5 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Docker Build and Push
# 仅主分支 (master) push 触发部署; 其他分支一律不触发 (见 readme「分支规则」)
# 每次提交: 构建前端 + 生成 OTA 升级包 (签名) → 打进 Docker 镜像 → 自动升级数据库 → 部署
# OTA 随镜像发布: 容器内 OTA_PACKAGE_DIR=/app/ota-package 指向打包的 ota-package/
#
# 分支策略 (2026-10 调整):
# - master = 主分支, 唯一部署出口, push 即发布生产 (容器 iboard, 宿主 3001)
# - dev-* = 开发集成分支, 只做 CI 校验 (构建/版本一致性), **不再自动部署**;
# 要上线必须先合并到 master 再推 master
# - tmp-* = 单个问题的临时分支, 合到 dev-* 后删除
# - android-*= 只用于触发 APK 外壳构建, 不部署; 改动前须与 master 同步
#
# 历史: 之前 dev-* 与 master 共用同一套生产环境 (推送即发布), 容易把半成品带上线,
# 现已取消 dev-* 的 push 触发。
on:
push:
branches:
- master
# 连续 push master 时取消还在跑的旧部署, 避免排队执行过时版本的完整部署
concurrency:
group: deploy-master
cancel-in-progress: true
jobs:
build-image:
runs-on: ubuntu-latest
env:
# 关键: 显式固定 Android/OTA 后端地址, 覆盖 runner 环境里可能存在的
# 其他 VITE_API_BASE 值。否则 OTA 包里的前端可能连错后端, 导致
# "OTA 更新后登录失败/进不去内部"。与 android-build.yml 保持一致。
VITE_API_BASE: https://iboard.honor3.com
# OTA bundle 要求的最低原生二进制版本 (hotswap 据此拒绝不兼容的旧外壳)。
# 0.2.1 起必须为 0.2.1: 旧外壳 (0.1.0) 的 CSP 白名单编译进了原生二进制,
# 不含 https://iboard.honor3.com, 若让它拉到 0.2.1 前端会因 CSP 拦截而白屏。
# 旧外壳请整包升级 (Windows 重装 exe / Android 换装 APK), 不要靠 OTA。
OTA_MIN_BINARY: '0.2.1'
steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Setup Node
uses: https://gitea.com/actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Check version consistency & branch
# 1) package.json / tauri.conf.json / Cargo.toml / Cargo.lock 四处版本必须完全相等且不带 -ota
# (tauri.conf.json 的 version 就是 APK 的 versionName)
# 2) 防止 OTA 版本号与分支错位: OTA 包的 version 直接取自 package.json,
# dev-0.2 分支上必须是 0.2.x, 否则线上 OTA 会报成上一个版本的号
# (master 分支跳过分支前缀校验, 但四处一致性照样强制)
env:
GITHUB_REF_NAME: ${{ github.ref_name }}
run: node scripts/ci/check-version.mjs
- name: Install deps
run: npm ci --no-audit --no-fund
- name: Build frontend
# 全流程唯一一次前端构建: dist/ 既供 OTA 打包, 也通过 build context 进入镜像
# (docker build 传 PREBUILT_DIST=true 跳过镜像内的重复构建)
run: npm run build
- name: Install minisign
# 静态二进制直接下载, 免去每次 apt-get update + install
# 注意: release tarball 是 macOS 上打包的, 混有多个平台的 minisign 二进制,
# 逐个试运行挑出本机可执行的, 都不行再回退 apt
run: |
set -euo pipefail
mkdir -p /tmp/ms
curl -sL https://github.com/jedisct1/minisign/releases/download/0.12/minisign-0.12-linux.tar.gz | tar xz -C /tmp/ms
BIN=""
for c in $(find /tmp/ms -type f -name minisign); do
if "$c" -v >/dev/null 2>&1; then BIN="$c"; break; fi
done
if [ -n "$BIN" ]; then
sudo install -m755 "$BIN" /usr/local/bin/minisign
else
echo "未找到可执行的静态二进制, 回退 apt 安装"
sudo apt-get update && sudo apt-get install -y minisign
fi
minisign -v
- name: Build & sign OTA package
# 每次主分支提交生成 OTA 升级包, sequence 默认时间戳 (单调递增)
# 发布说明 = 触发构建的 commit message (标题+正文; 打包脚本内安全截断 400 字符),
# commit sha 通过 --commit 单独写入 manifest, 前端分行展示
# 产物 ota-package/ (tar.gz + latest.json + .minisig) 进入 docker build context, 由 Dockerfile COPY 进镜像
env:
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
run: |
set -euo pipefail
# 不用 head -c 截断: 那会把多字节汉字切成非法 UTF-8, latest.json 解析失败
MSG="$(git log -1 --pretty=format:%B | sed -e '/^[[:space:]]*$/d')"
echo "notes: ${MSG}"
node scripts/build-ota.mjs --out ota-package --notes "${MSG}" --commit "${GITHUB_SHA}" --min-binary ${OTA_MIN_BINARY}
echo "===== latest.json ====="
cat ota-package/latest.json
echo "===== bundle sha256 ====="
sha256sum ota-package/iboard-ota.tar.gz
- name: Publish OTA package to server
# CI 跑在容器内, 无法直接写宿主文件系统; 用 docker cp 经bind-mount容器
# 把 ota-package 落到宿主 /app/iboard-prod/data/ota
# (该目录被生产 app 容器挂载为 /app/ota-package)。
# docker 不可用或目录缺失时仅告警, 不影响发布。
run: |
set -euo pipefail
HOST_DIR=/app/iboard-prod
if ! docker info >/dev/null 2>&1; then
echo "::warning::docker 不可用, 跳过 OTA 包同步"; exit 0
fi
docker run -d --name ota-bridge -v "${HOST_DIR}/data/ota:/dst" alpine:3.20 sleep 30 >/dev/null
docker cp ota-package/. ota-bridge:/dst/ || echo "::warning::OTA 包复制失败"
docker rm -f ota-bridge >/dev/null 2>&1 || true
echo "== 宿主 OTA 目录 =="
docker run --rm -v "${HOST_DIR}/data/ota:/d" alpine:3.20 sh -c 'ls -la /d | head -6'
docker restart iboard-prod-app-1 >/dev/null 2>&1 || true
- name: Build Docker image
# DATABASE_URL 仅在 prisma generate 阶段需要, 可用任何合法占位串
# PREBUILT_DIST=true: 复用上一步构建好的 dist/, 镜像内跳过前端构建
# DOCKER_BUILDKIT=1: Dockerfile 用了 cache mount 语法
run: docker build -t iboard:latest --build-arg DATABASE_URL=mysql://build:build@127.0.0.1:3306/build --build-arg PREBUILT_DIST=true .
env:
DOCKER_BUILDKIT: '1'
- name: List images
run: docker images
- 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'