GitHub Actions 可调用工作流:跨仓库复用与团队协作实践 前言我以前维护多个仓库的 GitHub Actions 时最怕遇到一类需求把所有项目的 CI 都升级一遍。表面上只是把 Node 版本从 18 改到 20或者把actions/cache的写法调整一下。真正动起来才发现十几个仓库里的 workflow 长得差不多但又不完全一样。有的多了安全扫描有的多了 artifact 上传有的部署前还要跑一段自定义脚本。每个仓库复制一份 YAML短期看起来省事时间长了就会变成维护负担。GitHub Actions 的可调用工作流也就是workflow_call解决的是这一类重复。它允许把一段完整 workflow 抽出来让其他 workflow 在 job 层级调用。这样一套 Node CI、一套安全扫描、一套镜像构建、一套部署流程都可以沉淀在共享仓库里业务仓库只保留触发条件和少量参数。不过这类能力不能只看语法。真正落地时最容易出问题的地方在边界什么逻辑应该抽成可调用工作流什么逻辑应该做成复合 Action哪些 secrets 可以传哪些 environment 不能从调用方直接传跨仓库引用应该锁版本还是追main。这些问题想清楚以后可调用工作流才会变成团队工程资产而不是另一层看不懂的 YAML。一、可调用工作流解决的是流程级复用workflow_call是 GitHub Actions 的一种触发方式。一个 workflow 声明了workflow_call后就可以被其他 workflow 调用。它和普通push、pull_request、workflow_dispatch不一样不会因为代码推送自动运行而是等待调用方通过uses明确引用。可调用工作流的文件位置也有要求。它必须放在仓库根目录下的.github/workflows目录里不能再往下放子目录。很多团队会自然想把共享流程整理成下面这种结构.github/workflows/ ci/ node.yml security/ dependency-scan.yml deploy/ production.yml这个结构看起来清楚但 GitHub Actions 不支持这样的 workflow 子目录。更稳的做法是用文件名前缀表达分类.github/workflows/ ci-node-reusable.yml ci-python-reusable.yml security-dependency-scan-reusable.yml deploy-azure-webapp-reusable.yml调用方式也要注意。可调用工作流在 job 层级使用不是在 step 层级使用。jobs:node-ci:uses:my-org/shared-workflows/.github/workflows/ci-node-reusable.ymlv1with:node-version:20working-directory:apps/web如果写到 steps 里那就变成调用 action 的语法了。这里的差异非常关键复合 Action 是 step 级别复用可调用工作流是 job 或 workflow 级别复用。我会这样区分两者。复用对象适合机制例子一整套 CI 流程可调用工作流安装依赖、lint、test、build、上传 artifact一个完整部署 job可调用工作流下载构建产物、OIDC 登录云平台、部署到环境几个重复步骤复合 Action读取 package 版本、安装内部 CLI、发送通知某个独立工具动作复合 Action格式化消息、执行脚本、生成变更摘要当前仓库触发条件普通 workflowpush、pull_request、workflow_dispatch这张表能避免很多后期维护问题。把完整 CI 做成复合 Action会导致调用方还要自己管理 job、权限、缓存、artifact 和并发把几个步骤硬塞进可调用工作流又会让一个小动作变得过重。二、从一个 Node CI 开始抽抽可调用工作流时我不建议一开始就抽生产部署。部署涉及权限、environment、审批和云平台登录风险比较高。更合适的起点是 CI例如 Node 项目的 lint、test、build。下面这份 workflow 就可以作为共享 Node CI 的起点。# .github/workflows/ci-node-reusable.ymlname:Reusable Node CIon:workflow_call:inputs:node-version:description:Node.js versionrequired:falsetype:stringdefault:20working-directory:description:Directory that contains package.jsonrequired:falsetype:stringdefault:.run-tests:description:Whether to run testsrequired:falsetype:booleandefault:trueupload-artifact:description:Whether to upload build outputrequired:falsetype:booleandefault:falseoutputs:artifact-name:description:Name of uploaded artifactvalue:${{jobs.node-ci.outputs.artifact-name}}jobs:node-ci:runs-on:ubuntu-latestdefaults:run:working-directory:${{inputs.working-directory}}outputs:artifact-name:${{steps.artifact-meta.outputs.artifact-name}}steps:-name:Checkout repositoryuses:actions/checkoutv4-name:Setup Node.jsuses:actions/setup-nodev4with:node-version:${{inputs.node-version}}cache:npmcache-dependency-path:${{inputs.working-directory}}/package-lock.json-name:Install dependenciesrun:npm ci-name:Run lintrun:npm run lint-name:Run testsif:inputs.run-testsrun:npm test-name:Buildrun:npm run build-name:Prepare artifact nameid:artifact-metarun:echo artifact-namedist-${GITHUB_SHA}$GITHUB_OUTPUT-name:Upload artifactif:inputs.upload-artifactuses:actions/upload-artifactv4with:name:${{steps.artifact-meta.outputs.artifact-name}}path:${{inputs.working-directory}}/dist这份示例里我把 lint、test、build 放在同一个 job 里。它不一定是性能最高的写法但作为可调用工作流的第一版更容易维护。很多团队刚开始抽象时会把 lint、test、build 拆成多个 job再用 inputs 控制开关最后遇到 job 被跳过以后下游needs也被影响的问题。第一版先保证流程稳定再考虑并行优化。共享 workflow 一旦被多个仓库引用稳定性比 YAML 是否足够漂亮更重要。业务仓库里的调用方会很薄。# .github/workflows/ci.ymlname:CIon:pull_request:push:branches:-mainpermissions:contents:readjobs:web:uses:my-org/shared-workflows/.github/workflows/ci-node-reusable.ymlv1with:node-version:20working-directory:apps/webrun-tests:trueupload-artifact:trueapi:uses:my-org/shared-workflows/.github/workflows/ci-node-reusable.ymlv1with:node-version:20working-directory:apps/apirun-tests:trueupload-artifact:false调用方只保留触发条件、权限和项目参数。共享仓库负责 CI 细节。后面要升级 Node 版本、缓存策略、artifact 命名规则都可以在共享 workflow 里统一处理。三、inputs、secrets 和 outputs 要分清workflow_call的参数化能力很强但也容易滥用。一个可调用工作流如果有十几个输入参数调用方会很痛苦如果所有 secrets 都通过inherit传进去安全边界又会变宽。我会先按用途拆。类型放什么示例inputs非敏感配置Node 版本、工作目录、是否上传产物、环境名secrets敏感值NPM token、Webhook、云平台私密配置outputs下游需要的数据artifact 名称、镜像 tag、版本号、部署地址variables非敏感环境信息应用名、region、资源组、订阅 IDenvironment审批和部署边界staging、productionsecrets 可以显式传递。jobs:publish:uses:my-org/shared-workflows/.github/workflows/npm-publish-reusable.ymlv1with:package-directory:packages/uisecrets:NPM_TOKEN:${{secrets.NPM_TOKEN}}同一个 organization 或 enterprise 里也可以使用secrets:inherit这个写法很省事但我不会作为默认选择。它会把调用方当前可用的 secrets 传给被调用工作流。共享 workflow 如果只需要一个NPM_TOKEN就只传这个 token如果只需要 Slack Webhook就只传 Webhook。能显式传递就不要整包继承。outputs 的写法也要留意三层映射。step 先写$GITHUB_OUTPUTjob 再暴露 step 输出workflow 再暴露 job 输出。# .github/workflows/build-image-reusable.ymlname:Reusable Docker Buildon:workflow_call:inputs:image-name:required:truetype:stringoutputs:image-tag:description:Docker image tagvalue:${{jobs.build.outputs.image-tag}}jobs:build:runs-on:ubuntu-latestoutputs:image-tag:${{steps.meta.outputs.image-tag}}steps:-uses:actions/checkoutv4-name:Generate image tagid:metarun:echo image-tag${{inputs.image-name}}:${GITHUB_SHA}$GITHUB_OUTPUT-name:Build imagerun:docker build-t ${{steps.meta.outputs.image-tag}} .调用方通过needs.job_id.outputs.name读取jobs:build:uses:my-org/shared-workflows/.github/workflows/build-image-reusable.ymlv1with:image-name:my-appdeploy:runs-on:ubuntu-latestneeds:buildsteps:-name:Print image tagrun:echo ${{needs.build.outputs.image-tag}}这里的 job id 是调用方自己的build不是被调用工作流内部的 job id。这个细节刚开始很容易搞混。四、矩阵可以配合可调用工作流但别一上来玩复杂可调用工作流可以和 matrix 配合。比如同一套 Node CI要在多个包、多个 Node 版本里跑。name:Monorepo CIon:pull_request:permissions:contents:readjobs:package-ci:strategy:fail-fast:falsematrix:include:-package:apps/webnode-version:20-package:apps/adminnode-version:20-package:packages/uinode-version:20uses:my-org/shared-workflows/.github/workflows/ci-node-reusable.ymlv1with:node-version:${{matrix.node-version}}working-directory:${{matrix.package}}run-tests:trueupload-artifact:false这个写法很适合 monorepo。每个包都走同一套 CI参数从 matrix 里传进去。共享 workflow 保持统一调用方只维护包列表。不过我不建议一开始就在 reusable workflow 里面再套很复杂的 matrix。调用方有 matrix被调用 workflow 内部也有 matrix再加 outputs很快就会变得难排查。尤其是 matrix workflow outputs有多个成功任务都设置输出时最终取值会受完成顺序和输出规则影响不适合承接关键部署数据。我的处理习惯是matrix 放在调用方时用来控制多个项目、多个版本、多个环境。reusable workflow 内部保持相对简单。关键输出尽量不要依赖多个 matrix job 汇总。如果确实需要汇总结果单独增加一个汇总 job。CI 可以并行部署要克制。尤其是 production不要轻易用 matrix 一次性并行推多个环境。五、嵌套调用要少用GitHub Actions 支持可调用工作流嵌套但这不代表应该频繁嵌套。当前限制是最多四层也就是顶层调用 workflow再往下最多三层可复用工作流。比如caller workflow → reusable workflow A → reusable workflow B → reusable workflow C再往下就不合适了。即使平台允许排查也会非常痛苦。一个 CI 失败你要从业务仓库跳到共享 workflow A再跳到 B再跳到 C看每一层的 inputs、secrets、permissions 和 outputs。团队里不是每个人都愿意这样查。权限也不能在下游工作流里扩大。上层 workflow 给了contents: read下层不能凭空拿到contents: write。secrets 也只会传给直接调用的下一层如果 A 调用 BB 再调用 CC 只有在 B 显式传递后才能拿到对应 secret。我会给团队定一个很简单的规则可调用工作流可以嵌套但默认不要超过两层。比较合理的结构是业务仓库 workflow → 共享 CI workflow或者业务仓库 workflow → 共享部署 workflow → 共享通知 workflow如果需要第三层通常说明共享 workflow 设计得太碎或者复合 Action 更合适。六、共享工作流仓库不要用子目录放 workflow团队做共享 workflow 仓库时经常会自然想按功能建目录shared-workflows/ .github/ workflows/ ci/ node.yml security/ dependency-scan.yml deploy/ k8s.yml这个结构在 GitHub Actions 里不能直接作为 workflow 使用。.github/workflows下面必须是 workflow 文件不能通过子目录引用。我会改成这种结构shared-workflows/ .github/ workflows/ ci-node-reusable.yml ci-python-reusable.yml security-dependency-scan-reusable.yml security-codeql-reusable.yml deploy-k8s-reusable.yml deploy-azure-webapp-reusable.yml docs/ ci-node.md deploy-k8s.md examples/ node-service-ci.yml k8s-deploy.ymlworkflow 文件放在.github/workflows顶层文档和示例放到docs、examples里。这样既符合平台要求也能保持仓库可读性。共享仓库还要像产品一样维护。维护项建议版本用v1、v2tag 或 release 分支示例每个 workflow 至少有一份最小调用示例文档写清 inputs、secrets、outputs、权限要求测试准备 sandbox 仓库跑真实调用审查修改共享 workflow 必须走 PR变更日志breaking change 单独记录引用策略业务仓库不要直接引用main共享 workflow 的影响范围比普通业务代码更大。一个没测试过的 YAML 改动可能会让多个仓库同一天 CI 全挂。共享仓库越核心越要有版本和回滚策略。七、权限和安全要放在设计里可调用工作流一旦跨仓库使用权限就不能只靠默认值。workflow 里的permissions要尽量写清楚。CI 里大多数时候只需要permissions:contents:read如果要上传包、创建 release、写 PR 评论、推送镜像再按需增加权限。不要为了省事直接给write-all。部署类 reusable workflow 要更谨慎。生产部署通常会同时涉及id-token: write用于 OIDC。contents: read用于 checkout。environment触发生产审批和环境 secrets。云平台 federated credential限制仓库、分支和环境。比如name:Reusable Production Deployon:workflow_call:inputs:environment-name:required:truetype:stringartifact-name:required:truetype:stringpermissions:contents:readid-token:writejobs:deploy:runs-on:ubuntu-latestenvironment:${{inputs.environment-name}}steps:-name:Download artifactuses:actions/download-artifactv4with:name:${{inputs.artifact-name}}-name:Cloud login with OIDCrun:./scripts/cloud-login.sh-name:Deployrun:./scripts/deploy.sh这个示例还不完整但能说明一件事部署边界要放在 workflow 层。复合 Action 里可以封装登录命令或部署脚本但 production environment、permissions、OIDC 这种东西应该由 job 级别显式声明。还有一点要特别留意。可调用工作流如果使用environmentenvironment secrets 的行为和普通 secrets 不一样。调用方不能通过workflow_call把 environment secrets 直接传进去。生产部署里我会让调用方和被调用方的 environment 设计保持非常明确不让 secrets 在多层 workflow 里绕来绕去。八、迁移时不要一次性全改如果团队已经有很多仓库每个仓库都有自己的 CI/CD迁移到可调用工作流时不要一次性全部替换。这样做出问题时很难判断是共享 workflow 的 bug还是某个仓库自己的差异。我会按四步来做。第一步收集现有 workflow找重复度最高的部分。通常最先抽的是 lint、test、build。第二步选一个低风险仓库试点。不要选最复杂、最核心的生产项目。先找一个中等复杂度项目验证 reusable workflow 的 inputs 是否够用。第三步把旧 workflow 和新 workflow 并行跑一段时间。比如新 workflow 先只在workflow_dispatch或测试分支上跑确认结果一致后再替换主流程。第四步逐步扩大到更多仓库。每迁一个仓库就记录它额外需要的参数和例外情况。如果例外越来越多说明共享 workflow 抽得太早或者抽象边界不对。迁移过程中我会特别关注这些问题检查项为什么要看CI 耗时有没有明显变化共享 workflow 可能引入额外步骤缓存是否命中工作目录和 cache-dependency-path 容易配置错secrets 是否传得太宽inherit容易扩大权限artifact 名称是否稳定下游 job 依赖输出时容易出问题权限是否最小化默认权限可能不符合安全要求失败日志是否可读共享 workflow 过度封装会影响排查调用方是否容易理解业务仓库维护者要能看懂参数含义可调用工作流不是为了让 YAML 消失。业务仓库里仍然应该能看出这条 CI 做了什么、用了哪个共享版本、传了哪些参数。抽象的目标是减少重复不是制造黑盒。总结GitHub Actions 可调用工作流最适合解决跨仓库重复流程。它通过workflow_call把完整 job 编排抽出来让业务仓库用少量参数调用共享流程。用得好团队可以统一 CI、测试、安全扫描和部署模板用得太随意也会带来版本、权限、secrets 和排查成本。我会按这几个原则落地完整流程用可调用工作流几个步骤用复合 Action。可调用工作流文件直接放在.github/workflows下不放子目录。跨仓库引用使用稳定 tag 或 commit SHA不直接追main。inputs 放非敏感参数secrets 显式传递少用整包继承。嵌套调用保持克制默认不要超过两层。生产部署要结合 environment、OIDC 和最小权限。共享 workflow 仓库要有文档、示例、测试和版本管理。迁移时从低风险 CI 开始不要直接抽生产部署。真正成熟的可调用工作流应该让调用方更轻也让维护者更清楚。它不是把复杂度藏起来而是把重复流程放到一个可以统一审查、统一升级、统一回滚的位置。