CI/CD 是现代软件交付的命脉。手动构建、手动部署不仅效率低下,更是事故的温床——“在我机器上能跑"的悲剧几乎都源于缺乏自动化流水线。GitHub Actions 作为 GitHub 原生的 CI/CD 平台,与代码仓库无缝集成,免费额度对开源项目友好,已成为最流行的 CI/CD 工具之一。从核心概念出发,结合 Go 项目和 Hugo 站点两个实战场景,完整讲解流水线设计。
参考来源:GitHub Actions 官方文档
一、GitHub Actions 核心概念
GitHub Actions 的架构围绕五个概念展开,理解它们的关系是设计流水线的基础:
Workflow(工作流)
│
├── Job A(任务)
│ ├── Step 1 → Action: checkout 代码
│ ├── Step 2 → Action: setup Go 环境
│ └── Step 3 → Shell: go test ./...
│
└── Job B(任务)
├── Step 1 → Action: 下载构建产物
└── Step 2 → Shell: 部署到服务器
| 概念 | 说明 | 类比 |
|---|---|---|
| Workflow | 一个 .yml 文件定义的完整流水线,放在 .github/workflows/ 目录 | 一个"流水线模板” |
| Job | Workflow 内的独立任务单元,由多个 Step 组成;Job 间可串行或并行 | 流水线上的"工位" |
| Step | Job 内的最小执行单元,可以是 Shell 命令或 Action | 工位上的"操作步骤" |
| Action | 可复用的步骤单元,类似函数调用;可在 GitHub Marketplace 查找 | 可复用的"函数" |
| Runner | 执行 Job 的服务器实例,分 GitHub 托管和自托管两种 | 执行任务的"工人" |
Runner 类型选择
| Runner 类型 | 规格 | 费用 | 适用场景 |
|---|---|---|---|
| ubuntu-latest | 4 vCPU / 16GB / 14GB SSD | 公开仓库免费,私有仓库按分钟计费 | 大多数 CI 场景 |
| macos-latest | 3 vCPU / 14GB / 14GB SSD | 费用较高(10x) | iOS/macOS 构建 |
| windows-latest | 4 vCPU / 16GB / 14GB SSD | 费用较高(2x) | Windows 应用构建 |
| self-hosted | 自定义 | 自担硬件成本 | 私有环境、特殊依赖、GPU |
二、触发机制
触发器(Trigger)决定了 Workflow 何时执行。GitHub Actions 支持丰富的触发方式:
name: CI
# ── 多触发条件(满足任一即触发)──
on:
# 1. 代码推送
push:
branches:
- main
- 'release/*'
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
paths-ignore:
- '**.md'
- 'docs/**'
# 2. Pull Request
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
# 3. 定时任务(Cron 语法,UTC 时区)
schedule:
# 每天北京时间 08:00 执行(UTC 00:00)
- cron: '0 0 * * *'
# 4. 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
type: choice
options:
- staging
- production
debug_mode:
description: '启用调试'
required: false
type: boolean
default: false
# 5. 发布 Release
release:
types: [published]
paths过滤是减少不必要 CI 运行的关键。只修改文档时不应触发 Go 测试流水线,通过paths和paths-ignore可以精确控制。
三、实战 1:Go 项目 CI 流水线
这是一个生产级 Go 项目 CI 流水线,覆盖 lint、测试、构建和镜像推送:
# .github/workflows/go-ci.yml
name: Go CI
on:
push:
branches: [main]
paths: ['**.go', 'go.mod', 'go.sum', '.github/workflows/go-ci.yml']
pull_request:
branches: [main]
env:
GO_VERSION: '1.22'
REGISTRY: ghcr.io
jobs:
# ──────────────────────────────────────────
# Job 1: 代码检查
# ──────────────────────────────────────────
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.59
args: --timeout=5m
# ──────────────────────────────────────────
# Job 2: 单元测试 + 覆盖率
# ──────────────────────────────────────────
test:
name: Test
runs-on: ubuntu-latest
services:
# 启动 PostgreSQL 容器供测试使用
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download dependencies
run: go mod download
- name: Run tests with coverage
env:
DB_HOST: localhost
DB_PORT: 5432
DB_PASSWORD: testpass
DB_NAME: testdb
run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV_TOKEN }}
# ──────────────────────────────────────────
# Job 3: 多平台构建 + 镜像推送
# ──────────────────────────────────────────
build:
name: Build & Push
needs: [lint, test]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
target:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Build binary
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.target.goos }}
GOARCH: ${{ matrix.target.goarch }}
run: |
LDFLAGS="-s -w -X main.Version=${GITHUB_REF_NAME} -X main.Commit=${GITHUB_SHA::8}"
go build -ldflags="$LDFLAGS" -o app-${{ matrix.target.goos }}-${{ matrix.target.goarch }} ./cmd/server
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/${{ matrix.target.goarch }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
流水线设计要点
push / pull_request
│
▼
┌─────────┐ ┌─────────┐
│ Lint │ │ Test │ ← 并行执行,互不阻塞
└────┬────┘ └────┬────┘
│ │
└──────┬────────┘
│ needs: [lint, test]
▼
┌─────────────┐
│ Build │ ← matrix 并行构建 amd64 + arm64
│ (x2 jobs) │
└─────────────┘
│
▼
推送镜像到 GHCR
关键设计决策:
lint和test并行执行,缩短流水线耗时build通过needs: [lint, test]确保前置检查通过才构建matrix策略并行构建linux/amd64和linux/arm64,支持 ARM 服务器- 使用
docker/metadata-action自动生成语义化镜像标签 cache-from/cache-to: type=gha利用 GitHub Actions 缓存加速 Docker 构建
四、实战 2:Hugo 站点 CD 流水线
本文所在的 SRE 学习笔记站点就是用 Hugo 构建的。下面是一个完整的 Hugo 站点部署流水线,支持 GitHub Pages 和 VPS 两种部署目标:
# .github/workflows/hugo-deploy.yml
name: Hugo Deploy
on:
push:
branches: [main]
paths:
- 'content/**'
- 'static/**'
- 'layouts/**'
- 'config.toml'
- 'hugo.toml'
- '.github/workflows/hugo-deploy.yml'
workflow_dispatch:
inputs:
target:
description: '部署目标'
required: true
type: choice
options:
- github-pages
- vps
- both
# 设置 Workflow 级权限
permissions:
contents: read
pages: write
id-token: write
# 同一时间只允许一个部署并发
concurrency:
group: pages
cancel-in-progress: false
env:
HUGO_VERSION: '0.129.0'
jobs:
# ──────────────────────────────────────────
# Job 1: 构建 Hugo 站点
# ──────────────────────────────────────────
build:
name: Build Hugo Site
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true # 获取 Hugo 主题子模块
fetch-depth: 0 # 获取完整 Git 历史(启用 lastmod)
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: ${{ env.HUGO_VERSION }}
extended: true # PaperMod 主题需要 extended 版本
- name: Setup Go cache
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-hugo-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-hugo-
- name: Build
run: |
hugo \
--minify \
--baseURL "https://www.sre.wang/" \
--enableGitInfo
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
# ──────────────────────────────────────────
# Job 2: 部署到 GitHub Pages
# ──────────────────────────────────────────
deploy-pages:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
if: >-
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.target == 'github-pages' ||
github.event.inputs.target == 'both'))
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
# ──────────────────────────────────────────
# Job 3: 部署到 VPS(通过 rsync over SSH)
# ──────────────────────────────────────────
deploy-vps:
name: Deploy to VPS
needs: build
runs-on: ubuntu-latest
if: >-
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.target == 'vps' ||
github.event.inputs.target == 'both'))
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: github-pages
- name: Deploy via rsync
uses: burnett01/rsync-deployments@7.0.1
with:
switches: -avzr --delete
path: ./
remote_path: /var/www/sre.wang/
remote_host: ${{ secrets.VPS_HOST }}
remote_user: ${{ secrets.VPS_USER }}
remote_key: ${{ secrets.VPS_SSH_KEY }}
CD 流水线设计要点
| 设计点 | 说明 |
|---|---|
| paths 过滤 | 仅在内容/布局/配置变更时触发,避免无意义构建 |
| concurrency | cancel-in-progress: false 确保部署不被中断,同一时间只跑一个 |
| artifact 传递 | build 上传产物,deploy 下载产物,实现构建与部署解耦 |
| 双部署目标 | 通过 workflow_dispatch 输入参数选择部署到 Pages/VPS/两者 |
| environment 保护 | environment: github-pages 可配置审批人、环境变量和部署日志 |
五、缓存策略:加速构建
缓存是缩短 CI 耗时的最有效手段。GitHub Actions 提供了 actions/cache 和多种内置缓存机制:
5.1 Go 模块缓存
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true # setup-go 内置缓存,自动缓存 ~/go/pkg/mod 和 ~/.cache/go-build
actions/setup-go@v5自带缓存功能,无需手动配置actions/cache。它自动以go.sum文件哈希作为缓存键。
5.2 通用缓存模式
- name: Cache Node modules
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
5.3 缓存策略对比
| 缓存方式 | 适用场景 | 优势 | 局限 |
|---|---|---|---|
actions/setup-go cache | Go 项目 | 零配置,自动管理 | 仅限 Go 模块 |
actions/cache@v4 | 通用文件缓存 | 灵活,支持任意路径 | 需手动管理缓存键 |
cache-from: type=gha | Docker 构建 | 缓存 Docker 层 | 有大小限制(10GB) |
actions/cache/restore | 部分恢复 | 不在命中时不写缓存 | 需配合 actions/cache/save |
缓存键设计原则
# 缓存键的三层设计:精确匹配 → 部分匹配 → 系统级兜底
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} # 精确:依赖文件变了就新建缓存
restore-keys: |
${{ runner.os }}-go- # 部分:依赖文件变了也能恢复大部分缓存
${{ runner.os }}- # 兜底:至少恢复系统级缓存
缓存键的设计直接影响命中率。
hashFiles保证依赖变更时新建缓存,restore-keys的渐进式匹配确保即使精确键未命中,也能恢复大部分缓存内容。
六、密钥管理:GitHub Secrets 与 OIDC
6.1 GitHub Secrets
GitHub Secrets 是存储敏感信息的标准方式。在仓库 Settings → Secrets and variables → Actions 中添加:
steps:
- name: Deploy to production
env:
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} # 数据库密码
API_KEY: ${{ secrets.API_KEY }} # 第三方 API Key
DEPLOY_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} # SSH 私钥
run: ./deploy.sh
Secrets 使用规则:
| 规则 | 说明 |
|---|---|
| 自动脱敏 | Secret 值在日志中自动替换为 *** |
| 环境隔离 | 通过 environment 级 Secrets 实现不同环境的密钥隔离 |
| 组织级共享 | Organization 级 Secrets 可在多个仓库间共享 |
| 不可在 if 中引用 | Secrets 不能用于 if 条件判断(出于安全考虑) |
6.2 OIDC 无密钥认证
传统方式中,CI 需要长期有效的云厂商 Access Key 来部署资源。这些密钥一旦泄露,攻击者可以完全控制你的云账户。OIDC(OpenID Connect) 通过短期令牌替代长期密钥,是更安全的方案。
# Go CI 中使用 OIDC 推送镜像到 AWS ECR
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # 必须启用,用于获取 OIDC 令牌
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: ap-northeast-1
- name: Login to ECR
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest
OIDC vs 长期密钥对比:
传统方式(长期密钥):
GitHub Secrets → AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
┌──────────┐ ┌──────────┐
│ CI Job │ ──key──→│ AWS │ 密钥泄露 = 账户沦陷
└──────────┘ └──────────┘
OIDC 方式(短期令牌):
┌──────────┐ OIDC Token ┌──────────┐ AssumeRole ┌──────────┐
│ CI Job │ ──────────→ │ AWS IAM │ ───────────→ │ AWS │
└──────────┘ (JWT, 1h) └──────────┘ (临时凭证) └──────────┘
无需存储任何长期密钥 信任关系预先配置 令牌 1 小时过期
| 维度 | 长期密钥 | OIDC |
|---|---|---|
| 密钥存储 | GitHub Secrets 中存储 Access Key | 无需存储任何密钥 |
| 泄露风险 | 密钥泄露后需手动轮换 | 令牌 1 小时自动过期 |
| 权限控制 | 密钥权限 = IAM 用户权限 | 通过 IAM Role 精确控制 |
| 审计能力 | 难以区分不同 Job 的操作 | 每次 AssumeRole 都有审计日志 |
| 配置复杂度 | 低(仅需配置密钥) | 中(需配置 IAM 信任关系) |
6.3 IAM 信任策略配置
在 AWS 端配置 IAM Role 的信任关系,允许 GitHub Actions 通过 OIDC 认证:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:lorock/sre.wang:ref:refs/heads/main"
}
}
}
]
}
Condition中的sub字段限制只有lorock/sre.wang仓库的main分支才能 AssumeRole,防止其他仓库冒用身份。这是 OIDC 安全模型的核心——通过信任策略精确控制"谁能在什么条件下扮演什么角色"。
七、流水线设计好的实践
| 维度 | 实践要点 |
|---|---|
| 触发控制 | 用 paths 过滤避免无关变更触发 CI;concurrency 防止并发部署 |
| Job 编排 | 独立检查并行执行;有依赖关系的 Job 用 needs 串行 |
| 构建矩阵 | 用 matrix 并行构建多平台/多版本产物 |
| 缓存优化 | 优先使用 Action 内置缓存;手动缓存用 hashFiles 做缓存键 |
| 密钥安全 | 敏感信息存 Secrets;云资源部署优先用 OIDC 无密钥认证 |
| 环境保护 | 生产部署使用 environment 配置审批人和部署日志 |
| 产物传递 | Job 间用 upload-artifact / download-artifact 传递构建产物 |
| 失败快速 | 把最可能失败的 Job(如 lint)放前面,用 needs 让后续 Job 依赖它 |
| 可观测性 | 关键步骤加 name;用 if: always() 确保通知 Job 总是执行 |
八、完整目录结构参考
.github/
├── workflows/
│ ├── go-ci.yml # Go 项目 CI 流水线
│ ├── hugo-deploy.yml # Hugo 站点部署流水线
│ ├── release.yml # 发布流水线(tag 触发)
│ └── scheduled-check.yml # 定期安全扫描
└── actions/ # 自定义 Composite Action(可选)
└── setup-env/
└── action.yml
CI/CD 流水线的核心价值不在于自动化本身,而在于建立信心——每次代码变更都经过统一的检查和验证,减少了"在我的机器上能跑"的不确定性。从本文的 Go CI 和 Hugo CD 两个实战出发,你可以根据自己项目的语言和部署目标,灵活组合触发器、Job 编排、缓存和密钥策略,构建出适合团队的流水线。
延伸阅读:
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- GitHub Actions 官方文档 — GitHub 开源社区,参考了GitHub Actions 官方文档相关内容
- GitHub Marketplace — GitHub 开源社区,参考了GitHub Marketplace相关内容
- Actions 安全加固指南 — GitHub 开源社区,参考了Actions 安全加固指南相关内容
- OIDC 云部署安全配置 — GitHub 开源社区,参考了OIDC 云部署安全配置相关内容