Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Publish

# Publishes tag v<version> to pub.dev, then creates the GitHub release from that version's CHANGELOG section.
# pub.dev authenticates the run with GitHub's OIDC token (no stored secret), and only accepts tag-push runs.
# One-time setup: enable "Publishing from GitHub Actions" on pub.dev (repository optimizely/optimizely-flutter-sdk,
# tag pattern v{{version}}, required environment pub.dev), and add required reviewers to the pub.dev environment here.
# Separate jobs so "Re-run failed jobs" can retry the GitHub release without publishing again.
on:
push:
tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ]

jobs:
publish:
runs-on: ubuntu-latest
environment: pub.dev
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
# Tags can also be pushed by hand, bypassing release.yml, so check before the irreversible publish
- name: Check tag
run: |
VERSION="${GITHUB_REF_NAME#v}"
git branch -r --contains "$GITHUB_SHA" | grep -qE '^ *origin/(master|[0-9]+\.x)$' \
|| { echo "$GITHUB_REF_NAME is not on master or a maintenance branch"; exit 1; }
awk -v h="## $VERSION" '$0==h{f=1;next} /^## [0-9]/{if(f)exit} f' CHANGELOG.md > notes.md
[ -s notes.md ] || { echo "CHANGELOG.md has no '## $VERSION' section"; exit 1; }
# Exchanges the OIDC token for a pub.dev credential used by `flutter pub publish`
- uses: dart-lang/setup-dart@v1
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
channel: 'stable'
- name: Publish to pub.dev
run: flutter pub publish --force
Comment on lines +38 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the pub.dev environment
  • github-release: needs: publish, contents: write only, 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.


github-release:
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Create GitHub release
env:
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

FLAGS=()
[[ "$VERSION" == *-* ]] && FLAGS+=(--prerelease)
git merge-base --is-ancestor "$GITHUB_SHA" origin/master || FLAGS+=(--latest=false)
gh release create "$GITHUB_REF_NAME" --verify-tag --title "Release $VERSION" \
--notes-file notes.md "${FLAGS[@]}"
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release

# Merging a version change in pubspec.yaml into master (or a maintenance branch like 3.x)
# pushes tag v<version>, which triggers publish.yml.
# On a release PR it only validates, so a missing CHANGELOG section or a version mismatch fails before merge.
# To retry a failed tag push, re-run that run from the Actions tab.
on:
push:
branches: [ master, '[0-9]+.x' ]
paths: [ pubspec.yaml ]
pull_request:
branches: [ master, '[0-9]+.x' ]
paths: [ pubspec.yaml ]

permissions:
contents: read

jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Validate version
id: version
run: |
VERSION=$(sed -nE 's/^version: *([^ ]+).*/\1/p' pubspec.yaml)
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || { echo "Invalid version: $VERSION"; exit 1; }
grep -qF "version = '$VERSION';" lib/package_info.dart || { echo "lib/package_info.dart does not match $VERSION"; exit 1; }
grep -qF "optimizely_flutter_sdk: ^$VERSION" README.md || { echo "README.md does not match $VERSION"; exit 1; }
if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null; then
echo "v$VERSION already released"; exit 0
fi
grep -qxF "## $VERSION" CHANGELOG.md || { echo "CHANGELOG.md has no '## $VERSION' section"; exit 1; }
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- uses: subosito/flutter-action@v2
if: steps.version.outputs.version
with:
flutter-version: '3.44.0'
channel: 'stable'
- name: Validate package
if: steps.version.outputs.version
run: flutter pub publish --dry-run
- name: Push tag
if: steps.version.outputs.version && github.event_name != 'pull_request'
env:
# A tag pushed with GITHUB_TOKEN would not trigger publish.yml
GH_TOKEN: ${{ secrets.CI_USER_TOKEN }}
VERSION: ${{ steps.version.outputs.version }}
run: gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/v$VERSION" -f sha="$GITHUB_SHA"
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ Claude will execute the full workflow:
- ✅ Commit and push
- ✅ Create PR

### After the release PR merges (automated)

- `release.yml` validates the version (pubspec.yaml, package_info.dart, README.md, CHANGELOG section) on the PR, and on merge pushes tag `vX.Y.Z`
- `publish.yml` runs on the tag: publishes to pub.dev (waits for approval in the `pub.dev` environment), then creates the GitHub release from the CHANGELOG section
- Versions with a `-` suffix (e.g. `4.0.0-rc.1`) are released as prereleases
- If a workflow fails, use "Re-run failed jobs" in the Actions tab. A failed GitHub release step is retried without publishing again

### Manual Release

```bash
Expand All @@ -117,11 +124,7 @@ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
git push -u origin prepare-X.Y.Z
gh pr create --title "[FSSDK-XXXXX] prepare for release X.Y.Z"

# 6. After merge, create GitHub release
gh release create vX.Y.Z --title "Release X.Y.Z" --draft --target master

# 7. Publish to pub.dev
flutter pub publish
# 6. After merge, the tag, pub.dev publish and GitHub release are automated (see above)
```

### CHANGELOG Format
Expand Down
Loading