跳转到内容

第 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

不用安装。在仓库根目录建 .github/workflows/ 目录,往里放一个 .yml 文件,提交并推送到 GitHub,Actions 就开始按文件里写的条件运行。

终端窗口
mkdir -p .github/workflows
终端窗口
New-Item -ItemType Directory -Force .github/workflows

文件写好后照常提交推送:

终端窗口
git add .github/workflows/ci.yml
git 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 上的结果为准。

  • 持续集成(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],等两个都成功才开始,任一失败它就被跳过。

push 或 pull_request 触发 ci.yml 工作流;lint 和 test 两个作业各在一台 ubuntu-latest 运行器上并行执行自己的步骤;build 作业 needs 前两个,二者都成功后才运行

本章的“命令”是工作流文件里的 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

三个模板都可以整份复制,存进 .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 上各跑一遍。

  1. 存成 .github/workflows/python-ci.yml;依赖只写在 pyproject.toml 里的项目,把安装那一行换成:pip install -e . pytest。
  2. Ruff 的版本要和第 11 章 .pre-commit-config.yaml 里的 rev 一致,否则本地通过的代码 CI 可能报错,把安装那一行写死版本:pip install ruff==<版本>。
  3. 推送前在本机跑一遍同样的三条检查:ruff check . && ruff format --check . && pytest。
  4. 推送后盯着这次运行到结束:gh run watch。
  5. 发 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/
  1. 存成 .github/workflows/node-ci.yml;npm ci 和 cache: npm 都要求 package-lock.json 已提交,没有就先生成:npm install。
  2. Prettier 和 ESLint 要装在项目的开发依赖里(装法和配置见第 11 章),CI 用的才是和本地相同的版本,确认一下:npm ls prettier eslint。
  3. package.json 的 scripts 里要有 test 和 build;构建输出不在 dist/ 的,改最后的 path:。推送前本机跑一遍:npm test && npm run build。
  4. 推送后看结果,构建出的 dist 在运行摘要页底部下载:gh run view --web。
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/ 即可。

  1. 存成 .github/workflows/release.yml,permissions: contents: write 不能省,否则建发布时报权限不够(见 10.6)。
  2. 回到最新的 main 打标签:git tag -a v1.0.0 -m "<说明>"。
  3. 推送标签,工作流随之开始:git push origin v1.0.0。
  4. 不想用第三方 Action,可以把最后一步换成 gh 命令,再给这一步加 env: { GH_TOKEN: '${{ github.token }}' }:gh release create "$GITHUB_REF_NAME" dist/* --generate-notes。
  5. 运行结束后检查发布页,附件和自动生成的说明都在:gh release view v1.0.0 --web。

工作流文件只认一个位置:仓库根目录下的 .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 命令
  • 工作流文件推上去了,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 减少无用运行。
  • GitHub Actions 文档:入门教程、各种语言的构建测试示例和运行器说明,有中文。
  • 工作流语法:工作流文件里每个键的写法和取值,查 on、jobs、strategy 到这里。
  • 安全使用指南:机密、GITHUB_TOKEN 权限、第三方 Action 和脚本注入的安全做法。
  • Actions 市场:按用途搜别人写好的 Action,用之前看它的 README 和维护情况。
  • 官方工作流模板:各种语言和框架的现成工作流,Actions 页“新建工作流”里列出的就是这些。