并发控制与环境审批
当你把一条流水线真正接到生产环境之后,会遇到两个新问题:同一个 workflow 被频繁触发时,多个运行会不会互相打架? 以及 部署生产这种高危操作,能不能在真正执行前加一道人工确认?
本章就来解决这两个问题。前半部分讲 concurrency(并发控制),让你能限制"同一时间只跑一个部署";后半部分讲 Environments(环境)与审批门禁,把"部署生产"变成一件需要人工点头的严肃事情。
1. concurrency:让同一时刻只跑一个
设想这样一个场景:你正在向 main 分支连续推送两次提交。第一次推送触发的部署还在跑,第二次推送又触发了一次。如果两次部署都往同一台服务器写文件,它们可能会相互覆盖,甚至留下一个"一半新一半旧"的坏状态。
concurrency 的作用就是:同一组运行里,只允许一个在跑,其余的要么排队、要么被取消。
最基础的写法是在 workflow 顶层声明一个并发组:
concurrency: deploy-production这表示:只要属于 deploy-production 这个组的运行,同一时间最多只有一个在执行。当新运行进来时,默认行为是让旧的运行继续跑完,新运行排队等待。
1.1 group 可以用表达式,让不同分支互不干扰
concurrency 真正强大的地方在于,group 的值可以是一个动态表达式。比如你想让每个分支各自独立、互不阻塞:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false这里 group 的值是 deploy-${{ github.ref }}。比如 main 分支的运行属于 deploy-refs/heads/main 组,dev 分支的运行属于 deploy-refs/heads/dev 组,二者互不影响,各自排队。而同一个分支的多次推送,则会在组内进行并发控制。
把 ${{ github.ref }} 这类动态值拼进 group,是 GitHub Actions 里非常常见的技巧:它让"不同分支/不同 PR 各自独立、同一条线内串行"成为可能,既不会让无关的分支互相阻塞,又能防止同一条线上的部署打架。
类似的写法还有 group: ci-${{ github.head_ref || github.run_id }},PR 用 head 分支名分组,普通 push 用 run_id 兜底,避免分组冲突。
1.2 cancel-in-progress:新运行来了就取消旧的
如果你希望"最新的提交才是我想要的",可以开启 cancel-in-progress:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true开启后,当新的运行被触发时,同一个组里还在跑的旧运行会被直接取消,资源让给最新的那次。这对"快速迭代的预览环境"很友好——你只关心最新状态。
cancel-in-progress: true 会把尚未完成的旧运行硬生生中断。如果你的部署是幂等可重入的(比如重新跑一次就能恢复一致),那没什么问题;但如果部署是一个"写一半不能停"的过程,中途取消可能把环境留在半坏状态。
因此:预览/测试环境可以放心开启,生产环境要谨慎,必要时宁可排队等待,也不要贸然中断线上部署。
2. Environments:为部署环境加一道审批门禁
concurrency 解决的是"会不会同时跑",而 Environments(环境) 解决的是"能不能随便部署到生产"。
在仓库的 Settings > Environments 里,你可以定义命名环境,例如 staging(预发)和 production(生产)。每个环境可以单独配置:
- 环境变量与 secrets:只在进入该环境时才注入,job 之外拿不到。
- 必需审查者(Required reviewers):配置一个或多个 GitHub 账号/团队,进入该环境前必须有人手动审批通过。
这就构成了一道审批门禁:当某个 job 关联了带审批的环境,它会先停在 pending 状态,等待审查者点击"Approve",通过后才真正执行部署步骤。
2.1 在 job 里关联环境
用 environment: 字段把一个 job 绑定到某个环境:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # 关联名为 production 的环境
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh一旦这个 job 运行,GitHub 会在执行 steps 之前先检查 production 环境是否配置了审批。如果有,流水线会暂停并等待审批,审批通过的邮件/界面提示点了 Approve,部署才会继续。
3. 组合拳:CI 前置 + 并发控制 + 环境审批
下面是一个"接近真实"的部署 job:它要求 CI 先通过(needs),保证同一时间只有一个生产部署(concurrency),并且必须经过人工审批(environment: production)。
name: Deploy
on:
push:
branches: [main]
jobs:
ci:
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: ci # 前置:CI 必须通过
runs-on: ubuntu-latest
environment: production # 关联生产环境(含审批门禁)
concurrency: deploy-production # 同一时间只跑一个生产部署
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh这段配置串起来就是:CI 挂了就不部署;部署时如果有更新的推送进来,由 concurrency 决定排队还是取消;真正动生产之前,必须由审批人点一下确认。 这正是很多团队保护生产环境的标配组合。
concurrency:控制"时间维度"——同一时刻会不会打架。environment:控制"权限维度"——进这个环境要不要审批、能拿到哪些 secret。
二者可以、也经常一起用:一个管"不要撞车",一个管"不要乱进"。
4. 小结
本章我们学习了如何给流水线加上"安全护栏":
concurrency通过group把一组运行标记为互斥,同一时间只跑一个,避免重复部署互相覆盖。group可以内嵌${{ }}表达式(如deploy-${{ github.ref }}),让不同分支各自独立、同一条线内串行。cancel-in-progress: true会在新运行到来时取消旧运行,适合预览环境,但生产环境要谨慎,避免中断进行中的部署。- Environments 在仓库 Settings 中定义,可为每个环境配置专属 secret 与必需审查者(审批门禁)。
- job 用
environment:关联环境,从而进入审批流程,实现"部署生产前人工确认"。 - 把
needs+concurrency+environment组合,即可得到一条"CI 通过 → 不撞车 → 人工审批"的稳健部署链。
并发和审批守住了"怎么跑、谁批准",但还有一个绕不开的话题:流水线自身的安全。下一章我们会讲 permissions 最小权限、如何用 OIDC 免密钥访问云、以及如何防止 secret 泄漏——这些正是把 GitHub Actions 用在生产里最关键的防线。
5. 练习
1. 你的团队有 main 和 feature/* 两类分支,希望"同一分支的部署串行、不同分支互不阻塞"。请写出 concurrency 的 group 配置。
2. cancel-in-progress: true 在什么场景下很合适?在什么场景下会带来风险?请各举一个例子。
3. 在仓库的哪个菜单路径下创建环境与配置审批人?job 里用哪个字段去关联这个环境?
4. 下面这段配置缺少一环,导致生产部署可能被并发触发而互相覆盖。请补上 concurrency 让它安全:
deploy:
needs: ci
runs-on: ubuntu-latest
environment: production
steps:
- run: ./deploy.sh