gitli 使用两个完全独立、触发条件互斥的 GitHub Actions 工作流:
| 文件 | 名称 | 触发 | 职责 |
|---|---|---|---|
.github/workflows/ci.yml |
CI | 分支 push(main)/ PR / 手动,显式排除 tag |
只做测试与验证 |
.github/workflows/cd.yml |
Release | 仅 v* tag / 手动 |
只做全量构建、打包、发布 |
两者不共享 job、不互相依赖、使用不同的 concurrency group:CI 为 ci-${{ github.ref }},Release 为 release-${{ github.ref }},互不阻塞。CI 只读(permissions: contents: read),Release 需要写权限(contents: write、packages: write)。
触发条件
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: ['**'] # 显式排除 tag,与 Release 互斥
pull_request:
branches: [main]tags-ignore: ['**'] 保证打 tag 时只跑 cd.yml,不会重复触发 CI。
执行步骤(单 job test,ubuntu-latest)
- Checkout(
fetch-depth: 1)。 - Setup Go
1.22.x,开启 module 缓存。 - 缓存
~/.cache/go-build(key 含**/*.go与go.sum哈希)。 gofmt -l .检查(有输出即失败)。go vet ./...。go test ./... -race -coverprofile=coverage.out。- 上传覆盖率 artifact(保留 7 天)。
- Setup Node
20(缓存web/package-lock.json)。 - 前端构建:
npm ci && npm run build。 - 静态编译验证(
CGO_ENABLED=0 -trimpath -ldflags="-s -w"),验证 embed 正常。 - 交叉编译检查(
linux/arm64)。 - 静态性校验(
file/ldd判定静态链接)。 golangci-lint(存在.golangci.yml时执行)。
CI 不发布、不推镜像、不上传构建产物(仅覆盖率 artifact)。提交代码或开 PR 后约 2 分钟内得出结果。
触发条件
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag: { description: 'Tag to release (e.g. v1.0.0)', required: true }Release 不跑单元测试(测试由 CI 负责)。
构建前端产物 web/dist,作为 artifact web-dist(保留 1 天)供后续分片下载。
needs: frontend,fail-fast: false。把 46 个平台/架构目标拆到 5 个 matrix 分片并行编译,2 核 runner 也能高效利用:
| 分片 | 目标 |
|---|---|
| 1 | linux/amd64、linux/arm64、linux/armv6、linux/armv7 |
| 2 | linux/386、riscv64、loong64、ppc64le、s390x、mips、mipsle、mips64、mips64le、ppc64 |
| 3 | darwin/amd64、darwin/arm64、windows/amd64、windows/arm64、windows/386 |
| 4 | freebsd(386/amd64/arm/arm64)、openbsd(386/amd64/arm/arm64/ppc64/riscv64)、netbsd(386/amd64/arm/arm64) |
| 5 | android(386/amd64/arm/arm64)、aix/ppc64、dragonfly/amd64、illumos/amd64、solaris/amd64、plan9(386/amd64/arm)、js/wasm、wasip1/wasm |
每个分片:
- 下载
web-dist到web/dist。 - 设置
VERSION=${{ github.ref_name }}、COMMIT=${{ github.sha }}、DATE,调用scripts/build-targets.sh构建该分片目标,armvN自动映射为GOARCH=arm GOARM=N,Windows 产物加.exe。 - 每个二进制生成
.sha256校验文件。 - 上传 artifact
binaries-<shard>。
needs: build。合并所有分片 artifact 后打包发布:
fetch-depth: 0(GoReleaser 需要完整历史生成 changelog)。- 下载并合并
binaries-*到dist/。 - 把二进制重命名为
<name>_<version>形式放入release/。 docker/setup-qemu-action+buildx,登录ghcr.io。- 运行
goreleaser release --clean --skip=build。
关键:--skip=build。 二进制已由分片构建,GoReleaser 只做打包与发布,避免重复编译。.goreleaser.yml 中 builds[].skip: true 印证此模式,ldflags 仍保留以便本地 goreleaser build --snapshot 校验。
GoReleaser 产出:
- 归档:默认
tar.gz;windows覆盖为zip;命名gitli_<Version>_<Os>_<Arch>[vN],内含LICENSE、README.md、config.example.toml。 - 校验和:
SHA256SUMS(sha256)。 - SBOM:基于 archive 生成。
- 系统包(nfpms):
deb、rpm、apk,安装到/usr/local/bin,附带/etc/gitli/config.example.toml与 systemd 单元。 - 多架构镜像:推送到
ghcr.io/LiStudioorg/gitli,构建amd64、arm64、armv7分别打 tag,再合并为多架构 manifest<version>与latest。 - GitHub Release:
draft: false,prerelease: auto(含预发布标识的 tag 自动标记为 prerelease)。
changelog:基于 GitHub,按提交类型生成,自动过滤 docs:、test:、chore:、ci: 前缀。
两个工作流统一使用以下缓存,缩短构建时间:
| 缓存 | 路径 | 用途 |
|---|---|---|
| Go module | ~/go/pkg/mod(setup-go 内建 cache: true) |
依赖下载 |
| Go build | ~/.cache/go-build |
增量编译 |
| 前端 | web/node_modules(setup-node,cache: npm) |
前端依赖 |
| Docker layer | type=gha(buildx) |
镜像层复用 |
缓存 key 以 **/*.go、go.sum(或 web/package-lock.json)内容哈希计算,配合 restore-keys 兜底。
- 触发分层:分支/PR 只做验证,tag 才全量构建——单次 Release 目标控制在 8 分钟以内。
- 分片并行:46 个目标均摊到 5 个 runner,避免单机串行编译。
- 避免重复编译:分片出二进制,最终 job 只打包(
--skip=build)。 - 职责隔离:CI 与 Release 互斥触发、不同并发组,任一失败不影响另一条流水线。
# CI 等价检查
gofmt -l .
go vet ./...
go test ./... -race
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o gitli .
cd web && npm ci && npm run build && cd ..
# 单目标构建(等价于分片脚本的一部分)
VERSION=v0.1.0 COMMIT=$(git rev-parse --short HEAD) DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
bash scripts/build-targets.sh "linux/amd64,linux/arm64" dist
# 打包(需安装 goreleaser,且 dist/ 已有匹配二进制)
goreleaser release --clean --skip=build本地沙箱无
goreleaser/docker时无法完整复现 Release 的打包与镜像推送;完整链路依赖 GitHub Actions 云端环境。