pub.dev 自动发布上手:Dart 官方推荐的 OIDC 流程怎么配
基于 Dart 官方 automated publishing 文档整理的一篇实操教程,讲清如何用 GitHub Actions、Google Cloud Build 和 GCP service account 在不保存长期发布凭证的前提下自动发布到 pub.dev,并补充 tag 规则、权限边界和常见坑。
如果你现在还在 CI 里放一个长期有效的 PUB_TOKEN,然后靠 secret 去发包,那这篇 Dart 官方文档很值得立刻看一遍。
我刚把 https://dart.dev/tools/pub/automated-publishing 完整过了一遍。先说最重要的结论:Dart 官方现在更推荐的 pub.dev 自动发布思路,不是长期 token,而是运行时用 OIDC 或 Google Cloud 身份换一个短期令牌,再执行发布。
截至 2026 年 4 月 23 日,这篇官方文档页面底部注明:文档反映的是 Dart 3.11.0,页面最后更新时间是 2025-09-15。这篇我不写成逐段翻译,直接整理成一篇能照着做的教程。
主线我先讲 GitHub Actions,因为这是官方给得最完整、也最容易落地的方案。后面再补 Google Cloud Build 和“别的平台怎么套这条思路”。
先说结论
如果你只想先抓住最关键的判断,记住下面这几条就够了。
pub.dev的首个版本不能自动发布,第一次还是要手动跑dart pub publish- 代码在 GitHub,就优先用
pub.dev后台配置 + GitHub Actions reusable workflow - 构建跑在 Google Cloud 上,就优先用 service account + Cloud Build
- 跑在别的 CI,只要支持 OIDC 或 SAML,也可以通过 Workload Identity Federation 接进来
- 只有在没有身份系统可用时,才退回到导出 service account key 文件
本质上,官方是在把发布链路往“短期凭证”方向推。前期看起来比老办法多几步,但长期安全性明显更好。
你应该先知道的事
自动发布不是“扔一个 YAML 进仓库就完了”,它前面有几个前提。
第一,你的 package 必须已经在 pub.dev 上存在。官方文档写得很直接:现在只能自动发布已有 package。如果这是你第一次发这个包,还是得手动执行一次:
dart pub publish
第二,你需要有足够权限。
如果你走 GitHub Actions 方案,你得是这个 package 的 uploader,或者是所属 publisher 的 admin。走 service account 方案也是一样,你得先有权限去改 package 的 Admin 页面配置。
第三,你得先想清楚 tag 规则。
官方这套自动发布,不是“代码一 push 就发”,而是把仓库、tag pattern 和 pubspec.yaml 里的版本号绑在一起。比如你在 pub.dev 里配置的是:
v{{version}}
那 pubspec.yaml 里如果写的是:
version: 1.2.3
你最终推的 tag 就应该是:
v1.2.3
如果这里没对齐,自动发布就不会按你预期工作。
方案一:GitHub Actions,官方最省事的路
如果你的包代码托管在 GitHub,这基本就是官方首推方案。
它的核心思路不是你自己去生成一个长期 token,然后塞进 GitHub Secrets。而是:
- 先在
pub.dev的 packageAdmin页面开启 GitHub 自动发布 - 配好 GitHub 仓库名和 tag pattern
- 让 GitHub Actions 在运行时签发一个 OIDC token
pub.dev验证这个临时身份、仓库来源和 tag/version 匹配关系- 验证通过以后,再允许发布
这条链路最值钱的地方就是:不用长期保存一个可反复复用的发布凭证。
第一步:在 pub.dev 后台开启 GitHub 自动发布
到你的 package 页面,进入 Admin。
然后在 Automated publishing 这一块里,点击开启 GitHub Actions 发布。这里会要求你填两项:
- GitHub 仓库
- tag pattern
仓库名要填成 <organization>/<repository> 这种格式。比如仓库地址是:
https://github.com/dart-lang/pana
那你填的就应该是:
dart-lang/pana
tag pattern 则必须包含 {{version}}。最常见的写法就是:
v{{version}}
如果你的 monorepo 里放了多个 package,官方建议给每个包单独配 tag pattern。比如包名叫 my_package_name,可以配成:
my_package_name-v{{version}}
这样多个包不会互相撞 tag。
第二步:直接用官方 reusable workflow
如果你的发布流程很标准,没有额外生成步骤,那最简单的做法不是自己拼一堆 action,而是直接复用 dart-lang/setup-dart 提供的发布 workflow。
一个最小可用版本可以写成这样:
# .github/workflows/publish.yml
name: Publish to pub.dev
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
publish:
permissions:
id-token: write
uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
# with:
# working-directory: path/to/package/within/repository
这里最容易漏掉的是:
permissions:
id-token: write
没有它,GitHub 就不会给这个 job 发 OIDC token。没有 OIDC token,后面的自动发布链路也就跑不起来。
还有一点很关键:on.push.tags 里的规则,必须和你在 pub.dev 后台配的 tag pattern 对齐。
比如后台写的是 v{{version}},那 workflow 里就应该匹配 v1.2.3 这种 tag。不要后台写一套,工作流再匹配另一套,不然你会以为“配置都对了,怎么就是没触发”。
第三步:你真正的发布动作,其实是 push tag
按官方这套设计,发布不是点个按钮,而通常是:
- 改
pubspec.yaml里的版本号 - 提交代码
- 打 tag
- push tag
比如:
git tag v1.2.3
git push origin v1.2.3
只要 tag 命中了 pattern,而且 pubspec.yaml 的版本号也一致,GitHub Actions 就会跑起来。
官方文档还特别提醒了一点:pub.dev 只接受由 push tag 触发的 GitHub Actions 发布。 如果你的 workflow 是被别的事件触发的,比如普通 push、手动触发或者别的事件,pub.dev 会拒绝这次发布。
所以别把这个链路理解成“只要跑到 dart pub publish --force 就行”。触发方式本身也是校验条件的一部分。
如果我发布前还要生成代码怎么办
这也是官方文档专门提到的一个现实问题。
如果你的包在发布前还要做额外步骤,比如:
- 跑代码生成
- 构建某些中间产物
- 先跑一轮测试
- 重新整理文档
那你就不一定适合只用默认的 reusable workflow 了。
官方给的建议很实在:如果生成结果可以安全地提交进仓库,那优先直接提交。
原因很简单。这样最容易保证“仓库里的内容”和“实际发布出去的内容”是一致的。以后出问题,回溯也更直接。
如果你确实不想把生成产物提交进仓库,那就自己写 workflow,但核心还是同一件事:让 GitHub job 拿到 OIDC 身份,然后在 job 里直接执行发布。
可以整理成下面这种结构:
# .github/workflows/publish.yml
name: Publish to pub.dev
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
publish:
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- name: Install dependencies
run: dart pub get
- name: Run tests
run: dart test
- name: Generate files
run: dart run build_runner build --delete-conflicting-outputs
- name: Publish
run: dart pub publish --force
重点不是 build_runner 这条命令本身,而是你可以在真正发布前,把你需要的步骤都串起来。
安全加固:tag 保护和 GitHub Environment
官方文档这里给了两层安全建议,我觉得都值得看。
第一层是 tag protection。因为只要有人能 push 命中规则的 tag,理论上他就能触发发布。你可以在 GitHub 里限制谁能创建这类 tag。
第二层是 GitHub Deployment Environment。这一层更灵活,我更推荐。
如果你在 pub.dev 后台要求必须带某个 GitHub environment,比如 pub.dev,那 reusable workflow 里就要显式传这个环境名:
jobs:
publish:
permissions:
id-token: write
uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1
with:
environment: pub.dev
这样你就可以在 GitHub 仓库设置里给这个 environment 配 reviewer 或其他保护规则。谁就算有 push 权限,也不能随便绕过 environment protection 直接发版。
如果你的包是多人维护的,这一步很有价值。
方案二:Google Cloud Build
如果你的发布流程本来就跑在 Google Cloud 上,官方给的另一条标准路线是:用 Google Cloud service account 去代表 package 发布。
这条线的核心链路是:
- 创建一个专门用于发布的 service account
- 在
pub.dev的 packageAdmin页面把这个 service account 注册进去 - 让 Cloud Build 在运行时模拟这个 service account
- 生成一个 audience 是
https://pub.dev的临时 OIDC token - 把这个临时 token 交给
dart pub - 最后执行
dart pub publish --force
注意,这里也不是长期保存一个凭证文件,而是运行时动态生成短期 token。
第一步:创建并注册发布 service account
官方示例里的 service account 是这样建的:
gcloud iam service-accounts create pub-dev \
--description='Service account to be impersonated when publishing to pub.dev' \
--display-name='pub-dev'
创建完成以后,你会拿到一个邮箱,形如:
pub-dev@$PROJECT_ID.iam.gserviceaccount.com
然后到 package 的 Admin 页面里,开启 Google Cloud Service account 自动发布,把这个邮箱填进去。
这一步做完以后,任何能 impersonate 这个 service account 的身份,都有能力发布你的 package。所以这里的重点从来不只是“能不能发”,还包括“谁能冒充它发”。
第二步:给 Cloud Build 足够的权限
官方文档这里写得很清楚,要做三件事。
第一,启用 IAM Service Account Credentials API:
gcloud services enable iamcredentials.googleapis.com
第二,找到当前项目的 PROJECT_NUMBER:
gcloud projects describe $PROJECT_ID --format='value(projectNumber)'
第三,把 roles/iam.serviceAccountTokenCreator 授给 Cloud Build 默认 service account,让它能 impersonate 前面那个发布账号:
gcloud iam service-accounts add-iam-policy-binding \
pub-dev@$PROJECT_ID.iam.gserviceaccount.com \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator
这一步如果没配对,Cloud Build 就算跑起来,也拿不到能给 pub.dev 用的临时 token。
第三步:写 cloudbuild.yaml
官方给了一份非常直接的 cloudbuild.yaml 范例,核心就是先打印临时 token,再在下一步里发布:
# cloudbuild.yaml
steps:
- id: Create temporary token
name: gcr.io/cloud-builders/gcloud
volumes:
- name: temporary-secrets
path: /secrets
script: |
gcloud auth print-identity-token \
--impersonate-service-account=pub-dev@$PROJECT_ID.iam.gserviceaccount.com \
--audiences=https://pub.dev \
--include-email > /secrets/temporary-pub-token.txt
env:
- PROJECT_ID=$PROJECT_ID
- id: Publish to pub.dev
name: dart
volumes:
- name: temporary-secrets
path: /secrets
script: |
cat /secrets/temporary-pub-token.txt | dart pub token add https://pub.dev
dart pub publish --force
这里有两个细节很关键。
第一,--audiences=https://pub.dev 不能少。这个值是在告诉 pub.dev:“这个 token 就是发给你的。”
第二,官方文档明确提醒:不要把临时 token 放进 /workspace。 因为 /workspace 是代码 checkout 的地方。把 token 丢在那里,会增加你误把它带进发布内容或者后续步骤的风险。用单独的 volume,比如 /secrets,才是更稳的做法。
第四步:配置 Cloud Build Trigger
有了 cloudbuild.yaml 以后,还要配触发器。
官方文档这里的建议也很明确:
- 用仓库里的
/cloudbuild.yaml - 触发器不要额外指定一个 service account,优先先用默认 Cloud Build service account
- 仔细想清楚“谁能触发这个 build”,因为能触发 build,很多时候就等于能发版
如果你想把边界再收紧一点,官方还建议你考虑 Cloud Build approvals。这样触发以后不会立即执行,而是等有 Approver 角色的人审批。
如果你的包比较关键,或者团队里不只你一个人,这一步值得上。
方案三:GitLab CI、Azure DevOps、自建 Runner 怎么办
官方文档没有给每个平台都单独写一套模板,但它给了一个更通用的方向:只要你的运行环境支持 OIDC 或 SAML,就优先看 Workload Identity Federation。
这句话的实际意思是:如果你跑在 GitLab CI、Azure DevOps,或者某些自建环境里,只要你能把当前作业身份接到 Google Cloud,然后让它 impersonate 那个已经注册到 pub.dev 的 service account,后面的发布动作和 Cloud Build 本质上是一样的。
也就是:
- 拿到平台自己的临时身份
- 通过 federation 让这个身份能代表 GCP service account
- 再生成面向
https://pub.dev的短期 token - 调
dart pub token add - 最后
dart pub publish --force
官方文档还举了 AWS EC2 metadata service 的例子,本质上也是这条思路。
所以如果你不在 GitHub,也不在 Cloud Build,不要第一反应就退回长期 key 文件。先看你的 CI 能不能走 federation。
最后兜底:导出 service account key 文件
官方也给了一个兜底方案:导出 service account key 文件。
这条路当然能用,但我不建议把它当默认方案。因为这个 key file 本质上就是长期凭证。只要它泄漏、误传、被打印进日志,后果都比短期 token 难收拾得多。
如果你真的只能这么干,官方文档给的流程是:
- 先生成 key file:
gcloud iam service-accounts keys create key-file.json \
--iam-account=pub-dev@$PROJECT_ID.iam.gserviceaccount.com
- 用这个 key file 激活 service account:
gcloud auth activate-service-account --key-file=key-file.json
- 生成一个给
pub.dev用的临时 token,再喂给dart pub:
gcloud auth print-identity-token \
--audiences=https://pub.dev \
| dart pub token add https://pub.dev
- 最后发布:
dart pub publish --force
官方文档对这个方案的态度其实也很明确:能用 Workload Identity Federation,就优先用 federation。 因为它避免了长期 secret。
一组最容易踩的坑
这篇文档里,我觉得最容易让人翻车的不是命令,而是下面这些边界条件。
1. 首次发布不能自动化
如果你的包还从没上过 pub.dev,那就先手动发第一版。这个前提很多人会忽略。
2. tag 和 pubspec.yaml 版本号没对齐
比如你 pubspec.yaml 是 1.2.3,但 tag 打成了 release-1.2.3,而后台配的是 v{{version}}。这种情况下,链路本身没坏,但就是不会按你预期匹配。
3. GitHub Actions 漏了 id-token: write
这个是最常见的配置错误之一。workflow 文件看上去挺完整,结果就是发不出去。先看这行权限是不是漏了。
4. workflow 不是由 push tag 触发的
pub.dev 会拒绝这种发布。别拿分支 push、普通 CI 任务或者别的事件去试。
5. monorepo 里多个 package 共用一个 tag 规则
这很容易让你后面分不清“这个 tag 到底是给谁发的”。多个包就配多个 tag pattern,必要时多个 workflow 分开。
6. 谁能推匹配 tag,谁理论上就能触发发布
所以 GitHub 的 tag protection、Deployment Environment,Cloud Build 的 approval,本质上都不是“锦上添花”,而是在补权限边界。
7. 临时 token 或 key file 落进了不该落的地方
Cloud Build 文档这里已经把话说得很明白了:不要随手扔进 /workspace。这不是代码风格问题,是泄漏面大小的问题。
我会怎么选
如果让我按工程上最省事的顺序给建议,我会这么排。
- 代码在 GitHub:优先
pub.dev后台 + GitHub Actions reusable workflow - 构建在 GCP:优先 Cloud Build + service account impersonation
- 构建在别的 CI,但支持 OIDC 或 SAML:优先 Workload Identity Federation
- 完全没有更好的身份系统:最后才考虑导出 service account key
本质上这不是“哪种 YAML 更好看”,而是哪条链路里长期凭证更少。
最后给一个最短执行清单
如果你只想先把 GitHub 这条路跑通,可以直接照这个顺序做:
- 先手动把 package 的第一个版本发布到
pub.dev - 到 package 的
Admin页面开启 GitHub 自动发布 - 仓库和 tag pattern 填好,最常见是
v{{version}} - 仓库里加发布 workflow,并确认有
id-token: write - 修改
pubspec.yaml版本号 - 打对应 tag,例如
v1.2.3 - push tag
- 去 GitHub Actions 看日志,再去
pub.dev的audit-log看发布记录
如果你能把这 8 步跑通,这条官方自动发布链路基本就算接好了。
一句话总结就是:Dart 官方现在推荐的 pub.dev 自动发布,不再是往 CI 里塞一个长期 token,而是尽量用 OIDC 或 service account 在运行时换出短期身份,再执行发布。 这套东西前期看起来比老办法绕一点,但长期看更安全,也更适合正式维护的 package。