一、本地调试GitHub Actions的核心痛点:环境差异坑

很多开发者用GitHub Actions跑CI/CD时,经常遇到一个糟心事:线上(GitHub服务器)跑的工作流(Workflow)完全没问题,本地用act工具模拟跑,结果要么报错,要么输出和线上不一样。我之前就踩过不少坑:比如本地跑时某个依赖包找不到、端口映射失败、甚至连系统自带的工具版本都和线上差一大截,折腾半天才发现是环境没对上。

要解决这个问题,核心得搞懂两个点:一是act怎么模拟GitHub的容器环境,二是工作流里用到的系统依赖、网络配置怎么和本地对齐。只有把这两个点摸透,才能实现离线验证,让本地跑的结果和线上完全一致。

二、先搞懂act的底层逻辑:容器映射到底是啥

act是专门用来本地模拟GitHub Actions的工具,它的核心原理是用Docker容器复刻GitHub服务器的运行环境。GitHub官方的工作流运行环境是预配置好的容器镜像(比如ubuntu-latest、windows-latest这些),act本地跑时,本质就是拉取对应镜像,把本地的代码、配置映射到容器里,再执行工作流的步骤。

这里的“容器映射”是关键,很多人踩坑就是没搞懂映射规则。举个例子:你本地项目根目录有个.github/workflows/deploy.yml的工作流文件,act跑的时候,会把你本地的整个项目目录映射到容器里的/github/workspace路径,同时把工作流文件映射到容器里的/github/workflows路径。但如果你的工作流里用到了本地的某个配置文件(比如config/prod.json),但这个文件没被正确映射到容器,或者路径写死了本地的绝对路径(比如/Users/xxx/config/prod.json),那容器里肯定找不到这个文件,直接报错。

2.1 容器映射的两个核心规则

  1. 代码目录映射:act默认会把你执行act命令的当前目录(也就是项目根目录),映射到容器的/github/workspace目录,容器里执行工作流时,默认的工作目录就是这个/github/workspace,相当于你在本地项目根目录执行命令。
  2. 工作流文件映射:act会把本地的.github/workflows/目录映射到容器的/github/workflows/目录,所以工作流里引用的文件路径,要基于容器内的路径来写,不能用本地的绝对路径。

三、用act调试的完整流程:从配置到验证

我以一个Node.js项目的CI工作流为例,给大家一步步演示怎么配置,怎么避免环境差异。这个例子的技术栈是:Node.js 18、Docker、GitHub Actions、act。

首先,先准备好基础文件:

3.1 项目基础文件

首先是项目的package.json,用来定义依赖和脚本:

{
  "name": "node-ci-demo",
  "version": "1.0.0",
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.7.0"
  },
  "scripts": {
    "test": "jest",
    "start": "node server.js"
  }
}

然后是测试用的server.js

const express = require('express');
const app = express();

// 定义一个简单的测试接口
app.get('/health', (req, res) => {
  res.status(200).send('OK');
});

module.exports = app; // 导出供测试用

还有测试文件test/server.test.js

const request = require('supertest');
const app = require('../server');

test('health接口返回200', async () => {
  const res = await request(app).get('/health');
  expect(res.statusCode).toBe(200);
  expect(res.text).toBe('OK');
});

3.2 线上的GitHub Actions工作流

接下来是线上跑的工作流文件.github/workflows/ci.yml,这个工作流的作用是拉取代码、安装依赖、跑测试:

name: Node.js CI
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest # 线上用的是Ubuntu最新版容器
    steps:
      - uses: actions/checkout@v4 # 拉取代码
      - name: 设置Node.js环境
        uses: actions/setup-node@v4
        with:
          node-version: 18 # 指定Node.js版本为18
          cache: 'npm' # 缓存npm依赖,加快速度
      - run: npm ci # 用npm ci安装依赖(比npm install更稳定,适合CI)
      - run: npm test # 跑测试

3.3 本地用act跑的坑:第一次跑肯定报错

如果现在直接在本地项目根目录跑act命令,大概率会报错,常见的错误有两个:

  1. 找不到Node.js:因为act默认的容器里可能没有预安装Node.js 18;
  2. npm ci报错:因为npm ci需要package-lock.json文件,而你本地可能没生成,或者缓存路径没对齐。

怎么解决?得搞懂系统依赖和容器环境的对应关系。

四、解决环境差异的核心:系统依赖与容器配置对齐

环境差异的本质,是本地act用的容器和线上GitHub用的容器,在系统工具版本、依赖、配置上不一样。要对齐,得从三个方面入手:

4.1 对齐系统依赖:确保容器里有需要的工具

线上的ubuntu-latest容器是预配置好的,比如自带git、curl、Node.js(通过actions/setup-node安装)。本地用act时,要确保容器里的工具版本和线上一致。

比如上面的例子里,线上用actions/setup-node安装Node.js 18,本地act跑时,也会执行这个步骤,但是如果你的Docker网络有问题,拉取Node.js镜像失败,就会报错。解决方法是提前拉取act需要的镜像,或者配置act用本地的Docker缓存。

另外,如果你的工作流里用到了系统级的工具(比如git、docker、jq),要确保act用的容器里也有这些工具。比如如果你需要在容器里跑docker命令,就得用act的--privileged参数,给容器足够的权限,同时确保容器里安装了docker。

4.2 对齐路径:避免绝对路径的坑

