diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b707c1f --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,61 @@ +name: Publish + +# Publishes tag v 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 + + 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 + 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[@]}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5f18100 --- /dev/null +++ b/.github/workflows/release.yml @@ -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, 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" diff --git a/CLAUDE.md b/CLAUDE.md index 69badbf..cf95d75 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -117,11 +124,7 @@ Co-Authored-By: Claude Sonnet 4.5 " 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