GitHub Actions自动化实战:从踩坑到流水线自由

先看一个典型的踩坑场景。你写好了一个Node.js项目,想要每次push代码后自动跑测试、lint检查、构建和部署。理想很丰满,现实很骨感。第一个坑就是环境变量的传递问题。

yaml

这是我的第一版配置,踩坑后修正的版本

name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
# 注意这里:环境变量要放在jobs级别,不是steps级别
env:
NODE_VERSION: '18'
CI: true
steps:
- uses: actions/checkout@v3

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
# 缓存是个好东西,但配置错了反而更慢
cache: 'npm'
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci
# npm ci比npm install更快,而且能保证版本一致

- name: Run tests
run: npm test
# 测试失败后自动终止,不会继续构建
`

为什么这么写?首先,npm cinpm install快30%左右,因为它是严格按照lock文件安装的。其次,缓存配置一定要指定cache-dependency-path,不然不同分支的缓存会互相干扰。我之前就是因为没指定这个,结果develop分支的缓存覆盖了main分支的缓存,导致构建时间从1.2分钟飙到4.5分钟。

(一个GitHub Actions运行失败的截图,红色叉号旁边显示"npm ci failed",配文:第一次跑通之前,先准备好三包纸巾)

另一个坑是并发控制。团队多人同时push,工作流会同时运行多个实例。如果不做限制,可能会同时部署、相互覆盖配置。

`yaml

并发控制:防止同时部署互相干扰

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
# 这个设计真的反人类:deploy的权限要单独配置
permissions:
contents: write
deployments: write
steps:
- uses: actions/checkout@v3

- name: Deploy to production
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
# 这里有个坑:如果publish_branch没有创建,部署会失败
publish_branch: gh-pages
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
`

concurrency配置是GitHub Actions在2023年才完善的功能。cancel-in-progress: true意味着当新的push触发时,会自动取消正在运行的同组任务。这个特性在团队开发中特别有用,避免了"最后一分钟提交覆盖他人变更"的尴尬。

还有个技巧:permissions字段很多人会忽略,但它是安全的关键。默认情况下,workflow只有读取权限,如果要写入仓库(比如部署到gh-pages分支),必须显式声明contents: writedeployments: write。官方文档这段文档不够清晰,我当初翻了三遍才搞明白。

(一张GitHub Actions工作流运行时间对比图:左侧是优化前的4.5分钟,右侧是优化后的1.2分钟,配文:缓存配置前后差距)

现在来说说性能优化。GitHub Actions的免费额度虽然慷慨,但也不能浪费。我优化过的流水线,从平均3.2秒降到0.8秒。关键点有三个:

  • 复用缓存:除了npm缓存,还可以缓存Python的pip、Go的mod等。如果你用多个语言,注意缓存键的命名规则。
  • 并行任务:测试和lint可以并行,不必串行运行。
  • 条件触发:只有push到main分支才部署,其他分支只做测试。
  • `yaml

    缓存策略示例:多语言项目的缓存配置

    jobs:
    cache-setup:
    runs-on: ubuntu-latest
    steps:
    - name: Cache Node.js
    uses: actions/cache@v3
    with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
    ${{ runner.os }}-node-

    - name: Cache Python
    uses: actions/cache@v3
    with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
    ${{ runner.os }}-pip-
    `

    restore-keys字段很多人不理解。它的作用是当精确匹配的key不存在时,尝试用最接近的key恢复缓存。比如,package-lock.json更新了,新的hash值没有缓存,就会匹配到${{ runner.os }}-node-这个前缀,恢复旧缓存,然后增量更新。这样即使缓存失效,也不会从头下载所有包。

    还有个让我崩溃的坑:GitHub Actions对时间敏感。如果你的工作流运行时间超过6小时(免费版),会自动超时。但更常见的是,某些步骤超过30分钟就会触发默认超时。解决方法很简单:

    `yaml

    • name: Long running task

    run: |
    # 长时间任务要设置超时
    timeout 600 npm run build
    timeout-minutes: 10
    `

    timeout-minutes可以覆盖默认超时时间。对于测试、构建这类任务,建议设得宽松一些,但也不要超过20分钟,否则会占用太多免费的并行配额。

    (一张GitHub Actions的Matrix策略截图,显示4个并行任务成功运行,配文:矩阵构建让测试快4倍)

    总结一下,你可以立刻用的三个点:

  • 缓存配置:所有项目都加上cache: ‘npm’cache-dependency-path,构建时间直接减半。
  • 并发控制:用concurrency配置防止重复部署,团队协作必备。
  • 权限声明:需要写入仓库的任务,显式设置permissions`,避免莫名其妙报403错误。
  • 最后说一句,GitHub Actions的文档确实不好啃,但一旦上手,你会爱上这种自动化带来的快感。我的CI/CD流水线现在全自动运行,从代码提交到生产部署,中间不需要任何人手动操作,稳定运行了3个月没出过问题。你只需要花一个下午配置好,就能享受数倍效率提升。

    如果你在配置过程中遇到其他坑,欢迎留言交流。别忘了,GitHub Actions的社区市场有很多现成的Action,遇到复杂需求先去看看,别自己从头造轮子。

    滚动至顶部