How to use GitHub Actions for devops

devopsbeginner

上个月,我的团队在一个周五下午把一个带有 Bug 的代码部署到了生产环境。配置文件里仅仅是漏掉了一个分号,却通过了本地检查,被合并到了主分支,并导致我们的支付服务宕机了两个小时。经历了这场“救火”之后,我意识到我们需要一条真正的 CI/CD 流水线——而不是我们一直沿用的那种“我发誓我推送前测试过了”式的流水线。

我之前听说过 GitHub Actions,但一直避而远之。YAML 文件、运行器、矩阵构建——对于一个小团队来说,这听起来像是有很多基础设施开销。但当我终于坐下来着手配置时,我真的很惊讶,一旦理解了核心概念,整个过程竟然如此简单直接。让我带你一步步看看我具体是怎么做的,包括那些我踩过的坑。

## 核心概念(你真正需要了解的)

GitHub Actions 有自己的一套术语,在看其他内容之前,你必须先搞懂这四个关键词:

- **Workflow(工作流)**:整个自动化过程。它是存放在你代码仓库中的一个 YAML 文件。

- **Event(事件)**:触发工作流的东西(比如一次 push、一个 pull request、一个新 issue 等)。

- **Job(作业)**:在同一个运行器(虚拟机)上运行的一组步骤。作业可以并行运行,也可以按顺序运行。

- **Step(步骤)**:作业中的单个任务——要么运行一个脚本,要么调用一个可复用的“action(操作)”。

- **Runner(运行器)**:执行你作业的服务器。GitHub 为你提供免费的 Linux、Windows 和 macOS 虚拟机,你也可以使用自托管的运行器。

可以把它想象成菜谱:工作流是整个菜谱,事件是决定做晚饭,作业是一个烹饪台,而步骤则是具体的操作指令。

## 第一步:创建你的第一个工作流

我首先为一个 Node.js 项目创建了一个简单的 CI 流水线。第一个让我绊倒的坑是文件位置。工作流*必须*存放在仓库根目录的 `.github/workflows/` 下。我一开始直接把它放在了 `.github/` 目录里,然后还在纳闷为什么什么反应都没有。

这是我创建的基础工作流,保存为 `.github/workflows/ci.yml`:

```yaml

name: CI Pipeline

on:

push:

branches: [ main ]

pull_request:

branches: [ main ]

jobs:

test:

runs-on: ubuntu-latest

steps:

- name: Checkout code

uses: actions/checkout@v4

- name: Setup Node.js

uses: actions/setup-node@v4

with:

node-version: '20'

- name: Install dependencies

run: npm ci

- name: Run tests

run: npm test

```

让我们来拆解一下:

- `name`:只是一个人类可读的标签,会显示在 GitHub Actions 的标签页中。

- `on`:这是事件触发器。在这里,该工作流会在每次针对 `main` 分支的 push 或 PR 时运行。

- `jobs`:我们有一个名为 `test` 的作业。

- `runs-on`:我们使用的是 GitHub 托管的 `ubuntu-latest` 运行器。

- `steps`:每个步骤要么使用一个 action(`uses:`),要么运行一个 shell 命令(`run:`)。

`actions/checkout@v4` 这个步骤至关重要——我第一次尝试时忘了加它,结果花了 20 分钟才搞清楚为什么我的 `npm ci` 命令找不到 `package.json`。运行器启动时是一个空的工作区,所以你*必须*先检出你的代码。

## 第二步:添加构建和部署作业

基本的测试流水线跑通后,我想添加一个部署步骤。这就涉及到了作业和依赖关系。我希望部署只在测试通过后发生,并且只在向 `main` 分支推送代码时发生(而不是在 PR 时)。

```yaml

name: CI/CD Pipeline

on:

push:

branches: [ main ]

pull_request:

branches: [ main ]

jobs:

test:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: '20'

- run: npm ci

- run: npm test

deploy:

needs: test

if: github.event_name == 'push'

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: '20'

- run: npm ci

- run: npm run build

- name: Deploy to staging

run: echo "Deploying to staging server..."

# 实际情况中,你会在这里使用部署操作或脚本

```

这里的关键新增内容:

- `needs: test` —— 这告诉 `deploy` 作业要等待 `test` 作业成功完成。没有这行的话,两个作业会并行运行。

- `if: github.event_name == 'push'` —— 这个条件确保了只有在直接推送代码时才进行部署,而在 pull request 时不部署。PR 只会运行测试而不会部署。

