深入理解 GitHub Actions 并发控制:优雅管理 CI/CD 工作流
在团队协作开发中,CI/CD 流水线的效率与稳定性至关重要。当多人频繁向同一个分支推送代码,或者短时间内打开/更新多个 Pull Request 时,GitHub Actions 默认会为每个事件启动独立的工作流运行。这可能导致资源浪费、部署冲突,甚至测试结果不一致。为了解决这些问题,GitHub Actions 提供了 concurrency 配置字段。本文将通过一个典型的配置片段,详细解析其工作原理与最佳实践。
典型配置示例
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
这段配置看似简短,却蕴含了对不同场景的精细控制。下面逐层拆解。
group:定义并发组标识
group 字段用于将多个工作流运行归入同一个逻辑组。同一时间,同一组内只允许一个运行处于活跃状态(其他运行将处于 pending 或被取消,取决于 cancel-in-progress 的设置)。
表达式的计算逻辑如下:
${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
github.workflow:当前工作流的名称(例如 “CI” 或 “Build”),确保不同工作流的并发组天然隔离。github.event.pull_request.number:当触发事件是pull_request时,该值为 PR 编号;如果当前不是 PR 事件,则该值为空。github.ref:触发工作流的分支或标签引用(如refs/heads/main)。
关键逻辑:|| 运算符提供了回退机制。
- 如果是 Pull Request 事件 → 组名为
工作流名-PR编号(例如CI-123)。 - 如果是 push 或 tag 事件 →
github.event.pull_request.number不存在,取github.ref,组名为工作流名-refs/heads/main。
为什么这样设计?
对 PR 场景:同一个 PR 的后续提交产生的运行属于同一组,避免浪费资源运行旧的 commit。
对分支场景:同一个分支(如 main)的多次 push 属于同一组,但不同分支(feature-a 和 feature-b)则分属不同组,互不影响。这既保证了关键分支的队列顺序,又允许并行开发多个独立功能。
cancel-in-progress:是否取消正在运行的旧任务
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
该表达式在 pull_request 事件下为 true,其他事件(如 push、workflow_dispatch)为 false。
true:当新运行进入该组时,直接取消组内正在运行的旧运行。false:新运行进入pending等待状态,直到旧运行完成。
这个设置的合理性
- Pull Request 场景:开发者频繁向 PR 推送修复或新增提交。较早的 CI 运行可能仍在进行,但针对旧代码的测试结果已无价值。此时取消旧运行可以立即释放 runner 资源,让新提交的检查更快得到结果。
cancel-in-progress: true是 PR 场景的最佳实践。 - 重要分支(如 main)场景:多个 push 连续发生时,你可能希望每个 push 的构建和部署都完整执行,而不是被后续 push 打断。例如,主分支的部署流水线需要按顺序完成,若中途取消可能导致部署状态不一致。此时设置
cancel-in-progress: false更安全。
组合效果详解
以常见配置为例:
| 触发事件 | 组名示例 | cancel-in-progress | 行为 |
|---|---|---|---|
| PR #123 第一次提交 | CI-123 |
true | 开始运行 |
| PR #123 第二次提交(运行中) | CI-123 |
true | 取消第一次运行,开始第二次运行 |
| 向 main 分支 push commit A | CI-refs/heads/main |
false | 运行 A |
| 紧接着向 main 分支 push B | CI-refs/heads/main |
false | B 进入 pending 等待,待 A 完成后开始 B |
| 向 feature-x 分支 push | CI-refs/heads/feature-x |
false | 与 main 组不同,可同时进行,互不阻塞 |
高级用法与注意事项
1. 更细粒度的分组策略
若仓库有多个工作流(例如 test.yml 和 deploy.yml),github.workflow 天然隔离了它们。但如果希望在不同 workflow 之间共享并发限制(例如限制总部署并发数),可以省略 github.workflow,只使用分支或 PR 标识。
2. 取消进行中的部署是否危险?
如果部署流程包含数据库迁移或原子更新,中途取消可能导致不一致状态。建议部署类工作流不开启自动取消,或者使用 cancel-in-progress: false,并在部署脚本中处理中断信号(如 trap)。
3. 在矩阵(matrix)策略中的表现
当工作流使用了 strategy.matrix,concurrency 作用在整个工作流运行级别,而不是单个矩阵任务。即一组矩阵中的所有 job 整体被视作一个运行,要么全部完成,要么整体被取消。这符合大多数预期。
4. 并发组作用域
并发组的作用域是仓库级别。不同仓库之间不共享组状态。因此当使用可复用的工作流(workflow_call)时,外部调用方的组设置独立于被调用方内部的组设置,需要注意设计。
5. 结合 if 条件按需启用
有时候只想在特定分支或事件类型下启用并发控制:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref == 'refs/heads/main' }}
上述配置仅在 main 分支上取消进行中的运行,其他分支则排队等待。
实际排查技巧
当遇到工作流“莫名被取消”或“一直 pending”时,可以:
- 检查工作流日志,被取消的运行会显示 “Canceling since a newer run was triggered for the same concurrency group”。
- 查看 GitHub 的 “Actions” 页面,在具体运行记录中会标明并发组名称(在设置部分可以找到实际计算的组字符串)。
- 使用 GitHub CLI 或 API 列出正在运行和等待的工作流,确认组名是否符合预期。
总结
合理利用 concurrency 配置可以显著提升 CI/CD 的效率和资源利用率:
- Pull Request 场景:推荐使用
cancel-in-progress: true,组名包含 PR 编号。 - 重要分支(main/release):推荐
cancel-in-progress: false,保证顺序执行且不中断。 - 其他功能分支:可以按需使用分支名作为组标识,允许独立并行。
本文给出的标准配置片段:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
已被验证为适用于大多数项目的一站式方案。它可以智能区分 PR 和分支事件,在 PR 中主动取消过时检查,在分支 push 中保留队列顺序。掌握这一机制,你的 GitHub Actions 将更加健壮、高效。