Repository navigation
ci: automate release tagging, pub.dev publishing and GitHub release - #122
Conversation
release.yml validates version files and the CHANGELOG section on release PRs, and pushes tag v<version> when the version change merges. publish.yml publishes the tag to pub.dev via OIDC and creates the GitHub release from the CHANGELOG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| - name: Publish to pub.dev | ||
| run: flutter pub publish --force |
There was a problem hiding this comment.
Re-running this job can't recover if the GitHub release step fails. Publishing and the GitHub release are steps in one job. If the publish succeeds and gh release create then fails (API error, tag problem, etc.), re-running runs flutter pub publish --force again. That fails because the version already exists on pub.dev, so the job never gets back to the release step. This contradicts the CLAUDE.md line "If either workflow fails, re-run it from the Actions tab."
Suggestion: split into two jobs, publish, then github-release with needs: publish. "Re-run failed jobs" then retries only the release job. Side benefit: only publish needs id-token: write and the pub.dev environment, and only github-release needs contents: write. Right now one job holds both permissions while running third-party actions.
There was a problem hiding this comment.
Agreed, the re-run couldn't recover: pub.dev rejects a second upload of the same version. Fixed in bee1622 by splitting into two jobs:
publish:contents: read+id-token: write, runs in thepub.devenvironmentgithub-release:needs: publish,contents: writeonly, no third-party actions
"Re-run failed jobs" now retries only the release. The CLAUDE.md line now says to use "Re-run failed jobs", which matches the new behavior.
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| VERSION="${GITHUB_REF_NAME#v}" | ||
| awk -v h="## $VERSION" '$0==h{f=1;next} /^## [0-9]/{if(f)exit} f' CHANGELOG.md > notes.md |
There was a problem hiding this comment.
Release notes aren't checked until after the irreversible publish. The CHANGELOG check only lives in release.yml. A tag pushed by hand (the tag pattern allows it) skips it, and if the heading format is slightly off, this awk silently writes an empty notes.md. By then the package is already on pub.dev.
Suggestion: extract the notes before the publish step and fail if the file is empty, e.g. [ -s notes.md ] || { echo "No CHANGELOG section for $VERSION"; exit 1; }. If you split the jobs, the notes can be uploaded as an artifact or extracted again in the release job.
There was a problem hiding this comment.
Good catch on hand-pushed tags. Fixed in bee1622: a Check tag step now runs before setup and publish. It extracts the notes and fails with CHANGELOG.md has no '## X.Y.Z' section if the file is empty. github-release extracts them again (same awk), which is simpler than passing an artifact between jobs.
A note on the heading scenario: on the normal path, release.yml (grep -qxF "## $VERSION") and this awk ($0==h) use the same exact-line match, so the two can't disagree. The real gap was hand-pushed tags skipping release.yml, and the new check covers that.
Tested locally: the v3.8.0 tag passes (16 note lines), and a tag for a version with no CHANGELOG section fails.
| # On a release PR it only validates, so a missing CHANGELOG section or a version mismatch fails before merge. | ||
| # Run manually (workflow_dispatch) to retry a failed release from the branch's latest commit. | ||
| on: | ||
| workflow_dispatch: |
There was a problem hiding this comment.
workflow_dispatch can be started from any branch. The branches: filter only applies to push/pull_request. Anyone with write access can pick a feature branch in the "Run workflow" dropdown. If that branch's pubspec.yaml has an unreleased version, the workflow tags that unmerged commit and starts a publish. The pub.dev environment reviewer is the only safeguard, and the reviewer sees just a version number.
Suggestion: guard the Push tag step, e.g.
if: >-
steps.version.outputs.version && github.event_name != 'pull_request' &&
(github.ref == 'refs/heads/master' || (startsWith(github.ref, 'refs/heads/') && endsWith(github.ref, '.x')))There was a problem hiding this comment.
Agreed this was a hole, but guarding the Push tag step only covers one way in: anyone with the same write access can git push origin v9.9.9 from any commit, which triggers publish.yml directly and bypasses release.yml. So I fixed it in two places instead (bee1622):
- Removed
workflow_dispatch. Its only job was retrying a failed tag push, and re-running the original push run (which ran on master or N.x) already does that. publish.ymlnow checks branch ancestry before publishing: the tagged commit must be reachable fromorigin/masterororigin/N.x, otherwise the job fails withvX.Y.Z is not on master or a maintenance branch. This covers manual dispatches and hand-pushed tags. (It also avoidsendsWith(github.ref, '.x'), which would match a branch likefeature/foo.x.)
Tested locally: v3.8.0 on master passes, a commit on 3.x passes, and a commit that's only on a feature branch fails.
Optionally, a tag ruleset restricting who can create v* tags would add another layer. That's a repo settings change, so it isn't in this PR.
jaeopt
left a comment
There was a problem hiding this comment.
Looks good to me with a few suggestions from AI.
- publish and github-release are separate jobs, so a failed release step can be retried without re-running the publish - check the tag is on master or N.x and has a CHANGELOG section before the irreversible publish (covers tags pushed by hand) - drop workflow_dispatch from release.yml; re-running the push run retries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Automates everything after the release PR merges. It follows the Swift SDK's
release.yml(swift-sdk #664), adapted for pub.dev, which only accepts publishing from a run triggered by a tag push.sequenceDiagram participant PR as Release PR (pubspec version bump) participant R as release.yml participant P as publish.yml PR->>R: pull_request: validate only Note over R: version format, package_info.dart + README match,<br/>CHANGELOG "## X.Y.Z" exists, pub publish --dry-run PR->>R: merge to master / N.x R->>P: push tag vX.Y.Z (CI_USER_TOKEN) Note over P: environment pub.dev → reviewer approves P->>P: job publish: check tag is on master/N.x + CHANGELOG notes,<br/>flutter pub publish --force (OIDC, no stored secret) P->>P: job github-release: gh release create from CHANGELOG section<br/>(-rc → prerelease; N.x → not latest)CI_USER_TOKEN: a tag pushed with the defaultGITHUB_TOKENdoes not start other workflows, sopublish.ymlwould never run.[0-9]+.xpattern, so 3.x patches work once these files are cherry-picked to3.x. Releases from those branches are created with--latest=false.Evidence
Simulated
release.yml's validation against a clean checkout ofmaster(3.8.0):Ran
release.ymlon GitHub via throwaway draft PR #123 (4.0.0 bump, closed unmerged): run 38761027841The first real run will be
4.0.0. Since that's a real release, the pipeline gets checked in two steps:release.ymlvalidates the 4.0.0 release PR (including the pub.dev dry-run) before it merges, andpublish.ymlwaits for a reviewer's approval before publishing. If authentication fails, nothing is published and the run can be retried.Before merging: one-time setup
pub.dev/packages/optimizely_flutter_sdk/admin, under Automated publishing, enable publishing from GitHub Actions: repositoryoptimizely/optimizely-flutter-sdk, tag patternv{{version}}, and enable Require GitHub Actions environment with the namepub.dev.pub.devand add required reviewers. Without reviewers, merging a version bump publishes with no approval step.CI_USER_TOKENcan create tags in this repo (contents: write).After this one login, releases need no pub.dev credentials: GitHub issues each run a short-lived OIDC token that pub.dev checks against the step 1 settings. Manual
flutter pub publishkeeps working as a fallback.Until step 1 is done,
publish.ymlfails at authentication. That's harmless: nothing is published, and the run can be retried afterwards.Merge Danger
Door: two-way for the workflows (revert the files). Each publish is one-way: pub.dev versions can't be deleted, only retracted.
Blast Radius:
pubspec.yamlchanges on PRs or merges tomaster/N.x, or onv*tags. Normal CI is unaffected./releaseClaude skill becomes redundant. Running it after a merge would race the pipeline, and the second publish fails because the version already exists. Remove it once4.0.0ships through this pipeline.🤖 Generated with Claude Code