From a789c69411de79b28cfe36c8e986ec380df7e917 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Fri, 11 Sep 2026 19:01:16 +0100 Subject: [PATCH 1/3] chore: add Prepare Release and Publish Release workflows Replaces the monolithic release workflow with a two-step process: - Prepare Release: bumps version, updates changelog, opens PR, creates GitHub draft release - Publish Release: promotes draft, tags master/main, publishes to distribution, posts Slack Ref: SDK release process team agreement 2026-09-11 --- .github/workflows/prepare-release.yml | 109 +++++++++++++------------- .github/workflows/publish-release.yml | 77 ++++++++++++++++++ .github/workflows/publish.yml | 41 ---------- 3 files changed, 132 insertions(+), 95 deletions(-) create mode 100644 .github/workflows/publish-release.yml delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index e2416fc2b..b93f76e3a 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -7,6 +7,10 @@ on: description: 'Version number (e.g., 3.6.0 or 1.0.0-beta1)' required: true type: string + ticket: + description: 'Jira ticket number (e.g., 1234 for SDK-1234)' + required: true + type: string permissions: contents: write @@ -20,67 +24,51 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} + - name: Validate version format + run: | + if ! [[ "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "::error::Invalid version format. Use semantic versioning (e.g., 3.6.0 or 3.6.0-beta1)" + exit 1 + fi + - name: Update Changelog id: update_changelog run: | + version="${{ github.event.inputs.version }}" changelog_file="CHANGELOG.md" - - # Function to extract content between two patterns, including the first pattern - extract_between() { - awk "/^## \[$1\]/{p=1;print;next} /^## \[/{p=0} p" "$3" - } - - # Get the unreleased content - unreleased_content=$(extract_between "Unreleased" "[0-9]" "$changelog_file") - + + unreleased_content=$(awk '/^## \[Unreleased\]/{p=1;next} /^## \[/{p=0} p' "$changelog_file") + if [ -z "$unreleased_content" ]; then echo "No unreleased changes found in $changelog_file" exit 1 fi - - # Get the current version - current_version=$(grep -oP "^## \[\K[0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9]+)?(?=\])" "$changelog_file" | head -n1) - new_version="${{ github.event.inputs.version }}" - - # Validate version format (now includes beta versions) - if ! [[ $new_version =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then - echo "Invalid version format. Please use semantic versioning (e.g., 3.6.0 or 1.0.0-beta1)" - exit 1 - fi - - echo "new_version=${new_version}" >> $GITHUB_OUTPUT - - # Create temporary file + temp_file=$(mktemp) - - # Preserve header and write new content { - # Preserve the header (first 4 lines) head -n 4 "$changelog_file" echo "## [Unreleased]" echo "" - echo "## [$new_version]" - # Remove the "## [Unreleased]" line from unreleased_content if it exists + echo "## [$version]" echo "$unreleased_content" | sed '1{/^## \[Unreleased\]/d}' echo "" - # Get the rest of the file starting from the first version entry sed -n '/^## \[[0-9]/,$p' "$changelog_file" } > "$temp_file" - - # Replace original file mv "$temp_file" "$changelog_file" + echo "notes<> "$GITHUB_OUTPUT" + echo "$unreleased_content" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + - name: Update Version Files run: | - # Create temp file for build.gradle updates + version="${{ github.event.inputs.version }}" + + sed -i "s/libraryVersion = '.*'/libraryVersion = '$version'/" iterableapi/build.gradle + sed -i "s/versionName=\".*\"/versionName=\"$version\"/" iterableapi/build.gradle + temp_gradle=$(mktemp) - - # Update iterableapi/build.gradle - libraryVersion and versionName - sed -i "s/libraryVersion = '.*'/libraryVersion = '${{ github.event.inputs.version }}'/" iterableapi/build.gradle - sed -i "s/versionName=\".*\"/versionName=\"${{ github.event.inputs.version }}\"/" iterableapi/build.gradle - - # Update buildConfigField version in defaultConfig section - awk -v version="${{ github.event.inputs.version }}" ' + awk -v version="$version" ' /defaultConfig {/,/}/ { if ($0 ~ /buildConfigField.*ITERABLE_SDK_VERSION/) { print " buildConfigField \"String\", \"ITERABLE_SDK_VERSION\", \"\\\"" version "\\\"\"" @@ -89,34 +77,47 @@ jobs: } { print }' iterableapi/build.gradle > "$temp_gradle" && cat "$temp_gradle" > iterableapi/build.gradle rm "$temp_gradle" - - # Update iterableapi-ui/build.gradle - sed -i "s/libraryVersion = '.*'/libraryVersion = '${{ github.event.inputs.version }}'/" iterableapi-ui/build.gradle - - # Update sample app versions - sed -i "s/implementation 'com.iterable:iterableapi:[^']*'/implementation 'com.iterable:iterableapi:${{ github.event.inputs.version }}'/" sample-apps/inbox-customization/app/build.gradle - sed -i "s/implementation 'com.iterable:iterableapi-ui:[^']*'/implementation 'com.iterable:iterableapi-ui:${{ github.event.inputs.version }}'/" sample-apps/inbox-customization/app/build.gradle + + sed -i "s/libraryVersion = '.*'/libraryVersion = '$version'/" iterableapi-ui/build.gradle + + sed -i "s/implementation 'com.iterable:iterableapi:[^']*'/implementation 'com.iterable:iterableapi:$version'/" sample-apps/inbox-customization/app/build.gradle + sed -i "s/implementation 'com.iterable:iterableapi-ui:[^']*'/implementation 'com.iterable:iterableapi-ui:$version'/" sample-apps/inbox-customization/app/build.gradle - name: Create Pull Request uses: peter-evans/create-pull-request@4e1beaa7521e8b457b572c090b25bd3db56bf1c5 # v5 with: token: ${{ secrets.GITHUB_TOKEN }} - title: "Prepare for Release ${{ steps.update_changelog.outputs.new_version }}" + title: "SDK-${{ github.event.inputs.ticket }}: Prepare for Release ${{ github.event.inputs.version }}" body: | - # Prepare for Release ${{ steps.update_changelog.outputs.new_version }} - + # Prepare for Release ${{ github.event.inputs.version }} + ## SDK Release Checklist - [ ] CHANGELOG.md updated - [ ] Version numbers updated in build.gradle files: - - [ ] iterableapi/build.gradle (libraryVersion, versionName, and ITERABLE_SDK_VERSION) + - [ ] iterableapi/build.gradle (libraryVersion, versionName, ITERABLE_SDK_VERSION) - [ ] iterableapi-ui/build.gradle (libraryVersion) - [ ] sample-apps/inbox-customization/app/build.gradle (both dependencies) - [ ] README.md reviewed (if needed) - [ ] Sample apps verified - [ ] All tests passing - [ ] Documentation updated (if needed) - - branch: "prepare-for-release-${{ steps.update_changelog.outputs.new_version }}" - commit-message: "Prepare for release ${{ steps.update_changelog.outputs.new_version }}" + branch: "release/SDK-${{ github.event.inputs.ticket }}-${{ github.event.inputs.version }}" + commit-message: "[SDK-${{ github.event.inputs.ticket }}]: Prepare for release ${{ github.event.inputs.version }}" labels: release - delete-branch: true \ No newline at end of file + delete-branch: true + + - name: Create Draft GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version="${{ github.event.inputs.version }}" + notes="${{ steps.update_changelog.outputs.notes }}" + + if gh release view "$version" &>/dev/null; then + echo "Draft release $version already exists, skipping." + else + gh release create "$version" \ + --draft \ + --title "$version" \ + --notes "${notes:-See CHANGELOG.md for release notes.}" + fi diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 000000000..245d84e6f --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,77 @@ +name: Publish Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 3.6.0)' + required: true + type: string + +env: + VERSION: ${{ github.event.inputs.version }} + +permissions: + contents: write + +jobs: + publish-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Verify release is ready + run: | + if ! grep -qF "## [$VERSION]" CHANGELOG.md; then + echo "::error::CHANGELOG.md has no entry for [$VERSION]. Merge the prepare-release PR to master before running this workflow." + exit 1 + fi + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: "17" + distribution: "temurin" + cache: gradle + + - name: Import GPG Key + run: | + echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 -d | gpg --batch --import + gpg --batch --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --export-secret-keys > ~/.gnupg/secring.gpg + GPG_KEY_ID="${{ secrets.GPG_KEY_ID }}" + echo "SHORT_GPG_KEY_ID=${GPG_KEY_ID: -8}" >> $GITHUB_ENV + + - name: Publish to Sonatype Central + run: ./gradlew clean :iterableapi:assemble :iterableapi-ui:assemble publishAllPublicationsToCentralPortal + env: + ORG_GRADLE_PROJECT_SIGNING_KEY_ID: ${{ env.SHORT_GPG_KEY_ID }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.GPG_PASSPHRASE }} + ORG_GRADLE_PROJECT_SIGNING_SECRET_KEY_RING_FILE: /home/runner/.gnupg/secring.gpg + ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + + - name: Publish draft GitHub release and tag master + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release edit "$VERSION" \ + --draft=false \ + --target "$(git rev-parse HEAD)" \ + --latest + + - name: Slack notification + run: | + release_url="https://github.com/${{ github.repository }}/releases/tag/$VERSION" + payload=$(jq -n \ + --arg text ":android: *Android SDK ${VERSION}* has been released. <${release_url}|View release notes>." \ + '{"text": $text}') + curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "${{ secrets.SLACK_WEBHOOK }}" + + - name: Slack failure notification + if: failure() + run: | + run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}" + payload=$(jq -n \ + --arg text ":alert: Android SDK release ${VERSION} failed (attempt ${{ github.run_attempt }}). Run: ${run_url}" \ + '{"text": $text}') + curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "${{ secrets.SLACK_WEBHOOK }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index f422a9280..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Publish to Sonatype Central - -on: - push: - branches: - - sonatype-publish # for testing the action - tags: - - "*.*.*" # Match semantic version tags like 3.5.13 - - "*.*.*-*" # Match pre-release tags like 3.6.0-beta1, 3.5.12-stg - workflow_dispatch: # Allow manual trigger - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - java-version: "17" - distribution: "temurin" - cache: gradle - - - name: Import GPG Key and Create Secring - run: | - echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 -d | gpg --batch --import - # Create secring.gpg for Gradle signing plugin compatibility - gpg --batch --pinentry-mode loopback --passphrase "${{ secrets.GPG_PASSPHRASE }}" --export-secret-keys > ~/.gnupg/secring.gpg - # Extract last 8 characters for short format - GPG_KEY_ID="${{ secrets.GPG_KEY_ID }}" - echo "SHORT_GPG_KEY_ID=${GPG_KEY_ID: -8}" >> $GITHUB_ENV - - - name: Build and Publish to Sonatype Central - run: ./gradlew clean :iterableapi:assemble :iterableapi-ui:assemble publishAllPublicationsToCentralPortal - env: - ORG_GRADLE_PROJECT_SIGNING_KEY_ID: ${{ env.SHORT_GPG_KEY_ID }} - ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.GPG_PASSPHRASE }} - ORG_GRADLE_PROJECT_SIGNING_SECRET_KEY_RING_FILE: /home/runner/.gnupg/secring.gpg - ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} From 090f89976c8c244146a65003f968ba72cdc980f2 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Wed, 16 Sep 2026 16:34:21 +0100 Subject: [PATCH 2/3] fix: pin checkout to default branch, use notes-file, anchor CHANGELOG grep - Add ref: master/main to actions/checkout in all prepare/publish workflows so workflows always operate on the default branch regardless of dispatch ref - Replace shell-injection-prone ${{ steps...outputs.notes }} pattern with --notes-file using $RUNNER_TEMP/release-notes.md (safe from backticks/quotes in changelog content) - Treat empty [Unreleased] section as a hard error in prepare-release - Fix CHANGELOG verification grep: grep -qE "^## \[VERSION\]" (anchored, prevents substring matches and prefix collisions like 3.1.0 vs 3.1.0-rc1) --- .github/workflows/prepare-release.yml | 13 ++++++------- .github/workflows/publish-release.yml | 4 +++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index b93f76e3a..9d9176bd2 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -22,6 +22,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: master token: ${{ secrets.GITHUB_TOKEN }} - name: Validate version format @@ -40,10 +41,13 @@ jobs: unreleased_content=$(awk '/^## \[Unreleased\]/{p=1;next} /^## \[/{p=0} p' "$changelog_file") if [ -z "$unreleased_content" ]; then - echo "No unreleased changes found in $changelog_file" + echo "::error::No unreleased changes found in $changelog_file. Add release notes to the [Unreleased] section before running this workflow." exit 1 fi + notes_file="$RUNNER_TEMP/release-notes.md" + printf '%s' "$unreleased_content" > "$notes_file" + temp_file=$(mktemp) { head -n 4 "$changelog_file" @@ -56,10 +60,6 @@ jobs: } > "$temp_file" mv "$temp_file" "$changelog_file" - echo "notes<> "$GITHUB_OUTPUT" - echo "$unreleased_content" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - name: Update Version Files run: | version="${{ github.event.inputs.version }}" @@ -111,7 +111,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | version="${{ github.event.inputs.version }}" - notes="${{ steps.update_changelog.outputs.notes }}" if gh release view "$version" &>/dev/null; then echo "Draft release $version already exists, skipping." @@ -119,5 +118,5 @@ jobs: gh release create "$version" \ --draft \ --title "$version" \ - --notes "${notes:-See CHANGELOG.md for release notes.}" + --notes-file "$RUNNER_TEMP/release-notes.md" fi diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 245d84e6f..4a57ff5ce 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -19,10 +19,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: master - name: Verify release is ready run: | - if ! grep -qF "## [$VERSION]" CHANGELOG.md; then + if ! grep -qE "^## \[$VERSION\]" CHANGELOG.md; then echo "::error::CHANGELOG.md has no entry for [$VERSION]. Merge the prepare-release PR to master before running this workflow." exit 1 fi From 56317c599c3536f843495fe72c1d12320b9e23f1 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 17 Sep 2026 15:53:38 +0100 Subject: [PATCH 3/3] chore: use iterable-sdk-release GitHub App for all release auth Replace SDK_RELEASE_TOKEN (iOS) and GITHUB_TOKEN (all repos) with a short-lived installation token from the iterable-sdk-release GitHub App, generated via actions/create-github-app-token@v1. Benefits: - App token triggers CI on PRs it creates (GITHUB_TOKEN cannot) - 1h TTL vs long-lived PAT - Workflow-scoped permissions so we can push .github/workflows/ files Required credentials (repo variable + secret, or set at org level): vars.ITERABLE_SDK_RELEASE_APP_ID secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY --- .github/workflows/prepare-release.yml | 12 +++++++++--- .github/workflows/publish-release.yml | 9 ++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9d9176bd2..278f2d3df 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -20,10 +20,16 @@ jobs: prepare-release: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.ITERABLE_SDK_RELEASE_APP_ID }} + private-key: ${{ secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY }} + - uses: actions/checkout@v4 with: ref: master - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token }} - name: Validate version format run: | @@ -86,7 +92,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@4e1beaa7521e8b457b572c090b25bd3db56bf1c5 # v5 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token }} title: "SDK-${{ github.event.inputs.ticket }}: Prepare for Release ${{ github.event.inputs.version }}" body: | # Prepare for Release ${{ github.event.inputs.version }} @@ -108,7 +114,7 @@ jobs: - name: Create Draft GitHub Release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | version="${{ github.event.inputs.version }}" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 4a57ff5ce..a5102da6b 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -18,9 +18,16 @@ jobs: publish-release: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.ITERABLE_SDK_RELEASE_APP_ID }} + private-key: ${{ secrets.ITERABLE_SDK_RELEASE_APP_PRIVATE_KEY }} + - uses: actions/checkout@v4 with: ref: master + token: ${{ steps.app-token.outputs.token }} - name: Verify release is ready run: | @@ -54,7 +61,7 @@ jobs: - name: Publish draft GitHub release and tag master env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | gh release edit "$VERSION" \ --draft=false \