复用工作流 Reusable Workflows
上一章我们让数据在 job 之间流动。但还有一类更常见的"重复":你在公司里有 10 个仓库,每个仓库都想跑一套几乎一样的 CI——检出代码、装依赖、跑测试、构建镜像。如果每处都复制粘贴同一段 YAML,一旦要改一条规则(比如把 Node 版本从 18 升到 20),就得改 10 个地方,迟早漏改、迟早出错。
可复用工作流(Reusable Workflows) 就是用来解决这个问题的:把"一整套 workflow"抽成一个独立文件,别处用一行 uses: 就能调用它。
1. 动机:别再复制粘贴
没有复用前,仓库 A、B、C 各有一份差不多的 ci.yml:
# 仓库 A / .github/workflows/ci.yml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test仓库 B、C 把这个文件原样再抄一遍。问题:
- 维护爆炸:改一处逻辑要同步改 N 个仓库。
- 容易漂移:各仓库悄悄改出差异,CI 行为不再一致。
- 审计困难:无法保证"所有服务都跑了同样的检查"。
解决方案:把这段 CI 抽成一个可复用工作流,集中放一个仓库(或同仓库的固定位置),大家 uses: 调用。
2. 被调用方:用 on: workflow_call 声明
一个 workflow 文件,只要把触发事件写成 on: workflow_call,就变成"可被调用"的。它用 inputs 接收参数、secrets 接收密钥、outputs 返回结果——这三者和我们熟悉的 on: push workflow 写法很像,只是语义变成"被别人传进来 / 传出去"。
# .github/workflows/ci-reusable.yml
name: 可复用 CI
on:
workflow_call:
inputs:
node-version:
description: "Node 版本"
required: false
default: "20"
type: string
secrets:
NPM_TOKEN:
required: false
outputs:
test-result:
description: "测试是否通过"
value: ${{ jobs.test.outputs.passed }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.run.outputs.passed }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }} # 读入调用方传来的参数
- run: npm ci
- id: run
run: |
npm test && echo "passed=true" >> "$GITHUB_OUTPUT"要点:
on: workflow_call是开关,没有它就只能被事件触发,不能uses:调用。inputs.*.type支持string/number/boolean/choice等,类型不对调用方会报错。${{ inputs.node-version }}在被调用方内部读取入参(注意是inputs,不是github)。secrets段声明"我需要哪些密钥",值由调用方提供。outputs段把某个 job 的输出再"升格"为整个 workflow 的返回值,供调用方的下游 job 使用。
workflow_call.inputs 的 type 必填(string/number/boolean/choice 等)。传错类型 GitHub 会直接拒绝调用,这比"全当字符串"更安全,能在入口拦住脏数据。
3. 调用方:用 uses: + with: / secrets:
在调用方 workflow 里,把"调用可复用工作流"当成一个 job 来写。这个 job 没有自己的 steps,只有 uses: 指向被调用文件,并用 with: 传参、secrets: 传密钥。
# .github/workflows/caller.yml
name: 调用 CI
on: [push]
jobs:
ci:
uses: ./.github/workflows/ci-reusable.yml # 同仓库内的相对路径
with:
node-version: "20"
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
report:
runs-on: ubuntu-latest
needs: ci
steps:
- run: echo "CI 结果:${{ needs.ci.outputs.test-result }}"关键点:
uses: ./.github/workflows/ci-reusable.yml是相对本仓库根目录的路径(注意开头的./)。- 也可以跨仓库调用:
uses: owner/repo/.github/workflows/ci.yml@ref(ref可以是分支/tag/commit,强烈建议锁定到 tag 或 commit 以保证可复现)。 with:对应被调用方的inputs;secrets:对应其secrets。- 调用产生的 job(上例
ci)同样可以被needs:引用,并通过needs.ci.outputs.*读取返回值。
4. secrets: inherit:一键透传全部密钥
如果你的可复用工作流需要调用方拥有的几乎所有密钥,一个个列举很麻烦,而且调用方改了密钥名还得同步。可以用 secrets: inherit 把调用方所有 secrets 原样透传给被调用工作流:
jobs:
ci:
uses: ./.github/workflows/ci-reusable.yml
secrets: inherit # 调用方全部 secrets 自动可见secrets: inherit 会暴露调用方的所有密钥给被调用工作流。如果被调用方来自外部仓库、或你不完全信任它的实现,这就是过度授权。最小权限原则:能只传 NPM_TOKEN 就别 inherit。另外注意,被调用方 workflow_call.secrets 里声明了 required: true 的密钥必须提供,否则调用失败。
5. 双向传参完整示例
把被调用方和调用方拼在一起看最清楚。下面假设一个"统一部署"可复用工作流:
# .github/workflows/deploy-reusable.yml (被调用方)
name: 可复用部署
on:
workflow_call:
inputs:
environment:
description: "目标环境"
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
outputs:
url:
description: "部署后的访问地址"
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.done.outputs.url }}
steps:
- id: done
run: |
echo "部署到 ${{ inputs.environment }},使用令牌 ${DEPLOY_TOKEN:0:4}****"
echo "url=https://${{ inputs.environment }}.example.com" >> "$GITHUB_OUTPUT"# .github/workflows/release.yml (调用方)
name: 发布
on:
push:
tags: ["v*"]
jobs:
deploy-staging:
uses: ./.github/workflows/deploy-reusable.yml
with:
environment: staging
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
deploy-prod:
needs: deploy-staging
uses: ./.github/workflows/deploy-reusable.yml
with:
environment: production
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
notify:
runs-on: ubuntu-latest
needs: deploy-prod
steps:
- run: echo "生产环境地址:${{ needs.deploy-prod.outputs.url }}"release.yml 自己没有任何部署逻辑,全部委托给 deploy-reusable.yml,并通过 with/secrets 传入差异化的环境和密钥,通过 needs.*.outputs.url 拿回结果。以后部署流程要升级,只改 deploy-reusable.yml 一个文件即可。
6. 与 composite action 的区别(先铺垫)
你可能会问:这和后面要讲的 composite action 听起来很像——都是"复用"。先给一句定位,下一章再展开:
- 可复用工作流(本章):复用的是一整套 workflow,内部可以包含多个 job、有独立触发与权限模型。适合"统一 CI/CD 流水线"这种粒度的复用。
- composite action(下章):复用的是一组 step,本身只是单个 job 里的一个步骤。适合"安装依赖 + 跑 lint"这种步骤序列的封装。
一句话:workflow 复用"流程",action 复用"步骤"。它们可以组合使用——可复用工作流内部也能调用 action。
7. 嵌套与层级限制
可复用工作流可以调用另一个可复用工作流(嵌套),但 GitHub 对层级有上限:最多 4 层嵌套(直接调用算一层)。更深会直接报错。日常实践中,把"统一 CI"做成一层、业务仓库调用它,就足够了,不必追求层层套娃。
- 跨仓库、跨团队的"公司级统一流水线" → 可复用工作流(
workflow_call)。 - 单仓库内"每个 job 都要做的固定准备动作" → 更轻量的 composite action(下章)。
- 两者都走版本化(锁 tag/commit),避免上游一改全崩。
8. 小结
- 用
on: workflow_call声明可复用工作流;通过inputs收参、secrets收密钥、outputs返回结果。 - 调用方把"调用"当成一个 job:
uses: 路径+with:+secrets:;secrets: inherit可透传全部密钥(慎用)。 - 调用产生的 job 可被
needs:引用,结果用needs.<job>.outputs.*读取;支持跨仓库调用(建议锁 tag)。 - 与 composite action 的区别:workflow 复用流程(含多 job),action 复用步骤。
- 嵌套最多约 4 层,别过度套娃。
可复用工作流很强大,但它复用的是"整条流水线"。如果我们只想复用一小段 step 序列(比如"装好依赖并跑 lint"),用 workflow 反而太重。下一章我们就来自己写 Action——尤其是最轻量好用的 composite action。
- 写一个被调用工作流
greet-reusable.yml:声明workflow_call,入参name(string,必填),step 里打印Hello, ${{ inputs.name }}。再写一个调用方call.yml,传name: "GitHub"。 - 在调用方里,用
secrets: inherit调用一个需要密钥的可复用工作流。说明什么场景下你不应该用inherit。 - 判断:可复用工作流能否在内部包含多个 job?能否被另一个可复用工作流调用?嵌套层数有上限吗?
- 下面调用方为什么可能报错?如何改正(假设被调用方要求
environment必填)?
jobs:
d:
uses: ./.github/workflows/deploy-reusable.yml
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}