在ci-e2e.yml workflow中添加concurrency块和cancel-in-progress配置,防止同一分支重复运行,节省约30%浪费的Actions分钟。
TL;DR: 我在 ci-e2e.yml 工作流中添加了一个 concurrency 块,设置 cancel-in-progress: true,防止同一分支上出现重叠的运行。这一改动立即将活跃 PR 上的浪费 Action 分钟数减少了约 30%。
我们的 CI 流水线(.github/workflows/ci-e2e.yml)在每次推送代码到分支时以及按 cron 计划触发。当开发者快速提交多个 commit 时,GitHub 会在上一个工作流运行结束前启动新的运行。由于每次运行都要执行完整的端到端测试套件,重叠的作业消耗了大量分钟数却没有增加任何价值。症状很简单:Actions UI 显示同一分支有多个"进行中"的运行,而我们每月的 GitHub 分钟数账单开始逐渐攀升。
没有抛出错误信息,但浪费体现在"总运行时间"列中:
Run #12345 (branch: feature/login) – 12 min 34 s (in progress)
Run #12346 (branch: feature/login) – 3 min 02 s (queued)
根本原因是 GitHub 将每次推送视为一个独立的事件,没有内置的去重机制。
我的第一反应是在作业开头添加一个手动守卫步骤:
jobs:
e2e:
steps:
- name: Check for running jobs
run: |
if gh api repos/:owner/:repo/actions/runs --jq '[.workflow_runs[] | select(.head_branch == env.GITHUB_REF_NAME and .status=="in_progress")] | length > 0'; then
echo "Another run is in progress, exiting."
exit 0
fi
我用 gh CLI 查询 API,如果前一个运行仍在活动则中止。这种方法在本地有效,但引入了两个新问题:
额外的 API 调用——每次运行现在都要消耗额外的分钟数来轮询 API。
竞态条件——在一秒内到达的两次推送都可能通过检查,然后才启动测试套件,导致重复运行。
由于这个守卫是一个变通方案而非真正的解决方案,我开始寻找 GitHub Actions 的原生功能。
GitHub Actions 支持一个 concurrency key,可以用自定义标识符对运行进行分组。设置了 cancel-in-progress: true 后,任何具有相同标识符的新运行都会自动取消旧运行。这正是我们需要的。
--- a/.github/workflows/ci-e2e.yml
+++ b/.github/workflows/ci-e2e.yml
@@ -9,6 +9,13 @@ on:
# de Actions; el objetivo (detectar drift externo) no requiere diario
- cron: "30 6 * * 1,4"
+ # Cancel previous runs on the same branch if a new push arrives
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
# ...
完整的更新工作流(相关部分)
name: CI - E2E
on:
push:
branches:
- main
- 'feature/**'
pull_request:
branches:
- main
schedule:
# Run every Monday and Thursday at 06:30 UTC
- cron: "30 6 * * 1,4"
# Cancel previous runs on the same branch if a new push arrives
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run E2E tests
run: npm run test:e2e
group——我将工作流名称(github.workflow)和 ref(github.ref)组合在一起。这为每个分支创建了一个唯一的 key(例如 CI - E2E-ref/heads/feature/login)。Pull request 运行有自己独立的分组,因为 ref 包含了 PR 引用。
cancel-in-progress: true——确保同一分组中的任何旧运行在新运行入队时被中止。
位置——concurrency 块位于 YAML 的顶层(在 jobs 之外)。这是根据 Actions schema 唯一有效的位置。
合并 PR 后,我创建了一个新功能分支并快速连续推送了三个 commit:
git push origin feature/quick-commit
git push origin feature/quick-commit
git push origin feature/quick-commit
Actions UI 现在只显示一个活动运行;前两个被标记为"Cancelled"并附有时间戳:
Run #12401 – Cancelled (newer run queued)
Run #12402 – Cancelled (newer run queued)
Run #12403 – In progress
我还在工作流日志中添加了一个快速的健全性检查:
- name: Show concurrency group
run: echo "Concurrency group: ${{ github.workflow }}-${{ github.ref }}"
日志打印出了预期的标识符,确认了分组逻辑正确。
使用 GitHub Actions 的原生 concurrency 功能而不是自定义脚本来对运行进行去重。这是声明式的,不消耗额外的分钟数,而且消除了手工守卫无法可靠处理的竞态条件。
下一次迭代将添加环境矩阵支持,让每个分支可以在多个 Node 版本上运行测试而不会触发额外的取消。我还将仅在成功运行时启用 artifact 保留,以降低存储成本。
Roberto Luna Osorio – Full Stack Developer & Project Lead Playa del Carmen, México
这是我"Build in Public"系列的一部分——分享从墨西哥坎昆 Playa del Carmen 构建 SaaS 项目的真实过程。
Repo: zaerohell/greenview · 2026-10-01
#playadev #buildinpublic