先看一个典型的踩坑场景。你写好了一个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 ci比npm 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: write和deployments: write。官方文档这段文档不够清晰,我当初翻了三遍才搞明白。
(一张GitHub Actions工作流运行时间对比图:左侧是优化前的4.5分钟,右侧是优化后的1.2分钟,配文:缓存配置前后差距)
现在来说说性能优化。GitHub Actions的免费额度虽然慷慨,但也不能浪费。我优化过的流水线,从平均3.2秒降到0.8秒。关键点有三个:
`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-dependency-path,构建时间直接减半。配置防止重复部署,团队协作必备。最后说一句,GitHub Actions的文档确实不好啃,但一旦上手,你会爱上这种自动化带来的快感。我的CI/CD流水线现在全自动运行,从代码提交到生产部署,中间不需要任何人手动操作,稳定运行了3个月没出过问题。你只需要花一个下午配置好,就能享受数倍效率提升。
如果你在配置过程中遇到其他坑,欢迎留言交流。别忘了,GitHub Actions的社区市场有很多现成的Action,遇到复杂需求先去看看,别自己从头造轮子。