第 10 章 · GitHub Actions 与 CI
一句话:GitHub 自带的自动化服务,按仓库里
.github/workflows/下的 YAML 文件,在推送、PR、打标签或定时的时候自动跑检查、测试、构建和发布。 用它还是用别的:代码放在 GitHub 上就用它,公开仓库免费、不用另搭服务器;代码在 GitLab 或 Gitee 上,用各自的 GitLab CI/CD、Gitee Go,对照见第 9 章 9.5。 核对版本:actions/checkout v7、setup-python v7、setup-node v7,2026-10
10.1 安装与版本
Section titled “10.1 安装与版本”不用安装。在仓库根目录建 .github/workflows/ 目录,往里放一个 .yml 文件,提交并推送到 GitHub,Actions 就开始按文件里写的条件运行。
mkdir -p .github/workflowsNew-Item -ItemType Directory -Force .github/workflows文件写好后照常提交推送:
git add .github/workflows/ci.ymlgit commit -m "ci: 加 CI 工作流"git push推送后在仓库页面的 Actions 标签看运行结果;在终端里看用第 9 章装的 gh:
gh run list想不推送就在本机试跑,可以装 act(winget install nektos.act、brew install act),它用 Docker 容器模拟运行器。容器镜像和 GitHub 的运行器装的软件不一样,缓存、产物、GITHUB_TOKEN 也是模拟的,本地能过不代表 GitHub 上能过,最终以 GitHub 上的结果为准。
10.2 五分钟概念
Section titled “10.2 五分钟概念”- 持续集成(Continuous Integration,CI):每次推送或发 PR,自动把代码拉到一台干净的机器上跑检查和测试,有问题当场在 PR 上标红,不用等合并之后才发现。
- 工作流(Workflow)、作业(Job)、步骤(Step):一个 YAML 文件是一个工作流;工作流里有一个或多个作业,作业之间默认同时开跑,写了
needs才排队;作业里的步骤从上到下依次执行,一步失败后面的步骤都不跑。 - 触发事件(
on):什么时候运行这个工作流。最常用的是push、pull_request、手动点按钮的workflow_dispatch和定时的schedule,还能限定分支、标签和路径。 - 运行器(Runner):执行作业的机器,
runs-on: ubuntu-latest就是要一台 GitHub 托管的 Ubuntu 虚拟机,另有windows-latest、macos-latest。每个作业都拿到一台全新的机器,上一个作业装的东西、生成的文件下一个作业看不到。 - Action:别人写好的、可复用的步骤,用
uses: 所有者/仓库@版本引用,比如actions/checkout负责把仓库代码取到运行器上。自己的命令用run:写。 - 机密(Secrets)与变量(Variables):密码、令牌这类值存在仓库设置里,工作流里用
${{ secrets.名字 }}取,日志里显示成***;不敏感的配置存成变量,用${{ vars.名字 }}取。每次运行还自动发一个GITHUB_TOKEN,权限由permissions:决定。 - 矩阵(Matrix):同一个作业按一组值展开成多份,比如两个 Python 版本各跑一遍,每份是独立的作业,并行运行。
- 缓存(Cache)与产物(Artifact):缓存是在多次运行之间复用的依赖目录,省下每次重新下载的时间;产物是这次运行生成、要留下来给人下载或给后面作业用的文件,比如构建出的安装包和测试报告。
下图是 10.4 Python 模板那种结构加一个构建作业:推送或 PR 触发工作流,lint 和 test 两个作业各占一台运行器同时跑,build 写了 needs: [lint, test],等两个都成功才开始,任一失败它就被跳过。
10.3 常用命令
Section titled “10.3 常用命令”本章的“命令”是工作流文件里的 YAML 写法。为了一行放得下,表里用了 YAML 的花括号写法,{ push: { branches: [main] } } 与分行缩进的写法完全等价,抄进文件时用哪种都行;10.4 的模板是分行写法。
| 任务 | 写法 | 说明 |
|---|---|---|
| 触发 | ||
| 推送和 PR 都触发 | on: [push, pull_request] |
推任何分支、任何标签都会跑 |
| 只在某个分支推送时 | on: { push: { branches: [main] } } |
分支名可用通配符,如 release/** |
| 只在某些文件改了时 | on: { push: { paths: ['src/**', 'pyproject.toml'] } } |
和 branches 同时写时两个条件都满足才跑 |
| PR 时跑 | on: { pull_request: { branches: [main] } } |
这里的 branches 指 PR 要合并进的目标分支 |
| 推送标签时跑 | on: { push: { tags: ['v*'] } } |
只写 tags 时,推送分支不再触发 |
| 手动运行 | on: workflow_dispatch |
文件在默认分支上,Actions 页才出现“运行工作流”按钮 |
| 定时运行 | on: { schedule: [ { cron: '0 1 * * *' } ] } |
默认按 UTC,这一行是北京时间每天 9:00;最短间隔 5 分钟 |
| 定时按本地时区 | - { cron: '0 9 * * *', timezone: Asia/Shanghai } |
写在 schedule: 下,填 IANA 时区名 |
| 作业 | ||
| 指定运行器 | runs-on: ubuntu-latest |
另有 windows-latest、macos-latest,私有仓库里后两者更费额度 |
| 等别的作业做完 | needs: [lint, test] |
被依赖的作业任一失败,这个作业跳过 |
| 按条件执行 | if: github.ref == 'refs/heads/main' |
作业或步骤上都能写,这里是只在 main 上执行 |
| 设超时 | timeout-minutes: 15 |
默认 360 分钟,卡住的作业会一直耗额度 |
| 新推送取消旧运行 | concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true } |
同一分支连推几次,只跑最后一次 |
| 步骤 | ||
| 取出仓库代码 | - uses: actions/checkout@v7 |
几乎每个作业的第一步;默认只取触发的那一个提交 |
| 给 Action 传参数 | with: { fetch-depth: 0 } |
写在 uses: 下面,参数名查该 Action 的 README;这一项是取全部历史 |
| 执行一条命令 | - run: npm test |
命令返回非 0 时这一步失败 |
| 一步执行多行命令 | run: | 后面每行缩进写一条 |
Linux 和 macOS 上用 bash 执行,任一行失败整步失败 |
| 在子目录执行 | working-directory: ./web |
写在 run: 那一步里,目录要已存在 |
| 设环境变量 | env: { NODE_ENV: production } |
工作流、作业、步骤三级都能写,内层覆盖外层 |
| 语言环境 | ||
| Python | uses: actions/setup-python@v7 加 with: { python-version: '3.14', cache: pip } |
版本号加引号,否则 3.10 会被读成 3.1 |
| Node.js | uses: actions/setup-node@v7 加 with: { node-version: 24, cache: npm } |
缓存要求仓库里提交了 package-lock.json |
| .NET | uses: actions/setup-dotnet@v6 加 with: { dotnet-version: 10.0.x, cache: true } |
开缓存要先提交 packages.lock.json,没有会报错 |
| Go | uses: actions/setup-go@v7 加 with: { go-version-file: go.mod, cache: true } |
Go 默认就开缓存,键取自 go.mod |
| 矩阵 | ||
| 多个版本各跑一遍 | strategy: { matrix: { python-version: ['3.13', '3.14'] } } |
步骤里用 ${{ matrix.python-version }} 取当前值 |
| 一个失败不取消其他 | strategy: { fail-fast: false, matrix: { os: [ubuntu-latest, windows-latest] } } |
默认 true,一个失败就取消其余,看不到别的版本结果 |
| 机密与权限 | ||
| 读机密 | env: { API_KEY: '${{ secrets.API_KEY }}' } |
在哪设见 10.5;从 fork 发来的 PR 拿不到 |
| 令牌只给读权限 | permissions: { contents: read } |
写在文件顶层;写了任一项,没写的权限都变成无 |
| 某个作业要写权限 | permissions: { contents: write } |
写在该作业下,建发布、推提交时需要 |
| 产物与缓存 | ||
| 上传产物 | uses: actions/upload-artifact@v7 加 with: { name: dist, path: dist/ } |
在运行摘要页底部下载,默认保留 90 天 |
| 自定义缓存 | uses: actions/cache@v6 加 with: { path: ~/.cache/pre-commit, key: "pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}" } |
key 不变就命中;setup 系列已带缓存时不用再写 |
| 查看与重跑 | ||
| 网页上重跑 | Actions → 点进那次运行 → 右上角“重新运行作业” | 30 天内可重跑全部作业或只重跑失败的 |
| 命令行重跑失败的作业 | gh run rerun <运行编号> --failed |
编号是 gh run list 输出里的 ID 列 |
| 看失败步骤的日志 | gh run view <运行编号> --log-failed |
只输出失败步骤,不用翻整份日志 |
| 命令行手动触发 | gh workflow run <文件名>.yml |
工作流里要有 workflow_dispatch |
10.4 典型工作流
Section titled “10.4 典型工作流”三个模板都可以整份复制,存进 .github/workflows/ 就能用。
Python 项目:Ruff 检查加两个版本的 pytest
Section titled “Python 项目:Ruff 检查加两个版本的 pytest”name: Python CI
on: push: branches: [main] pull_request:
permissions: contents: read
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: '3.14' - run: pip install ruff - run: ruff check . - run: ruff format --check .
test: runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: python-version: ['3.13', '3.14'] steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: pip - run: pip install -r requirements.txt pytest - run: pytest推送到 main 和每个 PR 都会跑;功能分支只推送、不发 PR 时不跑,这样同一个提交不会在推送和 PR 上各跑一遍。
- 存成
.github/workflows/python-ci.yml;依赖只写在pyproject.toml里的项目,把安装那一行换成:pip install -e . pytest。 - Ruff 的版本要和第 11 章
.pre-commit-config.yaml里的rev一致,否则本地通过的代码 CI 可能报错,把安装那一行写死版本:pip install ruff==<版本>。 - 推送前在本机跑一遍同样的三条检查:
ruff check . && ruff format --check . && pytest。 - 推送后盯着这次运行到结束:
gh run watch。 - 发 PR 后,PR 页会出现
lint、test (3.13)、test (3.14)三个检查,按第 9 章 9.5 把它们设成必需检查;检查名用这条看:gh pr checks。
Node 项目:安装、lint、测试、构建
Section titled “Node 项目:安装、lint、测试、构建”name: Node CI
on: push: branches: [main] pull_request:
permissions: contents: read
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
jobs: build: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 cache: npm - run: npm ci - run: npx prettier --check . - run: npx eslint . - run: npm test - run: npm run build - uses: actions/upload-artifact@v7 with: name: dist path: dist/- 存成
.github/workflows/node-ci.yml;npm ci和cache: npm都要求package-lock.json已提交,没有就先生成:npm install。 - Prettier 和 ESLint 要装在项目的开发依赖里(装法和配置见第 11 章),CI 用的才是和本地相同的版本,确认一下:
npm ls prettier eslint。 package.json的scripts里要有test和build;构建输出不在dist/的,改最后的path:。推送前本机跑一遍:npm test && npm run build。- 推送后看结果,构建出的
dist在运行摘要页底部下载:gh run view --web。
推送 v 开头的标签时自动发布
Section titled “推送 v 开头的标签时自动发布”name: Release
on: push: tags: ['v*']
permissions: contents: write
jobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: '3.14' - run: pip install build - run: python -m build - uses: softprops/action-gh-release@v3 with: files: dist/* generate_release_notes: true这里以 Python 包为例,python -m build 在 dist/ 下生成 .whl 和 .tar.gz;别的语言把这两步换成自己的构建命令,让产物落在 dist/ 即可。
- 存成
.github/workflows/release.yml,permissions: contents: write不能省,否则建发布时报权限不够(见 10.6)。 - 回到最新的
main打标签:git tag -a v1.0.0 -m "<说明>"。 - 推送标签,工作流随之开始:
git push origin v1.0.0。 - 不想用第三方 Action,可以把最后一步换成 gh 命令,再给这一步加
env: { GH_TOKEN: '${{ github.token }}' }:gh release create "$GITHUB_REF_NAME" dist/* --generate-notes。 - 运行结束后检查发布页,附件和自动生成的说明都在:
gh release view v1.0.0 --web。
10.5 配置要点
Section titled “10.5 配置要点”工作流文件只认一个位置:仓库根目录下的 .github/workflows/,扩展名 .yml 或 .yaml,子目录里的不算。其余设置都在仓库网页的 Settings 里:
| 配置项 | 在哪设 | 说明 |
|---|---|---|
| 工作流文件 | .github/workflows/*.yml |
一个文件一个工作流,文件名随意 |
| 仓库机密 | Settings → Secrets and variables → Actions → 机密 → 新建存储库机密 | 存好后只能覆盖、不能查看原值 |
| 仓库变量 | 同一页的“变量”标签 | 不敏感的配置,如部署地址 |
GITHUB_TOKEN 默认权限 |
Settings → Actions → General → 工作流权限 | 选只读那一项,个别作业要写再用 permissions: 加 |
| 允许运行哪些 Action | Settings → Actions → General → 操作权限 | 可以只允许 GitHub 官方和指定的 Action |
| 产物和日志保留天数 | Settings → Actions → General 里的保留期限 | 默认 90 天;2026 年 10 月起这个期限也管检查和运行记录,别为省存储调得太短 |
| CI 通过才能合并 | 保护分支或规则集里的“合并前需要通过状态检查” | 见第 9 章 9.5,检查名就是作业名,矩阵作业带括号里的值 |
真正值得改的三项:
- 默认令牌改成只读:个人账号下新建的仓库默认已是只读,组织里的仓库跟随组织设置,老仓库可能还是读写,去“工作流权限”看一眼。只读之后,哪个工作流要写,就在文件里用
permissions:单独给。 - 要求 CI 通过才能合并:工作流至少在 PR 上跑过一次,检查名才会出现在保护分支的列表里,所以先推一个 PR 跑一遍,再去设第 9 章 9.5 的那一项。
- 用
concurrency避免重复跑:同一分支连推几次,旧的运行自动取消,省时间也省私有仓库的额度。每个模板顶部那三行照抄即可。
最小骨架,每行的作用写在注释里:
name: CI # Actions 页左侧显示的名字on: [push, pull_request] # 推送和 PR 时都触发permissions: # 收紧这次运行的 GITHUB_TOKEN contents: read # 只能读代码,不能写jobs: # 下面每个键是一个作业 test: # 作业 ID,needs 里引用的就是它 runs-on: ubuntu-latest # 在 GitHub 托管的 Ubuntu 上跑 steps: # 从上到下依次执行 - uses: actions/checkout@v7 # 把仓库代码取到运行器上 - run: echo "换成你的构建和测试命令" # 任何 shell 命令10.6 注意事项
Section titled “10.6 注意事项”- 工作流文件推上去了,Actions 页里没有运行记录。 原因:文件不在根目录的
.github/workflows/下,或推送的分支不在branches里。处理:核对路径和on:;YAML 写错时 Actions 页会显示Invalid workflow file和出错行号,按行号改缩进。 - 步骤报
Resource not accessible by integration。 原因:GITHUB_TOKEN的权限不够,比如默认只读却要建发布、评论 PR。处理:在工作流或该作业下加permissions:,如contents: write、pull-requests: write,只加需要的那项。 - 来自 fork 的 PR 上,用到机密的步骤失败,日志里那个值是空的。 原因:安全设计,fork 的 PR 拿不到机密,
GITHUB_TOKEN也只读。处理:让这类步骤在 fork PR 上跳过;别为了拿机密改用pull_request_target,它会用机密运行别人提交的代码。 - 定时任务没在设定的时间跑,或者过一阵就不跑了。 原因:cron 默认按 UTC;整点前后负载高会延迟甚至丢弃;公开仓库 60 天没有活动,定时工作流被自动停用。处理:换算成 UTC 或加
timezone:,避开整点;到 Actions 页重新启用。 - 每次都显示
Cache not found for input keys。 原因:key里用了每次运行都变的值,比如github.sha、日期。处理:key只用runner.os和依赖锁文件的hashFiles(...),锁文件不变就命中。 - 同一脚本在
windows-latest上报The term 'export' is not recognized。 原因:Windows 运行器上run:默认用 PowerShell(pwsh)执行。处理:在该步骤加shell: bash,或在作业下写defaults: { run: { shell: bash } }。 - 日志里出现了密钥原文。 原因:机密只在原样出现时被替换成
***,经过 Base64 编码、拼接或截取后的值不会被遮挡。处理:别echo机密;派生出的敏感值先echo "::add-mask::<值>";已泄露的删掉日志并立刻换掉密钥。 - 私有仓库的作业排队后不开始,提示和账单或支出限额有关。 原因:当月免费分钟数用完了(GitHub Free 每月 2,000 分钟),公开仓库用标准运行器不计费。处理:到账单设置看用量;少用 Windows、macOS 运行器,加
concurrency和paths减少无用运行。
10.7 深入阅读
Section titled “10.7 深入阅读”- GitHub Actions 文档:入门教程、各种语言的构建测试示例和运行器说明,有中文。
- 工作流语法:工作流文件里每个键的写法和取值,查 on、jobs、strategy 到这里。
- 安全使用指南:机密、GITHUB_TOKEN 权限、第三方 Action 和脚本注入的安全做法。
- Actions 市场:按用途搜别人写好的 Action,用之前看它的 README 和维护情况。
- 官方工作流模板:各种语言和框架的现成工作流,Actions 页“新建工作流”里列出的就是这些。