diff --git a/.github/workflows/prep-plugin-releases.yml b/.github/workflows/prep-plugin-releases.yml new file mode 100644 index 0000000..3322336 --- /dev/null +++ b/.github/workflows/prep-plugin-releases.yml @@ -0,0 +1,131 @@ +name: Prepare Plugin Releases + +on: + workflow_dispatch: + inputs: + proxy_version: + description: 'New Framework Proxy Version (x.y.z)' + required: true + ios_version: + description: 'iOS SDK Version (x.y.z - optional)' + required: false + android_version: + description: 'Android SDK Version (x.y.z - optional)' + required: false + test_mode: + description: 'Test mode (adds -test suffix to branches)' + type: boolean + default: false + react_native: + description: 'Update React Native' + type: boolean + default: true + cordova: + description: 'Update Cordova' + type: boolean + default: true + flutter: + description: 'Update Flutter' + type: boolean + default: true + capacitor: + description: 'Update Capacitor' + type: boolean + default: true + +env: + GITHUB_TOKEN: ${{ secrets.MOBILE_PLUGIN_RELEASE_PAT }} + +jobs: + centralized-plugin-releases: + runs-on: macos-26-xlarge + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + + - name: Setup Gemini CLI + uses: google-github-actions/run-gemini-cli@main + with: + gemini_api_key: ${{ secrets.GEMINI_API_KEY }} + continue-on-error: true + + - name: Update plugins + id: update + run: | + # Build plugin filter args + PLUGIN_ARGS="" + if [ "${{ github.event.inputs.react_native }}" != "true" ]; then + PLUGIN_ARGS="$PLUGIN_ARGS --skip-react-native" + fi + if [ "${{ github.event.inputs.cordova }}" != "true" ]; then + PLUGIN_ARGS="$PLUGIN_ARGS --skip-cordova" + fi + if [ "${{ github.event.inputs.flutter }}" != "true" ]; then + PLUGIN_ARGS="$PLUGIN_ARGS --skip-flutter" + fi + if [ "${{ github.event.inputs.capacitor }}" != "true" ]; then + PLUGIN_ARGS="$PLUGIN_ARGS --skip-capacitor" + fi + + # Add test mode flag if enabled + if [ "${{ github.event.inputs.test_mode }}" = "true" ]; then + PLUGIN_ARGS="$PLUGIN_ARGS --test" + fi + + bash ./scripts/update_all_plugins.sh \ + "${{ github.event.inputs.proxy_version }}" \ + "${{ github.event.inputs.ios_version }}" \ + "${{ github.event.inputs.android_version }}" \ + $PLUGIN_ARGS + env: + GITHUB_TOKEN: ${{ secrets.MOBILE_PLUGIN_RELEASE_PAT }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + + - name: Display release summary + run: | + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + ## ๐Ÿ“ฆ Plugin Release Summary + + ### Proxy Version + **${{ github.event.inputs.proxy_version }}** + + ### SDK Versions + - iOS: ${{ github.event.inputs.ios_version || 'from proxy' }} + - Android: ${{ github.event.inputs.android_version || 'from proxy' }} + + ### Plugin PRs + + | Plugin | Version | Status | + |--------|---------|--------| + | React Native | ${{ steps.update.outputs.react_native_version }} | ${{ steps.update.outputs.react_native_pr_url }} | + | Cordova | ${{ steps.update.outputs.cordova_version }} | ${{ steps.update.outputs.cordova_pr_url }} | + | Flutter | ${{ steps.update.outputs.flutter_version }} | ${{ steps.update.outputs.flutter_pr_url }} | + | Capacitor | ${{ steps.update.outputs.capacitor_version }} | ${{ steps.update.outputs.capacitor_pr_url }} | + + --- + + ### Next Steps + 1. Review each plugin PR (links above) + 2. Merge when ready + + โš ๏ธ **Validation Note:** Changelogs across plugins will be similar since they update the same underlying SDKs. This is expected and correct. + EOF + + - name: Notify Slack (Success) + if: success() + run: | + curl -X POST ${{ secrets.MOBILE_SLACK_WEBHOOK }} \ + -H 'Content-Type: application/json' \ + -d '{ + "text": "โœ… Centralized plugin releases complete for proxy v${{ github.event.inputs.proxy_version }}\nโ€ข React Native v${{ steps.update.outputs.react_native_version }}: ${{ steps.update.outputs.react_native_pr_url }}\nโ€ข Cordova v${{ steps.update.outputs.cordova_version }}: ${{ steps.update.outputs.cordova_pr_url }}\nโ€ข Flutter v${{ steps.update.outputs.flutter_version }}: ${{ steps.update.outputs.flutter_pr_url }}\nโ€ข Capacitor v${{ steps.update.outputs.capacitor_version }}: ${{ steps.update.outputs.capacitor_pr_url }}\n@mobile-team" + }' || true + + - name: Notify Slack (Failure) + if: failure() + run: | + curl -X POST ${{ secrets.MOBILE_SLACK_WEBHOOK }} \ + -H 'Content-Type: application/json' \ + -d '{ + "text": "โŒ Centralized plugin release failed\nProxy version: ${{ github.event.inputs.proxy_version }}\nWorkflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n<@crow>" + }' || true diff --git a/.github/workflows/prep-proxy-release.yml b/.github/workflows/prep-proxy-release.yml new file mode 100644 index 0000000..e83fe1c --- /dev/null +++ b/.github/workflows/prep-proxy-release.yml @@ -0,0 +1,107 @@ +name: Prepare Proxy Release + +on: + workflow_dispatch: + +env: + BUNDLE_PATH: vendor/bundle + +jobs: + prepare-release: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - name: Detect SDK versions + id: detect + run: | + ./scripts/detect_sdk_versions.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Display version summary + run: | + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + ## ๐Ÿ“ฆ Proxy Release Summary + + | Component | Current | New | + |-----------|---------|-----| + | **Proxy** | - | **${{ steps.detect.outputs.proxy_version }}** | + | **iOS SDK** | - | ${{ steps.detect.outputs.ios_version }} | + | **Android SDK** | - | ${{ steps.detect.outputs.android_version }} | + + **Bump Type:** `${{ steps.detect.outputs.bump_type }}` + + --- + + Preparing release PR... + EOF + + - name: Prepare release + run: | + ./scripts/prep_proxy_release.sh + + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + Release ${{ steps.detect.outputs.proxy_version }} + + - Updated iOS SDK to ${{ steps.detect.outputs.ios_version }} + - Updated Android SDK to ${{ steps.detect.outputs.android_version }} + branch: proxy-${{ steps.detect.outputs.proxy_version }} + delete-branch: true + title: Release ${{ steps.detect.outputs.proxy_version }} + body: | + ## Proxy Release ${{ steps.detect.outputs.proxy_version }} + + Automated release PR for proxy version bump. + + ### SDK Updates + - iOS SDK: ${{ steps.detect.outputs.ios_version }} + - Android SDK: ${{ steps.detect.outputs.android_version }} + + --- + + ๐Ÿค– Generated by prep-proxy-release workflow + labels: | + release + automated pr + + - name: Update summary with PR link + if: steps.cpr.outputs.pull-request-number + run: | + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + + ## โœ… Release PR Created + + **PR:** [#${{ steps.cpr.outputs.pull-request-number }}](${{ steps.cpr.outputs.pull-request-url }}) + + ### Next Steps + 1. Review the PR + 2. Merge when ready + 3. Tag will be created automatically + 4. Run `prep-plugin-releases` workflow with version `${{ steps.detect.outputs.proxy_version }}` + EOF + + - name: Notify Slack (Success) + if: success() && steps.cpr.outputs.pull-request-number + run: | + curl -X POST ${{ secrets.MOBILE_SLACK_WEBHOOK }} \ + -H 'Content-Type: application/json' \ + -d '{ + "text": "โœ… Proxy release PR created: ${{ steps.cpr.outputs.pull-request-url }}\nVersion: ${{ steps.detect.outputs.proxy_version }}\niOS: ${{ steps.detect.outputs.ios_version }} | Android: ${{ steps.detect.outputs.android_version }}" + }' || true + + - name: Notify Slack (Failure) + if: failure() + run: | + curl -X POST ${{ secrets.MOBILE_SLACK_WEBHOOK }} \ + -H 'Content-Type: application/json' \ + -d '{ + "text": "โŒ Proxy release preparation failed\nWorkflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n<@crow>" + }' || true diff --git a/.github/workflows/prep-release.yml b/.github/workflows/prep-release.yml deleted file mode 100644 index 4914ed8..0000000 --- a/.github/workflows/prep-release.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: Prepare Plugin Releases - -on: - workflow_dispatch: - inputs: - proxy_version: - description: 'New Framework Proxy Version (x.y.z)' - required: true - pattern: '^\d+\.\d+\.\d+$' - ios_version: - description: 'iOS SDK Version (x.y.z)' - required: false - pattern: '^\d+\.\d+\.\d+$' - android_version: - description: 'Android SDK Version (x.y.z)' - required: false - pattern: '^\d+\.\d+\.\d+$' - -env: - GITHUB_TOKEN: ${{ secrets.MOBILE_PLUGIN_RELEASE_PAT }} - -jobs: - trigger-releases: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Fetch current proxy version (based on latest tags) - id: current_proxy - run: | - # Fetch the top 2 tags so we can fall back to the previous one if the latest equals the new version - readarray -t PROXY_TAGS < <(gh api repos/urbanairship/airship-mobile-framework-proxy/tags?per_page=2 \ - --header "Authorization: token $GITHUB_TOKEN" --jq '.[].name') - LATEST_PROXY_TAG="${PROXY_TAGS[0]}" - PREVIOUS_PROXY_TAG="${PROXY_TAGS[1]}" - - # If the latest tag equals the requested proxy_version and there's a previous tag, use that for comparison - if [ "$LATEST_PROXY_TAG" = "${{ github.event.inputs.proxy_version }}" ] && [ -n "$PREVIOUS_PROXY_TAG" ]; then - echo "current_proxy_version=$PREVIOUS_PROXY_TAG" >> $GITHUB_OUTPUT - else - echo "current_proxy_version=$LATEST_PROXY_TAG" >> $GITHUB_OUTPUT - fi - - - name: Determine increment type - id: increment_type - run: | - IFS='.' read -r OLD_MAJOR OLD_MINOR OLD_PATCH <<< "${{ steps.current_proxy.outputs.current_proxy_version }}" - IFS='.' read -r NEW_MAJOR NEW_MINOR NEW_PATCH <<< "${{ github.event.inputs.proxy_version }}" - - INC_TYPE="patch" - if [ "$NEW_MAJOR" -gt "$OLD_MAJOR" ]; then - INC_TYPE="major" - elif [ "$NEW_MINOR" -gt "$OLD_MINOR" ]; then - INC_TYPE="minor" - fi - - echo "increment_type=$INC_TYPE" >> $GITHUB_OUTPUT - - - name: Fetch React Native Version (latest tag) - id: rn - run: | - RN_VERSION=$(gh api repos/urbanairship/react-native-airship/tags?per_page=1 \ - --header "Authorization: token $GITHUB_TOKEN" --jq '.[0].name') - echo "rn_version=$RN_VERSION" >> $GITHUB_OUTPUT - - - name: Fetch Flutter Version (latest tag) - id: flutter - run: | - FLUTTER_VERSION=$(gh api repos/urbanairship/airship-flutter/tags?per_page=1 \ - --header "Authorization: token $GITHUB_TOKEN" --jq '.[0].name') - echo "flutter_version=$FLUTTER_VERSION" >> $GITHUB_OUTPUT - - - name: Fetch Capacitor Version (latest tag) - id: capacitor - run: | - CAPACITOR_VERSION=$(gh api repos/urbanairship/capacitor-airship/tags?per_page=1 \ - --header "Authorization: token $GITHUB_TOKEN" --jq '.[0].name') - echo "capacitor_version=$CAPACITOR_VERSION" >> $GITHUB_OUTPUT - - - name: Fetch Cordova Version (latest tag) - id: cordova - run: | - CORDOVA_VERSION=$(gh api repos/urbanairship/urbanairship-cordova/tags?per_page=1 \ - --header "Authorization: token $GITHUB_TOKEN" --jq '.[0].name') - echo "cordova_version=$CORDOVA_VERSION" >> $GITHUB_OUTPUT - - - name: Bump plugin versions - id: bump_plugins - run: | - function bump_version() { - local ver="$1" - local inc="$2" - IFS='.' read -r maj min pat <<< "$ver" - case "$inc" in - major) ((maj++)); min=0; pat=0;; - minor) ((min++)); pat=0;; - patch) ((pat++));; - esac - echo "${maj}.${min}.${pat}" - } - - RN_BUMPED=$(bump_version "${{ steps.rn.outputs.rn_version }}" "${{ steps.increment_type.outputs.increment_type }}") - FLUTTER_BUMPED=$(bump_version "${{ steps.flutter.outputs.flutter_version }}" "${{ steps.increment_type.outputs.increment_type }}") - CAPACITOR_BUMPED=$(bump_version "${{ steps.capacitor.outputs.capacitor_version }}" "${{ steps.increment_type.outputs.increment_type }}") - CORDOVA_BUMPED=$(bump_version "${{ steps.cordova.outputs.cordova_version }}" "${{ steps.increment_type.outputs.increment_type }}") - - echo "rn_bumped=$RN_BUMPED" >> $GITHUB_OUTPUT - echo "flutter_bumped=$FLUTTER_BUMPED" >> $GITHUB_OUTPUT - echo "capacitor_bumped=$CAPACITOR_BUMPED" >> $GITHUB_OUTPUT - echo "cordova_bumped=$CORDOVA_BUMPED" >> $GITHUB_OUTPUT - - - name: Trigger React Native Release - run: | - gh workflow run prep-release.yml \ - -R urbanairship/react-native-airship \ - -f react_native_version="${{ steps.bump_plugins.outputs.rn_bumped }}" \ - -f proxy_version="${{ github.event.inputs.proxy_version }}" \ - $([ -n "${{ github.event.inputs.ios_version }}" ] && echo "-f ios_version=${{ github.event.inputs.ios_version }}") \ - $([ -n "${{ github.event.inputs.android_version }}" ] && echo "-f android_version=${{ github.event.inputs.android_version }}") - - - name: Trigger Flutter Release - run: | - gh workflow run prep-release.yml \ - -R urbanairship/airship-flutter \ - -f flutter_version="${{ steps.bump_plugins.outputs.flutter_bumped }}" \ - -f proxy_version="${{ github.event.inputs.proxy_version }}" \ - $([ -n "${{ github.event.inputs.ios_version }}" ] && echo "-f ios_version=${{ github.event.inputs.ios_version }}") \ - $([ -n "${{ github.event.inputs.android_version }}" ] && echo "-f android_version=${{ github.event.inputs.android_version }}") - - - name: Trigger Capacitor Release - run: | - gh workflow run prep-release.yaml \ - -R urbanairship/capacitor-airship \ - -f capacitor_version="${{ steps.bump_plugins.outputs.capacitor_bumped }}" \ - -f proxy_version="${{ github.event.inputs.proxy_version }}" \ - $([ -n "${{ github.event.inputs.ios_version }}" ] && echo "-f ios_version=${{ github.event.inputs.ios_version }}") \ - $([ -n "${{ github.event.inputs.android_version }}" ] && echo "-f android_version=${{ github.event.inputs.android_version }}") - - - name: Trigger Cordova Release - run: | - gh workflow run prep-release.yaml \ - -R urbanairship/urbanairship-cordova \ - -f cordova_version="${{ steps.bump_plugins.outputs.cordova_bumped }}" \ - -f proxy_version="${{ github.event.inputs.proxy_version }}" \ - $([ -n "${{ github.event.inputs.ios_version }}" ] && echo "-f ios_version=${{ github.event.inputs.ios_version }}") \ - $([ -n "${{ github.event.inputs.android_version }}" ] && echo "-f android_version=${{ github.event.inputs.android_version }}") - - - name: Handle Success - if: success() - run: echo "Successfully triggered release preparations for all plugins" - - - name: Handle Failure - if: failure() - run: | - echo "::error::Failed to trigger one or more plugin releases. Check the logs for details." - exit 1 - - - # - name: Slack Notification (Success) - # if: success() - # uses: homoluctus/slatify@master - # with: - # type: success - # job_name: ":tada: Plugin release preparations triggered :tada:" - # message: "@mobile-team Release preparations have been triggered for all plugins with proxy version v${{ github.event.inputs.proxy_version }} :rocket:" - # url: ${{ secrets.MOBILE_SLACK_WEBHOOK }} - - # - name: Slack Notification (Failure) - # if: failure() - # uses: homoluctus/slatify@master - # with: - # type: failure - # job_name: ":disappointed: Plugin Release Preparations Failed :disappointed:" - # message: "@crow Failed to trigger plugin release preparations. Please check the workflow logs. :sob:" - # url: ${{ secrets.MOBILE_SLACK_WEBHOOK }} diff --git a/scripts/detect_sdk_versions.sh b/scripts/detect_sdk_versions.sh new file mode 100755 index 0000000..65cc1b9 --- /dev/null +++ b/scripts/detect_sdk_versions.sh @@ -0,0 +1,137 @@ +#!/bin/bash +set -e + +# Detect SDK versions and calculate proxy version bump +# Outputs: PROXY_VERSION, IOS_VERSION, ANDROID_VERSION, BUMP_TYPE + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# Check gh CLI authentication +if ! gh auth status &>/dev/null; then + echo -e "${RED}Error: gh CLI not authenticated${NC}" + echo "Run: gh auth login" + exit 1 +fi + +echo -e "${BLUE}${BOLD}Detecting SDK Versions${NC}\n" + +# Get current proxy version from podspec +CURRENT_PROXY_VERSION=$(grep "s.version" "$REPO_ROOT/AirshipFrameworkProxy.podspec" | grep -o "[0-9]*\.[0-9]*\.[0-9]*") +echo -e "Current proxy version: ${BOLD}$CURRENT_PROXY_VERSION${NC}" + +# Get current iOS SDK dependency +CURRENT_IOS_VERSION=$(grep "s.dependency.*'Airship'" "$REPO_ROOT/AirshipFrameworkProxy.podspec" | grep -o "[0-9]*\.[0-9]*\.[0-9]*") +echo -e "Current iOS SDK: ${BOLD}$CURRENT_IOS_VERSION${NC}" + +# Get current Android SDK dependency +CURRENT_ANDROID_VERSION=$(grep "airship =" "$REPO_ROOT/android/gradle/libs.versions.toml" | grep -o "[0-9]*\.[0-9]*\.[0-9]*") +echo -e "Current Android SDK: ${BOLD}$CURRENT_ANDROID_VERSION${NC}" + +echo -e "\n${BLUE}Fetching latest SDK tags...${NC}" + +# Fetch latest iOS SDK tag +LATEST_IOS_VERSION=$(gh api repos/urbanairship/ios-library/tags --jq '.[0].name' 2>/dev/null || echo "") +LATEST_IOS_VERSION="${LATEST_IOS_VERSION#v}" # Strip 'v' prefix if present +if [ -z "$LATEST_IOS_VERSION" ]; then + echo -e "${RED}Failed to fetch iOS SDK tags${NC}" + exit 1 +fi +echo -e "Latest iOS SDK: ${BOLD}$LATEST_IOS_VERSION${NC}" + +# Fetch latest Android SDK tag +LATEST_ANDROID_VERSION=$(gh api repos/urbanairship/android-library/tags --jq '.[0].name' 2>/dev/null || echo "") +LATEST_ANDROID_VERSION="${LATEST_ANDROID_VERSION#v}" # Strip 'v' prefix if present +if [ -z "$LATEST_ANDROID_VERSION" ]; then + echo -e "${RED}Failed to fetch Android SDK tags${NC}" + exit 1 +fi +echo -e "Latest Android SDK: ${BOLD}$LATEST_ANDROID_VERSION${NC}" + +# Function to determine bump type (major, minor, patch) +determine_bump_type() { + local old=$1 + local new=$2 + + IFS='.' read -r old_major old_minor old_patch <<< "$old" + IFS='.' read -r new_major new_minor new_patch <<< "$new" + + if [ "$new_major" -gt "$old_major" ]; then + echo "major" + elif [ "$new_minor" -gt "$old_minor" ]; then + echo "minor" + elif [ "$new_patch" -gt "$old_patch" ]; then + echo "patch" + else + echo "none" + fi +} + +# Determine bump types for each SDK +IOS_BUMP=$(determine_bump_type "$CURRENT_IOS_VERSION" "$LATEST_IOS_VERSION") +ANDROID_BUMP=$(determine_bump_type "$CURRENT_ANDROID_VERSION" "$LATEST_ANDROID_VERSION") + +echo -e "\n${BLUE}SDK Changes:${NC}" +echo -e "iOS: ${CURRENT_IOS_VERSION} โ†’ ${LATEST_IOS_VERSION} (${BOLD}${IOS_BUMP}${NC})" +echo -e "Android: ${CURRENT_ANDROID_VERSION} โ†’ ${LATEST_ANDROID_VERSION} (${BOLD}${ANDROID_BUMP}${NC})" + +# Determine overall bump type (max of both) +if [ "$IOS_BUMP" = "major" ] || [ "$ANDROID_BUMP" = "major" ]; then + BUMP_TYPE="major" +elif [ "$IOS_BUMP" = "minor" ] || [ "$ANDROID_BUMP" = "minor" ]; then + BUMP_TYPE="minor" +elif [ "$IOS_BUMP" = "patch" ] || [ "$ANDROID_BUMP" = "patch" ]; then + BUMP_TYPE="patch" +else + BUMP_TYPE="none" +fi + +# Calculate new proxy version +IFS='.' read -r major minor patch <<< "$CURRENT_PROXY_VERSION" +case "$BUMP_TYPE" in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + none) + echo -e "\n${YELLOW}No SDK version changes detected. No proxy bump needed.${NC}" + exit 0 + ;; +esac + +NEW_PROXY_VERSION="$major.$minor.$patch" + +echo -e "\n${GREEN}${BOLD}Bump Decision:${NC}" +echo -e "Type: ${BOLD}${BUMP_TYPE}${NC}" +echo -e "New version: ${BOLD}${NEW_PROXY_VERSION}${NC}" + +# Output for GitHub Actions / scripts +echo "" +echo "PROXY_VERSION=$NEW_PROXY_VERSION" +echo "IOS_VERSION=$LATEST_IOS_VERSION" +echo "ANDROID_VERSION=$LATEST_ANDROID_VERSION" +echo "BUMP_TYPE=$BUMP_TYPE" + +# If running in GitHub Actions, set outputs +if [ -n "$GITHUB_OUTPUT" ]; then + echo "proxy_version=$NEW_PROXY_VERSION" >> "$GITHUB_OUTPUT" + echo "ios_version=$LATEST_IOS_VERSION" >> "$GITHUB_OUTPUT" + echo "android_version=$LATEST_ANDROID_VERSION" >> "$GITHUB_OUTPUT" + echo "bump_type=$BUMP_TYPE" >> "$GITHUB_OUTPUT" +fi diff --git a/scripts/lib/changelog_prompt.txt b/scripts/lib/changelog_prompt.txt new file mode 100644 index 0000000..0184ce9 --- /dev/null +++ b/scripts/lib/changelog_prompt.txt @@ -0,0 +1,48 @@ +You are helping generate changelogs for Airship mobile SDK framework plugins. + +TASK: Analyze iOS and Android SDK changelogs and determine if the {{PLUGIN_NAME}} plugin changelog should include additional context beyond a simple version bump. + +PLUGIN: {{PLUGIN_NAME}} version {{PLUGIN_VERSION}} +iOS SDK: {{IOS_VERSION}} +Android SDK: {{ANDROID_VERSION}} + +iOS SDK CHANGELOG: +{{IOS_CHANGELOG}} + +ANDROID SDK CHANGELOG: +{{ANDROID_CHANGELOG}} + +DECISION CRITERIA: +- Use DETAILED format if SDK has critical/breaking changes, accessibility improvements, or must-update warnings +- Use SIMPLE format if this is a routine maintenance update with minor internal changes +- Include specific user-facing issues from SDK changelogs (e.g., 'fixes YouTube video playback') +- Keep plugin changelog concise (1-2 sentence summary + 2-5 bullet points max) + +OUTPUT FORMAT (choose one): + +DETAILED: +## Version {{PLUGIN_VERSION}} - {{RELEASE_DATE}} + +[Release type] release that [brief summary of key changes from SDK]. + +### Changes +- Updated Android SDK to [{{ANDROID_VERSION}}](https://github.com/urbanairship/android-library/releases/tag/{{ANDROID_VERSION}}) +- Updated iOS SDK to [{{IOS_VERSION}}](https://github.com/urbanairship/ios-library/releases/tag/{{IOS_VERSION}}) +[Optional: 1-3 additional bullets with key SDK fixes/features if user-facing] + +SIMPLE: +## Version {{PLUGIN_VERSION}} - {{RELEASE_DATE}} + +[Release type] release that updates the Android SDK to {{ANDROID_VERSION}} and the iOS SDK to {{IOS_VERSION}}. + +### Changes +- Updated Android SDK to [{{ANDROID_VERSION}}](https://github.com/urbanairship/android-library/releases/tag/{{ANDROID_VERSION}}) +- Updated iOS SDK to [{{IOS_VERSION}}](https://github.com/urbanairship/ios-library/releases/tag/{{IOS_VERSION}}) + +IMPORTANT INSTRUCTIONS: +1. Analyze both SDK changelogs carefully +2. Decide SIMPLE or DETAILED based on criteria above +3. Return ONLY the formatted changelog (no explanations, no preamble) +4. If unsure or SDK changelogs lack substance, use SIMPLE +5. Ensure all version numbers in links match exactly (no 'v' prefix in URLs) +6. Use release type: "Major" for X.0.0, "Minor" for X.Y.0, "Patch" for X.Y.Z diff --git a/scripts/lib/generate_changelog.sh b/scripts/lib/generate_changelog.sh new file mode 100755 index 0000000..d4e82eb --- /dev/null +++ b/scripts/lib/generate_changelog.sh @@ -0,0 +1,301 @@ +#!/bin/bash +# Intelligent changelog generation using Gemini CLI +# Fetches SDK changelogs, analyzes them, and generates appropriate format + +# Get script directory +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROMPT_TEMPLATE="$LIB_DIR/changelog_prompt.txt" + +# Fetch SDK changelog from GitHub release notes +fetch_github_changelog() { + local repo="$1" + local version="$2" + + echo " ๐Ÿ“ฅ Fetching ${repo} ${version} changelog..." >&2 + + # Fetch release notes from GitHub API + local changelog=$(gh api \ + "repos/urbanairship/${repo}/releases/tags/${version}" \ + --jq '.body' 2>/dev/null || echo "") + + if [ -z "$changelog" ]; then + echo " โš ๏ธ No changelog found for ${repo} ${version}" >&2 + return 1 + fi + + echo " โœ“ Fetched ${repo} changelog (${#changelog} chars)" >&2 + echo "$changelog" +} + +# Generate simple fallback changelog +generate_simple_changelog() { + local plugin_version="$1" + local ios_version="$2" + local android_version="$3" + local release_date=$(date +'%B %e, %Y' | sed 's/ / /') + + # Determine release type based on version pattern + local release_type="Patch" + if [[ $plugin_version =~ \.0\.0$ ]]; then + release_type="Major" + elif [[ $plugin_version =~ \.0$ ]]; then + release_type="Minor" + fi + + cat <&2 + echo " Using simple changelog format" >&2 + generate_simple_changelog "$plugin_version" "$ios_version" "$android_version" + return 0 + fi + + # Check if Gemini CLI is available + if ! command -v gemini &> /dev/null; then + echo " โš ๏ธ Gemini CLI not found, using simple changelog format" >&2 + generate_simple_changelog "$plugin_version" "$ios_version" "$android_version" + return 0 + fi + + echo " ๐Ÿค– Analyzing changelogs with Gemini..." >&2 + + # Read template and substitute variables + local prompt=$(cat "$PROMPT_TEMPLATE") + local release_date=$(date +'%B %e, %Y' | sed 's/ / /') + + # Perform variable substitutions + prompt="${prompt//\{\{PLUGIN_NAME\}\}/$plugin_name}" + prompt="${prompt//\{\{PLUGIN_VERSION\}\}/$plugin_version}" + prompt="${prompt//\{\{IOS_VERSION\}\}/$ios_version}" + prompt="${prompt//\{\{ANDROID_VERSION\}\}/$android_version}" + prompt="${prompt//\{\{RELEASE_DATE\}\}/$release_date}" + prompt="${prompt//\{\{IOS_CHANGELOG\}\}/$ios_changelog}" + prompt="${prompt//\{\{ANDROID_CHANGELOG\}\}/$android_changelog}" + + # Call Gemini CLI in non-interactive mode + local result=$(gemini -p "$prompt" 2>/dev/null) + + # Validate result has expected changelog structure + if [ -n "$result" ] && echo "$result" | grep -q "## Version"; then + echo " โœ“ Generated intelligent changelog" >&2 + echo "$result" + return 0 + fi + + # Check for rate limit error + if echo "$result" | grep -qi "rate limit"; then + echo " โš ๏ธ Gemini rate limit hit, using simple changelog format" >&2 + else + echo " โš ๏ธ Invalid Gemini response, using simple changelog format" >&2 + fi + + # Fallback to simple format + generate_simple_changelog "$plugin_version" "$ios_version" "$android_version" +} + +# Validate and fix common changelog issues (self-healing) +validate_and_fix() { + local plugin="$1" + local plugin_version="$2" + local ios_version="$3" + local android_version="$4" + local changelog_file="CHANGELOG.md" + + echo " ๐Ÿ” Validating and fixing changelog..." >&2 + + # Read first changelog entry + local changelog_content=$(awk '/^## Version/{p=1} p{print} /^## Version/ && NR>1{exit}' "$changelog_file") + + # Track if we made any fixes + local fixes_made=false + + # 1. Fix version format (remove 'v' prefix if present) + if echo "$changelog_content" | grep -q "Version v[0-9]"; then + sed -i '' 's/Version v\([0-9]\)/Version \1/g' "$changelog_file" + echo " โœ“ Fixed version format (removed 'v' prefix)" >&2 + fixes_made=true + fi + + # 2. Validate version number matches + local changelog_version=$(echo "$changelog_content" | grep "^## Version" | grep -o "[0-9]*\.[0-9]*\.[0-9]*" | head -1) + if [ "$changelog_version" != "$plugin_version" ]; then + echo " โš ๏ธ WARNING: Changelog version ($changelog_version) doesn't match expected ($plugin_version)" >&2 + fi + + # 3. Fix malformed GitHub links + if echo "$changelog_content" | grep -q "github.com/urbanairship/.*releases/tag/[^)]*[^0-9.)]"; then + # Remove any trailing characters after version number in links + sed -i '' -E 's|(https://github.com/urbanairship/[^/]+/releases/tag/[0-9.]+)[^)]*|\1|g' "$changelog_file" + echo " โœ“ Fixed malformed GitHub release links" >&2 + fixes_made=true + fi + + # 4. Remove trailing whitespace + if grep -q "[[:space:]]$" "$changelog_file"; then + sed -i '' 's/[[:space:]]*$//' "$changelog_file" + echo " โœ“ Removed trailing whitespace" >&2 + fixes_made=true + fi + + # 5. Ensure proper spacing around headers + local temp_file=$(mktemp) + if awk ' + /^## Version/ { + if (NR > 1 && prev !~ /^[[:space:]]*$/) print "" + print + next + } + /^### Changes/ { + if (prev !~ /^[[:space:]]*$/) print "" + print + next + } + { print } + { prev = $0 } + ' "$changelog_file" > "$temp_file" 2>/dev/null; then + if ! cmp -s "$changelog_file" "$temp_file"; then + mv "$temp_file" "$changelog_file" + echo " โœ“ Fixed spacing around headers" >&2 + fixes_made=true + else + rm -f "$temp_file" + fi + else + echo " โš ๏ธ Spacing fix failed, skipping" >&2 + rm -f "$temp_file" + fi + + # 6. Validate iOS/Android SDK links are correct + if [ -n "$ios_version" ]; then + if ! echo "$changelog_content" | grep -q "ios-library/releases/tag/${ios_version}"; then + echo " โš ๏ธ WARNING: iOS SDK version link missing or incorrect" >&2 + fi + fi + + if [ -n "$android_version" ]; then + if ! echo "$changelog_content" | grep -q "android-library/releases/tag/${android_version}"; then + echo " โš ๏ธ WARNING: Android SDK version link missing or incorrect" >&2 + fi + fi + + # 7. Check markdown structure (warn only, don't fail) + if ! echo "$changelog_content" | grep -q "^## Version"; then + echo " โš ๏ธ WARNING: Missing version header in changelog" >&2 + echo " This may indicate Gemini generated an unexpected format" >&2 + fi + + if ! echo "$changelog_content" | grep -q "^### Changes"; then + echo " โš ๏ธ WARNING: Missing Changes section in changelog" >&2 + echo " This may indicate Gemini generated an unexpected format" >&2 + fi + + if $fixes_made; then + echo " โœ… Changelog validated and fixed" >&2 + # Stage the fixes + git add "$changelog_file" + else + echo " โœ… Changelog validated (no fixes needed)" >&2 + fi + + # Always return success - warnings don't break the workflow + return 0 +} + +# Validate PR content for consistency +validate_pr() { + local plugin="$1" + local plugin_version="$2" + local pr_url="$3" + local repo="$4" + + echo " ๐Ÿ” Validating PR content..." >&2 + + # Check jq is available (required for PR validation) + if ! command -v jq &>/dev/null; then + echo " โš ๏ธ jq not installed, skipping PR validation" >&2 + return 0 + fi + + # Extract PR number from URL + local pr_number=$(echo "$pr_url" | grep -oE '/pull/[0-9]+' | grep -oE '[0-9]+') + if [ -z "$pr_number" ]; then + echo " โš ๏ธ Could not parse PR number from: $pr_url" >&2 + return 0 + fi + + # Fetch PR details + local pr_data=$(gh pr view "$pr_number" --repo "$repo" --json title,body 2>/dev/null) + + if [ -z "$pr_data" ]; then + echo " โš ๏ธ Could not fetch PR data for validation" >&2 + return 0 + fi + + local pr_title=$(echo "$pr_data" | jq -r '.title') + local pr_body=$(echo "$pr_data" | jq -r '.body') + + # Expected title format: "Release X.Y.Z" + local expected_title="Release ${plugin_version}" + + if [ "$pr_title" != "$expected_title" ]; then + echo " โš ๏ธ WARNING: PR title is '$pr_title', expected '$expected_title'" >&2 + fi + + # Validate version appears in body + if ! echo "$pr_body" | grep -q "$plugin_version"; then + echo " โš ๏ธ WARNING: Plugin version $plugin_version not found in PR body" >&2 + fi + + echo " โœ… PR validation complete" >&2 +} + +# Main intelligent changelog generation function +generate_intelligent_changelog() { + local plugin_name="$1" + local plugin_version="$2" + local ios_version="$3" + local android_version="$4" + + # Fetch SDK changelogs from GitHub + local ios_changelog=$(fetch_github_changelog "ios-library" "$ios_version") + local ios_fetch_status=$? + + local android_changelog=$(fetch_github_changelog "android-library" "$android_version") + local android_fetch_status=$? + + # If either fetch failed, use simple format + if [ $ios_fetch_status -ne 0 ] || [ $android_fetch_status -ne 0 ]; then + echo " โš ๏ธ Failed to fetch SDK changelogs, using simple format" >&2 + generate_simple_changelog "$plugin_version" "$ios_version" "$android_version" + return 0 + fi + + # Use Gemini CLI to analyze and decide format + gemini_analyze_changelog \ + "$plugin_name" \ + "$plugin_version" \ + "$ios_version" \ + "$android_version" \ + "$ios_changelog" \ + "$android_changelog" +} diff --git a/scripts/prep_proxy_release.sh b/scripts/prep_proxy_release.sh new file mode 100755 index 0000000..c932868 --- /dev/null +++ b/scripts/prep_proxy_release.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +# Prepare proxy release: update versions, dependencies, changelog +# Usage: ./prep_proxy_release.sh [--test] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Parse arguments +TEST_MODE=false +if [ "$1" = "--test" ]; then + TEST_MODE=true + echo "๐Ÿงช TEST MODE - No changes will be made" + echo "" +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +echo -e "${BLUE}${BOLD}Preparing Proxy Release${NC}\n" + +# Run detection script to get versions +DETECT_OUTPUT=$("$SCRIPT_DIR/detect_sdk_versions.sh") +echo "$DETECT_OUTPUT" +echo "" + +# Extract versions from output +PROXY_VERSION=$(echo "$DETECT_OUTPUT" | grep "^PROXY_VERSION=" | cut -d'=' -f2) +IOS_VERSION=$(echo "$DETECT_OUTPUT" | grep "^IOS_VERSION=" | cut -d'=' -f2) +ANDROID_VERSION=$(echo "$DETECT_OUTPUT" | grep "^ANDROID_VERSION=" | cut -d'=' -f2) +BUMP_TYPE=$(echo "$DETECT_OUTPUT" | grep "^BUMP_TYPE=" | cut -d'=' -f2) + +if [ -z "$PROXY_VERSION" ] || [ "$BUMP_TYPE" = "none" ]; then + echo -e "${RED}No version bump needed${NC}" + exit 1 +fi + +echo -e "${BLUE}Updating files...${NC}" + +# Update AirshipFrameworkProxy.podspec +if [ "$TEST_MODE" = "false" ]; then + sed -i '' "s/s.version[[:space:]]*=[[:space:]]*\"[0-9]*\.[0-9]*\.[0-9]*\"/s.version = \"${PROXY_VERSION}\"/" "$REPO_ROOT/AirshipFrameworkProxy.podspec" + sed -i '' "s/s.dependency[[:space:]]*'Airship',[[:space:]]*\"[0-9]*\.[0-9]*\.[0-9]*\"/s.dependency 'Airship', \"${IOS_VERSION}\"/" "$REPO_ROOT/AirshipFrameworkProxy.podspec" + echo "โœ“ Updated AirshipFrameworkProxy.podspec" +else + echo " Would update AirshipFrameworkProxy.podspec:" + echo " version: $PROXY_VERSION" + echo " iOS SDK: $IOS_VERSION" +fi + +# Update Package.swift +if [ "$TEST_MODE" = "false" ]; then + sed -i '' "s/from: \"[0-9]*\.[0-9]*\.[0-9]*\"/from: \"${IOS_VERSION}\"/" "$REPO_ROOT/Package.swift" + echo "โœ“ Updated Package.swift" +else + echo " Would update Package.swift:" + echo " iOS SDK: $IOS_VERSION" +fi + +# Update android/gradle/libs.versions.toml +if [ "$TEST_MODE" = "false" ]; then + sed -i '' "s/airshipProxy = '[0-9]*\.[0-9]*\.[0-9]*'/airshipProxy = '${PROXY_VERSION}'/" "$REPO_ROOT/android/gradle/libs.versions.toml" + sed -i '' "s/airship = '[0-9]*\.[0-9]*\.[0-9]*'/airship = '${ANDROID_VERSION}'/" "$REPO_ROOT/android/gradle/libs.versions.toml" + echo "โœ“ Updated android/gradle/libs.versions.toml" +else + echo " Would update android/gradle/libs.versions.toml:" + echo " proxy version: $PROXY_VERSION" + echo " Android SDK: $ANDROID_VERSION" +fi + +echo -e "\n${GREEN}${BOLD}โœ“ Proxy release prepared${NC}" +echo -e "Version: ${BOLD}${PROXY_VERSION}${NC}" +echo -e "iOS SDK: ${BOLD}${IOS_VERSION}${NC}" +echo -e "Android SDK: ${BOLD}${ANDROID_VERSION}${NC}" + +if [ "$TEST_MODE" = "false" ]; then + echo -e "\n${BLUE}Next steps:${NC}" + echo "1. Review changes: git diff" + echo "2. Commit and push to branch" + echo "3. Create PR" +fi diff --git a/scripts/update_all_plugins.sh b/scripts/update_all_plugins.sh new file mode 100755 index 0000000..625db5a --- /dev/null +++ b/scripts/update_all_plugins.sh @@ -0,0 +1,749 @@ +#!/usr/bin/env bash +set -e + +# Centralized Plugin Release Script +# Updates all framework plugins (React Native, Cordova, Flutter, Capacitor) +# Creates PRs in each repo with version updates and changelogs +# +# Usage: ./update_all_plugins.sh [ios_version] [android_version] [options] +# +# Options: +# --test Add -test suffix to branch names for testing +# --skip-react-native Skip React Native plugin +# --skip-cordova Skip Cordova plugin +# --skip-flutter Skip Flutter plugin +# --skip-capacitor Skip Capacitor plugin +# +# Compatible with bash 3.2+ (uses indexed arrays instead of associative arrays) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# Check gh CLI is installed and authenticated +check_gh_auth() { + if ! command -v gh &>/dev/null; then + echo -e "${RED}Error: gh CLI not installed${NC}" + echo "Install: https://cli.github.com/" + exit 1 + fi + if ! gh auth status &>/dev/null; then + echo -e "${RED}Error: gh CLI not authenticated${NC}" + echo "Run: gh auth login" + exit 1 + fi +} + +check_gh_auth + +# Source changelog generation library +CHANGELOG_LIB="$SCRIPT_DIR/lib/generate_changelog.sh" +if [ ! -f "$CHANGELOG_LIB" ]; then + echo -e "${RED}Error: Missing $CHANGELOG_LIB${NC}" + exit 1 +fi +source "$CHANGELOG_LIB" + +# Parse arguments +PROXY_VERSION="" +IOS_VERSION="" +ANDROID_VERSION="" +TEST_MODE=false +SKIP_REACT_NATIVE=false +SKIP_CORDOVA=false +SKIP_FLUTTER=false +SKIP_CAPACITOR=false + +for arg in "$@"; do + case "$arg" in + --test) + TEST_MODE=true + ;; + --skip-react-native) + SKIP_REACT_NATIVE=true + ;; + --skip-cordova) + SKIP_CORDOVA=true + ;; + --skip-flutter) + SKIP_FLUTTER=true + ;; + --skip-capacitor) + SKIP_CAPACITOR=true + ;; + *) + if [ -z "$PROXY_VERSION" ]; then + PROXY_VERSION="$arg" + elif [ -z "$IOS_VERSION" ]; then + IOS_VERSION="$arg" + elif [ -z "$ANDROID_VERSION" ]; then + ANDROID_VERSION="$arg" + fi + ;; + esac +done + +if [ "$TEST_MODE" = true ]; then + echo -e "${YELLOW}๐Ÿงช TEST MODE - Branches will have -test suffix${NC}" + echo "" +fi + +# Plugin configuration using parallel indexed arrays (bash 3.2+ compatible) +# Order: react-native, cordova, flutter, capacitor +PLUGIN_KEYS=("react-native" "cordova" "flutter" "capacitor") +REPO_NAMES=("react-native-airship" "urbanairship-cordova" "airship-flutter" "capacitor-airship") +DISPLAY_NAMES=("React Native" "Cordova" "Flutter" "Capacitor") +BRANCH_PREFIXES=("release" "cordova" "flutter" "capacitor") +NEW_VERSIONS=("" "" "" "") +PR_URLS=("" "" "" "") + +# Helper functions to work with indexed arrays +get_index() { + local key="$1" + local i + for i in "${!PLUGIN_KEYS[@]}"; do + if [ "${PLUGIN_KEYS[$i]}" = "$key" ]; then + echo "$i" + return 0 + fi + done + return 1 +} + +get_repo_name() { + local idx=$(get_index "$1") + echo "${REPO_NAMES[$idx]}" +} + +get_display_name() { + local idx=$(get_index "$1") + echo "${DISPLAY_NAMES[$idx]}" +} + +get_branch_prefix() { + local idx=$(get_index "$1") + echo "${BRANCH_PREFIXES[$idx]}" +} + +get_new_version() { + local idx=$(get_index "$1") + echo "${NEW_VERSIONS[$idx]}" +} + +set_new_version() { + local key="$1" + local value="$2" + local idx=$(get_index "$key") + NEW_VERSIONS[$idx]="$value" +} + +get_pr_url() { + local idx=$(get_index "$1") + echo "${PR_URLS[$idx]}" +} + +set_pr_url() { + local key="$1" + local value="$2" + local idx=$(get_index "$key") + PR_URLS[$idx]="$value" +} + +# Check if a plugin should be skipped +should_skip_plugin() { + local plugin="$1" + case "$plugin" in + react-native) [ "$SKIP_REACT_NATIVE" = true ] ;; + cordova) [ "$SKIP_CORDOVA" = true ] ;; + flutter) [ "$SKIP_FLUTTER" = true ] ;; + capacitor) [ "$SKIP_CAPACITOR" = true ] ;; + *) return 1 ;; + esac +} + +# Check if a branch exists on remote +branch_exists_remote() { + local branch="$1" + git ls-remote --heads origin "$branch" 2>/dev/null | grep -q "refs/heads/${branch}$" +} + +# Check if a branch exists locally +branch_exists_local() { + local branch="$1" + git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null +} + +# Get unique branch name for the release +# In test mode: release-X.Y.Z-test, release-X.Y.Z-test-2, release-X.Y.Z-test-3, ... +# In non-test mode: release-X.Y.Z (fails if exists - real releases shouldn't have duplicates) +get_unique_branch_name() { + local plugin="$1" + local version="$2" + local base_name="$(get_branch_prefix "$plugin")-${version}" + + if [ "$TEST_MODE" = true ]; then + # Test mode: find first available -test or -test-N suffix + local candidate="${base_name}-test" + local attempt=2 + local max_attempts=20 + + while branch_exists_remote "$candidate" || branch_exists_local "$candidate"; do + candidate="${base_name}-test-${attempt}" + attempt=$((attempt + 1)) + if [ $attempt -gt $max_attempts ]; then + echo "" + return 1 + fi + done + + echo "$candidate" + else + # Non-test mode: use base name, return empty if exists + if branch_exists_remote "$base_name"; then + echo "" + return 1 + fi + echo "$base_name" + fi +} + +# Validate inputs +validate_inputs() { + if [ -z "$PROXY_VERSION" ]; then + echo -e "${RED}Error: proxy_version required${NC}" + echo "Usage: $0 [ios_version] [android_version] [--test] [--skip-*]" + exit 1 + fi + + if ! [[ "$PROXY_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: Invalid proxy version format: $PROXY_VERSION${NC}" + exit 1 + fi + + if [ -n "$IOS_VERSION" ] && ! [[ "$IOS_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: Invalid iOS version format: $IOS_VERSION${NC}" + exit 1 + fi + + if [ -n "$ANDROID_VERSION" ] && ! [[ "$ANDROID_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: Invalid Android version format: $ANDROID_VERSION${NC}" + exit 1 + fi +} + +# Determine bump type (major, minor, patch) +determine_bump_type() { + local old=$1 + local new=$2 + + IFS='.' read -r old_major old_minor old_patch <<< "$old" + IFS='.' read -r new_major new_minor new_patch <<< "$new" + + if [ "$new_major" -gt "$old_major" ]; then + echo "major" + elif [ "$new_minor" -gt "$old_minor" ]; then + echo "minor" + elif [ "$new_patch" -gt "$old_patch" ]; then + echo "patch" + else + echo "none" + fi +} + +# Calculate new plugin versions +calculate_plugin_versions() { + echo -e "${BLUE}Calculating plugin versions...${NC}" + + # Get current proxy version to determine bump type + CURRENT_PROXY_VERSION=$(grep "s.version" "$REPO_ROOT/AirshipFrameworkProxy.podspec" | grep -o "[0-9]*\.[0-9]*\.[0-9]*") + BUMP_TYPE=$(determine_bump_type "$CURRENT_PROXY_VERSION" "$PROXY_VERSION") + + echo -e "Current proxy: ${BOLD}${CURRENT_PROXY_VERSION}${NC}" + echo -e "New proxy: ${BOLD}${PROXY_VERSION}${NC}" + echo -e "Bump type: ${BOLD}${BUMP_TYPE}${NC}" + echo "" + + if [ "$BUMP_TYPE" = "none" ]; then + echo -e "${RED}Error: No version bump detected${NC}" + exit 1 + fi + + # Fetch latest versions for each plugin and calculate new versions + for plugin in "${PLUGIN_KEYS[@]}"; do + local repo=$(get_repo_name "$plugin") + + # Fetch latest tag + local latest_tag=$(gh api "repos/urbanairship/${repo}/tags" --jq '.[0].name' 2>/dev/null || echo "") + latest_tag="${latest_tag#v}" # Strip 'v' prefix if present + + if [ -z "$latest_tag" ]; then + echo -e "${RED}Failed to fetch version for ${repo}${NC}" + exit 1 + fi + + IFS='.' read -r major minor patch <<< "$latest_tag" + + # Apply bump type + case "$BUMP_TYPE" in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + esac + + local new_version="${major}.${minor}.${patch}" + set_new_version "$plugin" "$new_version" + echo -e "${plugin}: ${latest_tag} โ†’ ${BOLD}${new_version}${NC}" + done + echo "" +} + +# Clone plugin repositories +clone_plugins() { + echo -e "${BLUE}Cloning plugin repositories...${NC}" + WORK_DIR=$(mktemp -d) + cd "$WORK_DIR" + + # Clone repos in parallel (only non-skipped ones) + local clone_count=0 + for plugin in "${PLUGIN_KEYS[@]}"; do + if should_skip_plugin "$plugin"; then + echo -e " Skipping ${plugin} (disabled)" + continue + fi + local repo="$(get_repo_name "$plugin")" + gh repo clone "urbanairship/${repo}" -- --depth 1 & + clone_count=$((clone_count + 1)) + done + + if [ $clone_count -gt 0 ]; then + wait + echo -e "${GREEN}โœ“ Repositories cloned${NC}" + else + echo -e "${YELLOW}No plugins selected to update${NC}" + fi + echo "" +} + +# Update React Native plugin files +update_react_native_files() { + local version="$1" + local repo_path="$2" + + cd "$repo_path" + + # Update package.json version + npm version "$version" --no-git-tag-version + + # Update iOS Swift version constant + sed -i '' "s/\(version:\ String *= *\)\".*\"/\1\"$version\"/g" ios/AirshipReactNative.swift +} + +# Update Cordova plugin files +update_cordova_files() { + local version="$1" + local repo_path="$2" + + cd "$repo_path" + + # Update core package + npm --prefix cordova-airship version "$version" --no-git-tag-version + + # Update HMS package + npm --prefix cordova-airship-hms version "$version" --no-git-tag-version + + # Update plugin.xml files + sed -i '' "s///" cordova-airship-hms/plugin.xml + + # Update version constants + sed -i '' "s/var version = \"[-0-9.a-zA-Z]*\"/var version = \"$version\"/" cordova-airship/src/android/AirshipCordovaVersion.kt + sed -i '' "s/static let version = \"[-0-9.a-zA-Z]*\"/static let version = \"$version\"/" cordova-airship/src/ios/AirshipCordovaVersion.swift +} + +# Update Flutter plugin files +update_flutter_files() { + local version="$1" + local repo_path="$2" + + cd "$repo_path" + + # Update pubspec.yaml + sed -i '' "s/\(^version: *\).*/\1$version/g" pubspec.yaml + + # Update podspec + sed -i '' "s/\(^AIRSHIP_FLUTTER_VERSION *= *\)\".*\"/\1\"$version\"/g" ios/airship_flutter.podspec + + # Update version constants + sed -i '' "s/\(pluginVersion *= *\)\".*\"/\1\"$version\"/g" ios/airship_flutter/Sources/airship_flutter/AirshipPluginVersion.swift + sed -i '' "s/\(AIRSHIP_PLUGIN_VERSION *= *\)\".*\"/\1\"$version\"/g" android/src/main/kotlin/com/airship/flutter/AirshipPluginVersion.kt +} + +# Update Capacitor plugin files +update_capacitor_files() { + local version="$1" + local repo_path="$2" + + cd "$repo_path" + + # Update package.json + npm version "$version" --no-git-tag-version + + # Update version constants + sed -i '' "s/var version = \"[-0-9.a-zA-Z]*\"/var version = \"$version\"/" android/src/main/java/com/airship/capacitor/AirshipCapacitorVersion.kt + sed -i '' "s/static let version = \"[-0-9.a-zA-Z]*\"/static let version = \"$version\"/" ios/Plugin/AirshipCapacitorVersion.swift +} + +# Update proxy dependencies +update_proxy_dependencies() { + local plugin="$1" + local proxy_version="$2" + + case "$plugin" in + react-native) + sed -i '' -E "s/(Airship_airshipProxyVersion=)([^$]*)/\1$proxy_version/" android/gradle.properties + sed -i '' -E "s/(s\.dependency *\"AirshipFrameworkProxy\", *\")([^\"]*)(\")/\1$proxy_version\3/" react-native-airship.podspec + ;; + cordova) + sed -i '' -E "s/(pod name=\"AirshipFrameworkProxy\" spec=\")[^\"]*\"/\1$proxy_version\"/" cordova-airship/plugin.xml + sed -i '' -E "s/(api \"com.urbanairship.android:airship-framework-proxy:)[^\"]*\"/\1$proxy_version\"/" cordova-airship/src/android/build-extras.gradle + sed -i '' -E "s/(implementation \"com.urbanairship.android:airship-framework-proxy-hms:)[^\"]*\"/\1$proxy_version\"/" cordova-airship-hms/src/android/build-extras.gradle + ;; + flutter) + sed -i '' -E "s/(ext\.airship_framework_proxy_version *= *')([^']*)(')/\1$proxy_version\3/" android/build.gradle + sed -i '' -E "s/(s\.dependency *\"AirshipFrameworkProxy\", *\")([^\"]*)(\")/\1$proxy_version\3/" ios/airship_flutter.podspec + sed -i '' -E "s/(\.package\(name: *\"AirshipFrameworkProxy\", *url: *\"[^\"]+\", *from: *\")([^\"]*)(\")/\1$proxy_version\3/" ios/airship_flutter/Package.swift + ;; + capacitor) + sed -i '' "s/s\.dependency.*AirshipFrameworkProxy.*$/s.dependency \"AirshipFrameworkProxy\", \"$proxy_version\"/" UaCapacitorAirship.podspec + sed -i '' "s/airshipProxyVersion = project\.hasProperty('airshipProxyVersion') ? rootProject\.ext\.airshipProxyVersion : '.*'/airshipProxyVersion = project.hasProperty('airshipProxyVersion') ? rootProject.ext.airshipProxyVersion : '$proxy_version'/" android/build.gradle + sed -i '' "s/pod 'AirshipFrameworkProxy'.*$/pod 'AirshipFrameworkProxy', '$proxy_version'/" ios/Podfile + sed -i '' "s/\.package(url: \"https:\/\/github\.com\/urbanairship\/airship-mobile-framework-proxy\.git\", from: \".*\")/.package(url: \"https:\/\/github.com\/urbanairship\/airship-mobile-framework-proxy.git\", from: \"$proxy_version\")/" Package.swift + ;; + esac +} + +# Generate changelog entry using intelligent Gemini-powered generation +generate_changelog() { + local plugin="$1" + local version="$2" + + echo " ๐Ÿ“ Generating changelog..." + + # Use intelligent generation if SDK versions provided + local changelog_entry + if [ -n "$IOS_VERSION" ] && [ -n "$ANDROID_VERSION" ]; then + changelog_entry=$(generate_intelligent_changelog \ + "$(get_display_name "$plugin")" \ + "$version" \ + "$IOS_VERSION" \ + "$ANDROID_VERSION") + else + # Fallback to simple format if no SDK versions + changelog_entry=$(generate_simple_changelog "$version" "$IOS_VERSION" "$ANDROID_VERSION") + fi + + # Prepend to CHANGELOG.md + local temp_file=$(mktemp) + local first_line=$(head -n 1 CHANGELOG.md) + + # Check if file starts with a header we should preserve (e.g., "# Changelog") + if [[ "$first_line" =~ ^#[[:space:]] ]]; then + # Preserve header + echo "$first_line" > "$temp_file" + echo "" >> "$temp_file" + echo "$changelog_entry" >> "$temp_file" + echo "" >> "$temp_file" + tail -n +2 CHANGELOG.md >> "$temp_file" + else + # No header, just prepend + echo "$changelog_entry" > "$temp_file" + echo "" >> "$temp_file" + cat CHANGELOG.md >> "$temp_file" + fi + mv "$temp_file" CHANGELOG.md + + echo " โœ“ Changelog updated" + + # Validate and fix the changelog (self-healing) + # Note: This function always succeeds, it just warns about issues + validate_and_fix "$plugin" "$version" "$IOS_VERSION" "$ANDROID_VERSION" +} + +# Generate PR body +generate_pr_body() { + local plugin="$1" + local version="$2" + + cat </dev/null || echo "Unable to determine changed files") +\`\`\` + +## Next Steps +1. Review version updates +2. Verify changelog accuracy +3. Merge when ready + +--- +๐Ÿค– Generated by centralized release automation +EOF +} + +# Update a single plugin +update_plugin() { + local plugin="$1" + local plugin_version="$2" + local repo_path="$3" + + echo -e "${BLUE}Updating $(get_display_name "$plugin")...${NC}" + + cd "$repo_path" + + # Get unique branch name (handles test mode auto-increment) + local branch_name + branch_name=$(get_unique_branch_name "$plugin" "$plugin_version") + + if [ -z "$branch_name" ]; then + if [ "$TEST_MODE" = true ]; then + echo " โœ— Failed to find available test branch after 20 attempts" + else + echo " โœ— Branch $(get_branch_prefix "$plugin")-${plugin_version} already exists" + echo " For real releases, clean up the existing branch first" + echo " Or use --test mode for testing" + fi + return 1 + fi + + # Delete local branch if it exists (stale from previous run) + if branch_exists_local "$branch_name"; then + git branch -D "$branch_name" 2>/dev/null || true + fi + + # Create the branch + git checkout -b "$branch_name" + echo " โœ“ Created branch: $branch_name" + + # Update version files (plugin-specific) + case "$plugin" in + react-native) + update_react_native_files "$plugin_version" "$repo_path" + ;; + cordova) + update_cordova_files "$plugin_version" "$repo_path" + ;; + flutter) + update_flutter_files "$plugin_version" "$repo_path" + ;; + capacitor) + update_capacitor_files "$plugin_version" "$repo_path" + ;; + esac + + # Update proxy dependencies + if [ -n "$PROXY_VERSION" ]; then + update_proxy_dependencies "$plugin" "$PROXY_VERSION" + fi + + # Generate changelog + generate_changelog "$plugin" "$plugin_version" + + # Commit changes + git add . + git commit -m "Release ${plugin_version} + +- Updated plugin version to ${plugin_version} +- Updated framework proxy to ${PROXY_VERSION} +${IOS_VERSION:+- Updated iOS SDK to ${IOS_VERSION}} +${ANDROID_VERSION:+- Updated Android SDK to ${ANDROID_VERSION}} + +๐Ÿค– Generated with centralized release automation" + + # Push to remote with retry logic + local push_attempts=0 + local max_push_attempts=3 + local push_output + while [ $push_attempts -lt $max_push_attempts ]; do + if push_output=$(git push -u origin "$branch_name" 2>&1); then + echo " โœ“ Pushed branch to remote" + break + else + push_attempts=$((push_attempts + 1)) + if [ $push_attempts -lt $max_push_attempts ]; then + echo " โš ๏ธ Push failed, retrying... (attempt $push_attempts/$max_push_attempts)" + sleep 5 + else + echo " โœ— Failed to push: $push_output" + return 1 + fi + fi + done + + # Create PR and capture URL + local pr_body=$(generate_pr_body "$plugin" "$plugin_version") + + # Check if PR already exists for this head branch + local existing_pr=$(gh pr list \ + --repo "urbanairship/$(get_repo_name "$plugin")" \ + --head "$branch_name" \ + --json url \ + --jq '.[0].url' 2>/dev/null || echo "") + + if [ -n "$existing_pr" ]; then + echo " โ„น๏ธ PR already exists for branch $branch_name" + echo "$existing_pr" + return 0 + fi + + # Create new PR + local pr_title="Release ${plugin_version}" + if [ "$TEST_MODE" = true ]; then + pr_title="[TEST] Release ${plugin_version}" + fi + + local pr_output + local pr_exit_code + pr_output=$(gh pr create \ + --title "$pr_title" \ + --body "$pr_body" \ + --label "release,automated pr" \ + --base main \ + --head "$branch_name" \ + --repo "urbanairship/$(get_repo_name "$plugin")" 2>&1) + pr_exit_code=$? + + if [ $pr_exit_code -ne 0 ]; then + # Check for "already exists" which is not an error + if echo "$pr_output" | grep -q "already exists"; then + echo " โ„น๏ธ PR already exists for this branch" + PR_URL=$(gh pr list --repo "urbanairship/$(get_repo_name "$plugin")" \ + --head "$branch_name" --json url --jq '.[0].url' 2>/dev/null) + echo "$PR_URL" + return 0 + fi + echo " โœ— Failed to create PR: $pr_output" >&2 + return 1 + fi + + # Extract URL from successful output (URL is typically on its own line) + PR_URL=$(echo "$pr_output" | grep -oE "https://github.com/[^[:space:]]+" | head -1) + + if [ -z "$PR_URL" ]; then + echo " โš ๏ธ PR created but couldn't extract URL from output" >&2 + PR_URL="CHECK_GITHUB" + fi + + # Validate PR content + if [ -n "$PR_URL" ] && [ "$PR_URL" != "CHECK_GITHUB" ]; then + validate_pr "$plugin" "$plugin_version" "$PR_URL" "urbanairship/$(get_repo_name "$plugin")" + fi + + echo "$PR_URL" +} + +# Main execution +main() { + echo -e "${BLUE}${BOLD}Centralized Plugin Release System${NC}" + echo "====================================" + echo -e "Proxy: ${BOLD}${PROXY_VERSION}${NC}" + echo -e "iOS: ${IOS_VERSION:-not specified}" + echo -e "Android: ${ANDROID_VERSION:-not specified}" + echo "" + + # Validate inputs + validate_inputs + + # Calculate plugin versions + calculate_plugin_versions + + # Clone all repos + clone_plugins + + # Update each plugin and collect PR URLs + # Disable exit-on-error for plugin updates so one failure doesn't kill all + set +e + for plugin in "${PLUGIN_KEYS[@]}"; do + # Skip if plugin is disabled + if should_skip_plugin "$plugin"; then + set_pr_url "$plugin" "SKIPPED" + continue + fi + + echo "" + + # Update plugin (with error handling) + if pr_url=$(update_plugin "$plugin" "$(get_new_version "$plugin")" "$WORK_DIR/$(get_repo_name "$plugin")" 2>&1); then + set_pr_url "$plugin" "$pr_url" + echo -e " ${GREEN}โœ“ PR created: $pr_url${NC}" + else + echo -e " ${RED}โœ— Failed to update ${plugin}${NC}" + echo -e " Error: $pr_url" + set_pr_url "$plugin" "FAILED" + fi + done + # Re-enable exit-on-error + set -e + + # Output summary + echo "" + echo -e "${GREEN}${BOLD}โœ… Release Preparation Complete${NC}" + echo "====================================" + echo "" + + for plugin in "${PLUGIN_KEYS[@]}"; do + local pr_url="$(get_pr_url "$plugin")" + local status_icon="โœ“" + local color="$GREEN" + if [ "$pr_url" = "FAILED" ]; then + status_icon="โœ—" + color="$RED" + elif [ "$pr_url" = "SKIPPED" ]; then + status_icon="โ—‹" + color="$YELLOW" + fi + echo -e "${color}${status_icon} $(get_display_name "$plugin") $(get_new_version "$plugin"): ${pr_url}${NC}" + done + + echo "" + + # Output for GitHub Actions + if [ -n "$GITHUB_OUTPUT" ]; then + for plugin in "${PLUGIN_KEYS[@]}"; do + echo "${plugin//-/_}_version=$(get_new_version "$plugin")" >> "$GITHUB_OUTPUT" + echo "${plugin//-/_}_pr_url=$(get_pr_url "$plugin")" >> "$GITHUB_OUTPUT" + done + fi + + # Cleanup + [ -n "$WORK_DIR" ] && rm -rf "$WORK_DIR" +} + +main "$@"