## 第三步:处理机密信息(别学我)

对于实际的部署,你需要 API 密钥、凭证和其他机密信息。我的第一直觉是在 YAML 文件中硬编码一个部署令牌。千万别这么做。任何对你的仓库有读取权限的人都能看到它。

GitHub 有一个内置的机密存储区。进入你的仓库设置 → Secrets and variables → Actions,然后在那里添加你的机密信息。之后就可以像这样在你的工作流中引用它们:

```yaml

- name: Deploy to production

env:

DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

run: |

./deploy-script.sh

```

有个让人意外的地方:机密信息会在日志中自动被遮蔽。如果你的脚本不小心打印了机密值,GitHub 会用 `***` 替换它。但有个坑——如果机密信息少于 4 个字符,它就不会被遮蔽。此外,如果你构建的命令可能会通过退出代码或执行时间泄露机密信息,它仍然可能被泄露。请谨慎对待机密信息。

## 第四步:用于多环境测试的矩阵构建

我的下一个挑战是确保我们的代码能在不同的 Node.js 版本下正常运行。这就是矩阵构建大显身手的地方。你不需要为每个版本编写单独的作业,只需定义一个矩阵策略:

```yaml

jobs:

test:

runs-on: ubuntu-latest

strategy:

matrix:

node-version: [18, 20, 22]

steps:

- uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}

uses: actions/setup-node@v4

with:

node-version: ${{ matrix.node-version }}

- run: npm ci

- run: npm test

```

这个单一的作业定义会创建三个并行的测试运行——每个 Node.js 版本一个。如果你的应用还需要支持多种操作系统,你可以这样添加:

```yaml

strategy:

matrix:

os: [ubuntu-latest, windows-latest, macos-latest]

node-version: [18, 20]

```

这会创建 6 个并行的作业(3 个操作系统 × 2 个 Node 版本)。这非常强大,但要注意,GitHub 免费套餐为私有仓库每月提供 2,000 分钟的运行时间。矩阵构建会很快耗尽这些时间,尤其是在 macOS 运行器上,它的消耗速率是 10 倍。

## 第五步:使用缓存加速

运行几次后,我注意到我的工作流很慢——`npm ci` 每次都要从头开始下载所有内容。添加缓存后,我们的构建时间从 3 分钟缩短到了 45 秒以内:

```yaml

- name: Cache node modules

uses: actions/cache@v4

with:

path: ~/.npm

key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

restore-keys: |

${{ runner.os }}-node-

```

`key` 使用了锁文件的哈希值,因此当依赖项发生更改时,缓存会自动失效。`restore-keys` 回退机制意味着如果没有精确匹配,它将使用该操作系统最新的缓存,并在运行后对其进行更新。

## 实用技巧与坦诚的局限性

在使用 GitHub Actions 几个月后,以下是我的经验总结:

**技巧:**

- 务必使用 `@v4` 甚至特定的 commit SHA 来锁定你的 action 版本。我曾因为某个 action 更新了它的 `main` 分支并引入了破坏性更改,导致工作流崩溃。

- 在 CI 中使用 `npm ci` 代替 `npm install`。它更快,并且严格遵守锁文件,可以防止“在我的机器上能跑”的差异问题。

- Actions 标签页中的实时日志出奇地有用。你可以点击具体的失败行并获取一个可分享的链接——非常适合让队友来查看失败原因。

- 从简单开始。不要在第一天就试图构建一个包含 10 个作业的流水线。先让基本的测试工作流跑通,然后再迭代。

**局限性:**

- 对于私有仓库,每月 2,000 分钟的免费额度在矩阵构建和 macOS 运行器面前消耗得很快。请注意监控你的使用量。

- 工作流的 YAML 文件可能会变得很长且难以管理。除了创建自定义 action 或使用可复用工作流(这会增加复杂性)之外,没有太好的模块化方法。

- 调试失败的工作流可能会让人抓狂。失败后你无法 SSH 进运行器。你只能添加额外的 `run: echo` 步骤来检查状态,这非常繁琐。

- 自托管运行器能给你更多控制权,但需要维护和安全加固。它们不是 GitHub 托管运行器那种“设好就不管”的解决方案。

GitHub Actions 从根本上改变了我们团队发布代码的方式。那个周五的支付宕机事件?再也没发生过,因为我们的 CI 流水线现在能在配置错误、测试失败和构建问题进入生产环境之前就将其拦截。初始设置只花了一个下午,但从那以后为我们节省了无数个小时。