CI 挂在别人的机器上,最痛苦的不是写 workflow,而是「改一行、推一次、等三分钟、看一行日志」的循环。把调试动作前移到本地,把日志开到最细,把失败原因定位到具体行——这三件事决定了你是十分钟解决问题还是耗掉一整个下午。本文系统梳理 GitHub Actions 的本地调试与排错手段:用 act 在容器里跑真实 workflow、用 workflow_dispatch 手动触发并传参、用调试日志与注解命令放大信号、用重跑与 artifact 保留现场,并给出高频错误码的排查路径。
一、act:把 workflow 跑到本地容器里
1.1 安装与初始化
act 通过读取 .github/workflows/ 下的 YAML,在本地 Docker 容器中模拟 runner 执行 job。macOS 用 Homebrew 安装,Linux 用官方脚本:
# macOS
brew install act
# Linux(安装到 ~/.local/bin)
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# 校验
act --version
docker info >/dev/null && echo "docker ok"
安装后先在仓库根目录列出所有可运行的 job,确认 act 解析到的 workflow 符合预期:
# 列出所有 job(含触发事件与所在文件)
act -l
# 只看某个 workflow 文件
act -l -W .github/workflows/ci.yml
# 图形化查看 job 依赖关系
act -g
1.2 运行指定 job 与事件
act 默认触发 push 事件,可以指定事件名、job id、workflow 文件与工作目录:
# 运行单个 job
act -j build
# 触发 pull_request 事件
act pull_request
# 指定 workflow 文件与工作目录
act -W .github/workflows/ci.yml -C ./apps/web
# 只跑某个 job 及其依赖
act -j test --graph
1.3 Apple Silicon 的架构坑
M1/M2/M3 机器上,绝大多数 ubuntu-latest 镜像只有 amd64 版本。不指定架构时会出现 exec format error 或镜像拉取后立即退出:
# Apple Silicon 必加:以 amd64 模拟运行
act -j build --container-architecture linux/amd64
把这个参数写进 .actrc,避免每次手输。.actrc 每行一个参数,等价于命令行:
# .actrc
--container-architecture
linux/amd64
--pull=false
1.4 镜像选择与运行环境差异
act 用「runner 标签 → Docker 镜像」的映射决定容器。默认的 micro 镜像不含常用工具,中等镜像体积与完整性较均衡:
# 推荐:中等镜像,覆盖大多数 CI 场景
act -P ubuntu-latest=catthehacker/ubuntu:act-latest
# 轻量:体积小但缺工具(无 git/curl 之外的多数命令)
act -P ubuntu-latest=node:16-buster-slim
# 完整:与 GitHub 托管 runner 最接近,体积数 GB
act -P ubuntu-latest=catthehacker/ubuntu:full-latest
在 .actrc 中固化映射,团队内保持一致:
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
1.5 密钥、环境变量与 artifact 服务
workflow 里引用的 secrets.* 与 env 需要显式注入,否则得到空字符串——这是「本地跑通、线上失败」的最常见原因:
# 单条 secret
act -s GITHUB_TOKEN=ghp_xxx
# 批量从文件读取(每行 KEY=VALUE)
act --secret-file .secrets
# 普通环境变量(非密钥)
act --env-file .env.local
# 自定义 event payload(模拟 PR 内容)
act pull_request -e .github/act/pr-event.json
涉及 actions/upload-artifact 或 download-artifact 的 job,本地必须启用 artifact 服务端,否则步骤直接报错:
# 启动内置 artifact 服务,产物落到本地目录
act -j build --artifact-server-path /tmp/act-artifacts
1.6 act 的能力边界
act 不是 GitHub 托管 runner 的等价物,以下能力在本地无法完整复现,遇到时必须回到线上验证:
不支持或不完整:
- OIDC 令牌签发(cloud 联邦登录无法本地模拟)
- GitHub 托管 runner 的预装工具链(Android SDK、Xcode 等)
- 服务容器 health check 的部分行为与网络隔离细节
- 环境(Environment)审批、保护规则与并发控制
- 缓存服务(actions/cache 后端)的跨运行命中行为
- macOS / Windows runner
行为差异:
- GITHUB_TOKEN 默认权限与线上不同,需手动传
- 事件 payload 字段缺失,github.event 结构可能不全
二、workflow_dispatch:手动触发与参数化
2.1 定义 inputs
workflow_dispatch 让 workflow 出现在仓库的 Actions 页签,可点按钮运行。inputs 支持 string、boolean、choice、environment 四种类型:
name: Manual Debug
on:
workflow_dispatch:
inputs:
environment:
description: "部署目标环境"
type: environment
required: true
version:
description: "要发布的版本号,例如 1.4.2"
type: string
required: true
default: "0.0.0"
dry_run:
description: "仅演练不实际执行"
type: boolean
required: false
default: true
log_level:
description: "日志级别"
type: choice
required: true
default: info
options:
- debug
- info
- warn
- error
2.2 inputs 与 github.event.inputs 的差异
这是最容易踩的坑。inputs 上下文是强类型的,boolean 会解析成真正的布尔值;github.event.inputs 全部是字符串,布尔会变成 "true" / "false" 字符串。用错会导致条件判断永远成立或永远不成立:
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: 正确用法(强类型)
if: ${{ inputs.dry_run }}
run: echo "dry run 模式"
- name: 错误示范(字符串 "false" 仍为真)
if: ${{ github.event.inputs.dry_run }}
run: echo "这行即使传 false 也会执行"
- name: 打印全部参数
run: |
echo "env=${{ inputs.environment }}"
echo "version=${{ inputs.version }}"
echo "level=${{ inputs.log_level }}"
2.3 命令行触发
用 gh 触发比点按钮更适合脚本化与复现。-f 传字符串与布尔,-F 会尝试按 JSON 解析:
# 触发并传参
gh workflow run manual-debug.yml \
-f environment=staging \
-f version=1.4.2 \
-f dry_run=false \
-f log_level=debug
# 指定分支
gh workflow run manual-debug.yml --ref release/1.4
# 列出并查看刚触发的运行
gh run list --workflow=manual-debug.yml --limit 5
gh run watch # 实时跟踪最新一次运行
若 gh workflow run 报 could not find any workflows named ...,多半是 workflow 文件尚未推送到默认分支——workflow_dispatch 的按钮与命令行触发都要求该文件已存在于默认分支。
三、调试日志与注解命令
3.1 开启 debug 日志
在仓库 Settings 的 Secrets and variables 中新增名为 ACTIONS_STEP_DEBUG 的 secret,值设为 true,之后所有步骤都会输出 runner 内部的详细日志(包括每一步的实际命令行、环境变量、shell 解析过程)。ACTIONS_RUNNER_DEBUG 则输出 runner 自身诊断信息:
ACTIONS_STEP_DEBUG = true # 步骤级 debug 日志(最常用)
ACTIONS_RUNNER_DEBUG = true # runner 进程诊断日志
线上排查时,也可以在重跑界面直接勾选 Enable debug logging,无需改 secret。
3.2 workflow 命令:debug、group、注解
runner 提供一组以 :: 开头的命令,用于在日志中打标记。这些命令比 echo 更结构化,会在 UI 中高亮或折叠:
- name: 使用 workflow 命令
run: |
echo "::debug::当前版本号为 $VERSION"
echo "::group::安装依赖"
npm ci --no-audit
echo "::endgroup::"
echo "::notice::构建完成,产物大小 $(du -sh dist | cut -f1)"
::error:: 支持 file、line、col 参数,会在 PR 的 Files changed 里生成行内注解:
echo "::error file=src/index.ts,line=42,col=7::未处理的空值分支"
echo "::warning file=src/api.ts::该接口已标记废弃"
注意:这些命令必须由 runner 执行 shell 输出才能生效,写在 run: 里即可;但如果在被 set -x 包裹的脚本中输出,前缀可能被 trace 信息污染。
四、Shell 排查技巧与临时放行
4.1 set -x 与环境打印
GitHub Actions 的 bash shell 默认带 -e(任一命令失败即退出)和 -o pipefail。排查时用 set -x 打印每条实际执行的命令,用 env 确认注入的变量:
- name: 排查脚本
shell: bash
run: |
set -x
pwd
ls -la
env | sort | grep -i -E 'CI|NODE|GITHUB' || true
set +x
node -e "console.log(process.env.NODE_ENV)"
set -x 的 trace 输出会带上 + 前缀,若脚本里有 ::group:: 之类的命令,可能被打断。排查完记得移除。
4.2 用 continue-on-error 临时放行
在不阻塞流水线的前提下观察某个可疑步骤,可以加 continue-on-error: true,让它失败也继续跑后续步骤,从而收集更多上下文:
- name: 可能失败的诊断步骤
continue-on-error: true
run: ./scripts/collect-diagnostics.sh
- name: 失败时上传诊断信息
if: failure()
uses: actions/upload-artifact@v4
with:
name: diagnostics
path: |
/tmp/*.log
**/test-results/**
retention-days: 3
if: failure() 与 if: always() 的差别要分清:前者仅在有失败时执行,后者无论如何都执行。收集现场通常用 if: always(),只在出错时上传则用 if: failure()。
4.3 让失败信息更明确
shell 脚本里默认的 exit 1 不带任何上下文,排查时极其痛苦。用带消息的退出让日志自解释:
if [ ! -f "$CONFIG" ]; then
echo "::error::配置文件缺失:$CONFIG(检查上一步的生成步骤)"
exit 1
fi
# 也可以用 trap 捕获错误行号
trap 'echo "::error::命令失败于第 $LINENO 行"' ERR
五、失败排查路径:重跑与日志下载
5.1 重跑策略
Actions 运行页面右上角的 Re-run jobs 提供两个选项:
Re-run all jobs
重新执行所有 job,缓存与 artifact 重新生成
适用于:基础镜像或依赖源临时抖动
Re-run failed jobs
只重跑失败的 job,成功的 job 结果保留
适用于:单个 job 的网络抖动或 runner 抢占
注意:依赖上游 artifact 时,上游 job 未重跑可能读到旧产物
Enable debug logging
重跑时开启步骤级 debug 日志,等价于 ACTIONS_STEP_DEBUG
适用于:失败原因不明、需要看 runner 内部行为
5.2 下载与查看日志
# 列出运行
gh run list --limit 10
# 查看某次运行的整体日志
gh run view <run-id>
# 只看失败步骤的日志
gh run view <run-id> --log-failed
# 下载完整日志包(zip)
gh run download <run-id> -n logs 2>/dev/null || gh api \
/repos/{owner}/{repo}/actions/runs/<run-id>/logs > run-logs.zip
# 实时跟踪
gh run watch <run-id> --interval 5
日志本身有 90 天保留期,且单个 job 日志上限较大时会被截断。关键排查信息应主动用 actions/upload-artifact 落盘:
- name: 保留测试报告
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report-${{ github.run_id }}
path: reports/
retention-days: 14
六、常见错误码与含义
6.1 退出码速查
exit code 1
最泛化的失败:脚本返回非零。常见于命令拼写错误、断言失败。
排查:开 ACTIONS_STEP_DEBUG,用 set -x 打印实际命令。
exit code 137
128 + 9,进程被 SIGKILL,通常是 OOM。
排查:换更大的 runner、拆小 job、限制并行度;
Node 项目加 NODE_OPTIONS=--max-old-space-size=4096。
exit code 143
128 + 15,收到 SIGTERM,多为超时或并发取消。
排查:检查 timeout-minutes 与 concurrency 配置。
exit code 127
命令未找到。PATH 未包含工具,或上一步安装失败被静默吞掉。
排查:确认 setup 步骤的 if 条件与 continue-on-error。
Process completed with exit code 137
与上面同义,UI 里的完整措辞。
The runner has received a shutdown signal
runner 被平台回收(通常是超时或账号额度耗尽),任务被强制中断。
排查:检查 timeout-minutes,拆分长任务,或改用 self-hosted runner。
Resource not accessible by integration
GITHUB_TOKEN 权限不足。默认 token 是只读的。
排查:在 workflow 顶部声明 permissions,例如 contents: write。
6.2 YAML 解析类错误
on 被解析成布尔 true
YAML 1.1 把裸 on / off / yes / no 当布尔。
某些编辑器与校验器会误判,写 on: 时建议加引号或保持标准缩进。
缩进错误:steps 与 runs-on 不同级
workflow 报 "Invalid workflow file: .github/workflows/x.yml#L12"。
排查:用 yamllint 或编辑器 YAML 插件校验,注意 tab 与空格混用。
意外符号
多行字符串块里出现未转义的冒号或引号。
排查:run 的多行内容用 | 块标量,避免行内嵌套引号。
6.3 权限声明示例
permissions:
contents: write # 创建 release / 推送标签
pull-requests: write # 评论 PR
id-token: write # OIDC 联邦登录(jobs 级声明更安全)
issues: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: 上传诊断信息
if: failure()
run: echo "::error::部署失败,见上方日志"
原则是「最小权限 + 按 job 声明」:顶层给只读,需要写的 job 单独覆盖。
七、交互式排障与速查表
7.1 self-hosted runner 的 SSH 调试
自托管 runner 上可以保留一个失败现场,直接 SSH 进去看。给 runner 打标签,只在手动触发时启用调试:
debug:
runs-on: [self-hosted, linux, x64]
if: ${{ github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v4
- name: 暂停等待 SSH 接入
run: |
echo "runner: $(hostname)"
echo "SSH 接入后手动执行下一步"
sleep 1800
7.2 tmate 交互式会话
临时需要进入 runner 的 shell 时,用 tmate 起一个可分享的会话。它把 SSH 连接串打印在日志里,点开即可进入容器:
- name: 开启 tmate 调试会话
if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
timeout-minutes: 20
limit-access-to-actor: true 限制只有触发者本人能接入,避免公开仓库被他人连入。调试完务必删除该步骤——它会让 job 长时间挂起并消耗额度。
7.3 速查表
本地运行
act -l 列出所有 job
act -j build 运行指定 job
act pull_request 触发指定事件
act -P ubuntu-latest=catthehacker/ubuntu:act-latest
指定镜像
act --container-architecture linux/amd64
Apple Silicon 必加
act -s TOKEN=xxx 注入 secret
act --secret-file .secrets 从文件注入
act --artifact-server-path /tmp/a 启用 artifact 服务
.actrc 固化以上参数
手动触发
workflow_dispatch.inputs 定义 string/boolean/choice/environment
inputs.x 强类型上下文(推荐)
github.event.inputs.x 全字符串上下文(易踩坑)
gh workflow run x.yml -f k=v 命令行触发
日志与调试
ACTIONS_STEP_DEBUG=true 步骤级 debug 日志
ACTIONS_RUNNER_DEBUG=true runner 诊断日志
::debug:: / ::notice:: / ::warning:: / ::error::
日志标记与注解
::group:: / ::endgroup:: 折叠日志
set -x / env 打印命令与环境
失败处理
Re-run failed jobs 只重跑失败 job
Re-run with debug logging 重跑并开 debug
gh run view --log-failed 查看失败日志
if: failure() / if: always() 条件收集现场
actions/upload-artifact@v4 保留诊断产物
常见退出码
1 泛化失败 137 OOM 被杀
143 超时被终止 127 命令未找到
总结
GitHub Actions 的调试效率取决于三层能力:本地化、可观测性、现场保留。本地化靠 act 把 workflow 跑进 Docker,注意 Apple Silicon 必须指定 --container-architecture linux/amd64、镜像用 catthehacker 系列、密钥与 artifact 服务都要显式开启,同时清楚 OIDC、托管工具链、环境审批这些本地跑不了的部分。可观测性靠 ACTIONS_STEP_DEBUG 打开步骤级日志、靠 ::group:: 与 ::debug:: 让输出结构化、靠 set -x 确认实际执行的命令;workflow_dispatch 则把手动触发参数化,用 inputs 而非 github.event.inputs 才能拿到正确的布尔类型。现场保留靠 if: always() 配合 actions/upload-artifact 把日志与测试报告落盘,重跑时优先用 Re-run failed jobs 节省时间,原因不明时用 Enable debug logging。最后,遇到 exit 137 先怀疑内存、遇到 Resource not accessible by integration 先查 permissions——把高频错误码的排查路径固化成肌肉记忆,比反复读日志更快。
延伸阅读:
- GitHub Actions Python CI — 完整 CI 流水线示例
- GitHub Actions 缓存优化 — 缓存键设计与命中排查
- GitHub Actions 通知与 ChatOps — 失败告警接入
- GitHub Actions 矩阵策略 — 矩阵调试与条件组合
- DevOps 专题 — CI/CD 通用原则
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。