GitHub Actions 本地调试与排错:act、workflow_dispatch、日志与重跑

GitHub Actions 本地调试与排错实战:用 act 在本地运行 workflow(镜像选择、密钥注入、artifact 服务、能力边界)、workflow_dispatch 手动触发的 inputs 定义与命令行触发、ACTIONS_STEP_DEBUG 调试日志与 ::debug:: / ::group:: 注解命令、set -x 与 continue-on-error 排查技巧、重跑与日志下载路径、常见错误码含义(exit 137、Resource not accessible)、tmate 交互式排障与速查表

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」更多文章

  1. GitHub Actions 工作流性能与并发控制:concurrency、超时与分钟数优化
  2. GitHub Actions 容器作业与 Service 容器:job container、健康检查与集成测试
  3. GitHub Actions 分支保护与 Rulesets:必需检查、CODEOWNERS 与仓库治理