diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a050a2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,216 @@ +name: Release + +# 推送 v 开头的 tag 时触发,例如 git tag v2.2.0 && git push origin v2.2.0 +on: + push: + tags: + - 'v*' + # 允许手动跑一次做验证(不会创建 Release,只构建产物) + workflow_dispatch: + +permissions: + contents: write # 创建 Release + packages: write # 推送镜像到 ghcr + +env: + GO_VERSION: '1.25' + NODE_VERSION: '22' + IMAGE_NAME: ${{ github.repository }} + +jobs: + # ========================================================== + # 前端只构建一次,产物给所有平台的二进制共用 + # ========================================================== + frontend: + name: 构建前端 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: web/frontend/package-lock.json + + - name: 安装依赖 + working-directory: web/frontend + run: npm ci + + - name: 类型检查与构建 + working-directory: web/frontend + run: npm run build + + - name: 上传前端产物 + uses: actions/upload-artifact@v4 + with: + name: web-dist + path: web/dist + retention-days: 1 + if-no-files-found: error + + # ========================================================== + # 交叉编译各平台二进制 + # ========================================================== + build: + name: 构建 ${{ matrix.goos }}/${{ matrix.goarch }} + needs: frontend + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: windows + goarch: amd64 + ext: .exe + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: 取回前端产物 + uses: actions/download-artifact@v4 + with: + name: web-dist + path: web/dist + + - name: 准备版本信息 + id: vars + run: | + VERSION="${GITHUB_REF_NAME}" + # 手动触发时没有 tag,用短 commit 顶替 + if [ "${GITHUB_REF_TYPE}" != "tag" ]; then + VERSION="dev-$(git rev-parse --short HEAD)" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" + + - name: 编译 + env: + CGO_ENABLED: '0' # sqlite 用纯 Go 实现,无需 CGO,可安全交叉编译 + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + go build -trimpath \ + -ldflags="-w -s \ + -X main.version=${{ steps.vars.outputs.version }} \ + -X main.buildDate=${{ steps.vars.outputs.build_date }} \ + -X main.gitCommit=${GITHUB_SHA::7}" \ + -o "goodbaby${{ matrix.ext }}" . + + - name: 打包 + id: pack + run: | + NAME="goodbaby_${{ steps.vars.outputs.version }}_${{ matrix.goos }}_${{ matrix.goarch }}" + mkdir -p "dist/${NAME}" + cp "goodbaby${{ matrix.ext }}" "dist/${NAME}/" + cp README.md LICENSE "dist/${NAME}/" + cp -r docs "dist/${NAME}/docs" + + cd dist + if [ "${{ matrix.goos }}" = "windows" ]; then + zip -qr "${NAME}.zip" "${NAME}" + echo "archive=dist/${NAME}.zip" >> "$GITHUB_OUTPUT" + else + tar -czf "${NAME}.tar.gz" "${NAME}" + echo "archive=dist/${NAME}.tar.gz" >> "$GITHUB_OUTPUT" + fi + + - name: 上传构建产物 + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.goos }}-${{ matrix.goarch }} + path: ${{ steps.pack.outputs.archive }} + retention-days: 7 + if-no-files-found: error + + # ========================================================== + # 汇总产物并创建 Release(仅 tag 触发时执行) + # ========================================================== + release: + name: 发布 Release + needs: build + runs-on: ubuntu-latest + if: github.ref_type == 'tag' + steps: + - name: 取回全部产物 + uses: actions/download-artifact@v4 + with: + pattern: release-* + path: dist + merge-multiple: true + + - name: 生成校验和 + working-directory: dist + run: | + sha256sum * > checksums.txt + cat checksums.txt + + - name: 创建 Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" dist/* \ + --repo "${GITHUB_REPOSITORY}" \ + --title "${GITHUB_REF_NAME}" \ + --generate-notes + + # ========================================================== + # 构建多架构镜像并推送 ghcr + # ========================================================== + docker: + name: 构建并推送镜像 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # 交叉编译由 Dockerfile 里的 TARGETARCH 完成, + # 这里的 QEMU 只用于最终 alpine 运行阶段的镜像组装 + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: 登录 ghcr + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 生成镜像标签 + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref_type == 'tag' }} + type=sha,format=short,enable=${{ github.ref_type != 'tag' }} + + - name: 构建并推送 + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + GIT_COMMIT=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 4ac56c5..e15ea32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ # ====================== # 前端构建阶段 (Frontend Stage) # ====================== -FROM node:22-alpine AS frontend +# 固定在构建机架构上运行:前端产物与目标架构无关, +# 否则交叉构建 arm64 镜像时 npm 要跑在 QEMU 里,慢得离谱 +FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend WORKDIR /app/web/frontend @@ -14,7 +16,9 @@ RUN npm run build # ====================== # 后端构建阶段 (Builder Stage) # ====================== -FROM golang:1.25-alpine AS builder +# 同样固定在构建机架构上,用 Go 的交叉编译产出目标架构的二进制。 +# 依赖里没有 CGO(sqlite 用的是纯 Go 实现),所以交叉编译是安全的。 +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder WORKDIR /app @@ -26,12 +30,17 @@ COPY . . # 用前端构建产物覆盖占位的 dist COPY --from=frontend /app/web/dist /app/web/dist -ARG VERSION="2.0.0" +ARG VERSION="dev" ARG BUILD_DATE="unknown" ARG GIT_COMMIT="unknown" +# TARGETARCH 由 buildx 自动注入(amd64 / arm64 / ...) +ARG TARGETARCH +ARG TARGETOS=linux RUN CGO_ENABLED=0 \ - GOOS=linux \ + GOOS=${TARGETOS} \ + GOARCH=${TARGETARCH} \ go build \ + -trimpath \ -ldflags="-w -s -X main.version=${VERSION} -X main.buildDate=${BUILD_DATE} -X main.gitCommit=${GIT_COMMIT}" \ -o /app/main . diff --git a/README.md b/README.md index 4055f32..14db7f4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,25 @@ docker compose up -d 首次启动会在 `./data` 下自动生成 `config.json` 与 `data.db`。 打开 `http://localhost:8088`,按引导创建第一个账号即可。 +### 直接拉取镜像 + +每次发版会自动构建 `linux/amd64` 与 `linux/arm64` 镜像推送到 GitHub Container Registry: + +```bash +docker run -d --name goodbaby -p 8088:8088 -v ./data:/app/data ghcr.io/ssdomei232/goodbaby:latest +``` + +可用标签:`latest`、`v2.2.0`(完整版本)、`2.2`(次版本)、`2`(主版本)。 + +### 下载预编译二进制 + +[Releases](https://github.com/ssdomei232/goodBaby/releases) 提供 Linux / Windows / macOS 的 amd64 与 arm64 产物, +解压后直接运行即可(前端已内嵌,无需额外部署)。压缩包旁的 `checksums.txt` 可校验完整性: + +```bash +sha256sum -c checksums.txt --ignore-missing +``` + ### 从源码构建 需要 Go 1.25+ 与 Node.js 20+: @@ -93,6 +112,11 @@ cd web/frontend && npm run dev 默认 SQLite,可选 PostgreSQL,详见 [docs/database.md](docs/database.md)。 +## 发版 + +推送 `v` 开头的 tag 即可自动构建产物、创建 Release 并推送镜像到 ghcr, +详见 [docs/release.md](docs/release.md)。 + ## 驱动配置 各驱动的账号 / 规则配置说明见 [docs/](docs/): diff --git a/docker-compose.yml b/docker-compose.yml index 2d0dd8b..deca929 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: goodbaby: - image: mei232/goodbaby:v2.1.0 + # 发版镜像由 GitHub Actions 自动推送到 ghcr + image: ghcr.io/ssdomei232/goodbaby:latest + # 想用本地源码构建时把上面一行注释掉,保留 build build: . ports: - "8088:8088" diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..f087f52 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,71 @@ +# 发版流程 + +发版由 [`.github/workflows/release.yml`](../.github/workflows/release.yml) 全自动完成, +触发条件是推送一个 `v` 开头的 tag。 + +## 发一个版本 + +```bash +git tag v2.2.0 +git push origin v2.2.0 +``` + +推上去之后 workflow 会依次做四件事: + +| 阶段 | 内容 | +| --- | --- | +| `frontend` | 跑一次 `npm ci && npm run build`(含 `vue-tsc` 类型检查),产物作为 artifact 共享 | +| `build` | 5 个平台交叉编译,打包成 tar.gz / zip | +| `release` | 汇总产物、生成 `checksums.txt`、创建 GitHub Release(发布说明自动生成) | +| `docker` | buildx 构建 `linux/amd64` + `linux/arm64` 镜像推送到 ghcr | + +前端只构建一次,所有平台的二进制共用同一份产物,保证各平台内嵌的前端完全一致。 + +## 产物 + +二进制覆盖: + +* `linux/amd64`、`linux/arm64` +* `windows/amd64` +* `darwin/amd64`、`darwin/arm64` + +每个压缩包内含可执行文件、`README.md`、`LICENSE` 与 `docs/`。 + +镜像标签由 tag 推导:`v2.2.0` 会产出 `2.2.0`、`2.2`、`2` 与 `latest`。 + +## 版本信息注入 + +编译时通过 ldflags 注入,启动日志里能看到: + +``` +goodBaby v2.2.0 (a1b2c3d, 2026-07-27T07:00:00Z) 启动中... +``` + +对应 `main.go` 里的 `version` / `gitCommit` / `buildDate` 三个变量。 + +## 关于交叉编译 + +项目依赖里没有 CGO——sqlite 用的是纯 Go 的 `glebarez/sqlite`——所以所有平台都能用 +`CGO_ENABLED=0` 直接交叉编译,不需要各平台的 runner。 + +Dockerfile 里前端与后端构建阶段都标了 `--platform=$BUILDPLATFORM`,配合 `TARGETARCH` +做 Go 交叉编译,因此构建 arm64 镜像时不会把 npm 和 go build 丢进 QEMU 里跑。 + +## 手动验证 + +workflow 支持 `workflow_dispatch` 手动触发。手动跑时**不会创建 Release**(`release` job 有 +`if: github.ref_type == 'tag'` 保护),只会构建产物并推一个 `sha-xxxxxxx` 标签的镜像, +适合在正式打 tag 前先验证流程。 + +## 权限 + +workflow 只用了内置的 `GITHUB_TOKEN`,不需要额外配置 secret: + +```yaml +permissions: + contents: write # 创建 Release + packages: write # 推送镜像到 ghcr +``` + +首次推送后,包默认是私有的。如果要公开,去仓库的 Packages 页面把 +`goodbaby` 这个包的可见性改成 Public。