很多人写工作流时,会不小心用本地的绝对路径,比如把配置文件的路径写成/Users/xxx/project/config.json,但容器里根本没有这个路径,肯定报错。正确的做法是用容器内的相对路径,比如./config.json,因为容器的工作目录是/github/workspace,也就是项目根目录,相对路径会自动对应到容器里的正确位置。

举个例子,如果你在工作流里加一个步骤,要读取本地的配置文件:

- name: 读取配置文件
  run: cat ./config.json

这个命令在容器里会读取/github/workspace/config.json,也就是你本地项目根目录的config.json,因为act已经把本地项目目录映射到容器的/github/workspace了。

4.3 对齐缓存:避免依赖安装失败

线上的GitHub Actions会缓存npm、pip等依赖,加快后续跑工作流的速度。本地用act跑时,也可以配置缓存,避免每次都重新下载依赖,同时确保缓存路径和线上一致。

比如上面的工作流里,actions/setup-node配置了cache: 'npm',线上会把npm的缓存存到GitHub的缓存里,本地act跑时,会把缓存存到你本地的Docker缓存目录。如果你的本地缓存损坏,就会导致npm ci报错,解决方法是删除本地的Docker缓存,或者用act的--no-cache参数重新跑,强制重新下载依赖。

五、离线验证的完整步骤:让本地结果和线上一致

现在我们一步步调整,让本地跑的结果和线上完全一致:

5.1 准备本地环境

首先,确保你本地安装了Docker(act依赖Docker),然后安装act:

# 安装act(Mac系统用brew,Windows用choco,Linux用curl)
brew install act

然后,在项目根目录生成package-lock.json(因为npm ci需要这个文件):

npm install

5.2 配置act的容器参数

为了让act的容器和线上的ubuntu-latest完全一致,我们可以指定act用的镜像,同时配置映射和权限。比如跑上面的ci.yml工作流,命令可以写成:

# 用act跑ci.yml的test任务,指定用ubuntu-latest镜像,开启特权模式(如果需要的话)
act -W . -j test -P ubuntu-latest=catthehacker/ubuntu:act-latest --privileged

这里的参数解释:

  • -W .:指定工作流所在的目录是当前目录(项目根目录);
  • -j test:指定跑test任务;
  • -P ubuntu-latest=catthehacker/ubuntu:act-latest:指定act用的ubuntu-latest镜像,这个镜像和GitHub官方的ubuntu-latest几乎一致;
  • --privileged:给容器足够的权限,避免一些权限相关的错误(比如跑docker命令、挂载设备等)。

5.3 验证结果

跑上面的命令后,你会看到和线上几乎一样的输出:

  1. 拉取指定的ubuntu镜像;
  2. 拉取代码(act会把本地项目目录映射到容器,所以这里的拉取代码步骤其实是复用本地的代码);
  3. 安装Node.js 18;
  4. 安装npm依赖;
  5. 跑测试,输出测试通过的结果。

如果这次跑的结果和线上一致,说明你的环境已经对齐了。

六、应用场景、优缺点、注意事项

6.1 应用场景

  1. 工作流调试:线上跑工作流需要等待GitHub的队列,本地用act可以快速调试,节省时间;
  2. 离线验证:比如你的工作流用到了敏感信息(比如密钥),不想上传到GitHub,本地用act可以离线验证工作流的逻辑;
  3. 复杂工作流测试:比如你的工作流涉及到多个任务、多个步骤的依赖关系,本地用act可以快速测试,避免线上跑失败。

6.2 技术优缺点

优点

  1. 快速调试:本地跑比线上跑快很多,不需要等待GitHub的队列;
  2. 离线验证:可以在没有网络的情况下,验证工作流的逻辑;
  3. 节省资源:不需要占用GitHub的CI/CD配额,适合频繁调试的场景。

缺点

  1. 环境对齐难:如果工作流用到了很多特殊的系统依赖,本地很难完全对齐线上的环境;
  2. 部分功能不支持:act不支持GitHub Actions的所有功能,比如一些官方的特殊动作(比如actions/upload-artifact的部分高级功能);
  3. 依赖Docker:本地必须安装Docker,否则无法运行。

6.3 注意事项

  1. 镜像版本要对齐:一定要用和线上一致的镜像版本,比如线上用ubuntu-latest,本地act也要用对应的ubuntu-latest镜像;
  2. 路径要用相对路径:工作流里的文件路径一定要用相对路径,避免绝对路径的坑;
  3. 敏感信息要注意:如果你的工作流用到了敏感信息(比如密钥),本地用act跑时,要确保这些信息不会泄露;
  4. 测试act的兼容性:如果你的工作流用到了一些特殊的动作,要先测试act是否支持这些动作,避免本地跑没问题,线上跑失败。

七、总结

用act本地调试GitHub Actions的核心,是搞懂容器映射和系统依赖的对齐方法。只要把容器的镜像版本、路径、缓存、系统依赖都和线上对齐,就能实现离线验证,让本地跑的结果和线上完全一致。

很多人踩坑都是因为没搞懂act的底层原理,只是盲目地跑act命令,遇到问题就不知道怎么解决。其实只要抓住“容器映射”和“环境对齐”这两个核心点,再结合具体的例子一步步调整,就能轻松解决环境差异的问题。

最后,要注意act的局限性,它不是万能的,部分特殊的工作流还是需要线上跑验证,但对于大部分常见的CI/CD工作流,本地用act调试已经足够高效。