diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml new file mode 100644 index 00000000..3fdf3c57 --- /dev/null +++ b/.github/workflows/update-dependencies.yml @@ -0,0 +1,440 @@ +name: Update Intercom SDK Dependencies + +on: + schedule: + # Run daily at 9 AM UTC + - cron: '0 9 * * *' + workflow_dispatch: + +jobs: + update-dependencies: + runs-on: macos-latest + permissions: + contents: write + pull-requests: write + issues: read + actions: read + + steps: + - name: 🔍 Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: âš™ī¸ Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.14.0' + + - name: đŸ“Ļ Enable Corepack and install semver + run: | + corepack enable + npm install -g semver@7.7.2 + + - name: 🔍 Get current dependency versions + id: current-versions + run: | + # Extract current iOS version from podspec + IOS_VERSION=$(grep -E "s\.dependency.*Intercom" intercom-react-native.podspec | sed -E "s/.*'([^']+)'.*/\1/" | sed 's/~> //') + + # Extract current Android version from build.gradle + ANDROID_VERSION=$(grep -E "io\.intercom\.android:intercom-sdk:" android/build.gradle | head -1 | sed -E "s/.*:([^']+)'.*/\1/") + + echo "📱 Current iOS SDK: $IOS_VERSION" + echo "🤖 Current Android SDK: $ANDROID_VERSION" + + echo "ios_version=$IOS_VERSION" >> $GITHUB_OUTPUT + echo "android_version=$ANDROID_VERSION" >> $GITHUB_OUTPUT + + - name: 🚀 Get latest SDK releases + id: latest-versions + run: | + echo "🔄 Fetching latest releases..." + + # Get iOS release + IOS_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-ios/releases/latest) + IOS_VERSION=$(echo "$IOS_RELEASE" | jq -r '.tag_name // "null"') + IOS_CHANGELOG=$(echo "$IOS_RELEASE" | jq -r '.body // "No changelog available"') + + # Get Android release + ANDROID_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-android/releases/latest) + ANDROID_VERSION=$(echo "$ANDROID_RELEASE" | jq -r '.tag_name // "null"') + ANDROID_CHANGELOG=$(echo "$ANDROID_RELEASE" | jq -r '.body // "No changelog available"') + + echo "📱 Latest iOS SDK: $IOS_VERSION" + echo "🤖 Latest Android SDK: $ANDROID_VERSION" + + echo "ios_version=$IOS_VERSION" >> $GITHUB_OUTPUT + echo "android_version=$ANDROID_VERSION" >> $GITHUB_OUTPUT + echo "ios_changelog<> $GITHUB_OUTPUT + echo "$IOS_CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "android_changelog<> $GITHUB_OUTPUT + echo "$ANDROID_CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: 🧮 Analyze update requirements + id: update-analysis + run: | + CURRENT_IOS="${{ steps.current-versions.outputs.ios_version }}" + LATEST_IOS="${{ steps.latest-versions.outputs.ios_version }}" + CURRENT_ANDROID="${{ steps.current-versions.outputs.android_version }}" + LATEST_ANDROID="${{ steps.latest-versions.outputs.android_version }}" + + echo "📊 Version Analysis:" + echo " iOS: $CURRENT_IOS → $LATEST_IOS" + echo " Android: $CURRENT_ANDROID → $LATEST_ANDROID" + + # Determine updates needed + IOS_NEEDS_UPDATE=false + ANDROID_NEEDS_UPDATE=false + OVERALL_UPDATE_NEEDED=false + + # Check iOS + if [ "$LATEST_IOS" != "null" ] && [ -n "$LATEST_IOS" ]; then + CURRENT_IOS_CLEAN=$(echo "$CURRENT_IOS" | sed 's/^v//') + LATEST_IOS_CLEAN=$(echo "$LATEST_IOS" | sed 's/^v//') + + if [ "$CURRENT_IOS_CLEAN" != "$LATEST_IOS_CLEAN" ]; then + IOS_NEEDS_UPDATE=true + OVERALL_UPDATE_NEEDED=true + echo "✅ iOS update needed: $CURRENT_IOS_CLEAN → $LATEST_IOS_CLEAN" + else + echo "â„šī¸ iOS already up to date" + fi + else + echo "âš ī¸ No iOS release available - skipping" + LATEST_IOS_CLEAN="null" + fi + + # Check Android + if [ "$LATEST_ANDROID" != "null" ] && [ -n "$LATEST_ANDROID" ]; then + CURRENT_ANDROID_CLEAN=$(echo "$CURRENT_ANDROID" | sed 's/^v//') + LATEST_ANDROID_CLEAN=$(echo "$LATEST_ANDROID" | sed 's/^v//') + + if [ "$CURRENT_ANDROID_CLEAN" != "$LATEST_ANDROID_CLEAN" ]; then + ANDROID_NEEDS_UPDATE=true + OVERALL_UPDATE_NEEDED=true + echo "✅ Android update needed: $CURRENT_ANDROID_CLEAN → $LATEST_ANDROID_CLEAN" + else + echo "â„šī¸ Android already up to date" + fi + else + echo "âš ī¸ No Android release available - skipping" + LATEST_ANDROID_CLEAN="null" + fi + + # Output results + echo "ios_needs_update=$IOS_NEEDS_UPDATE" >> $GITHUB_OUTPUT + echo "android_needs_update=$ANDROID_NEEDS_UPDATE" >> $GITHUB_OUTPUT + echo "overall_update_needed=$OVERALL_UPDATE_NEEDED" >> $GITHUB_OUTPUT + echo "ios_new_version=${LATEST_IOS_CLEAN:-null}" >> $GITHUB_OUTPUT + echo "android_new_version=${LATEST_ANDROID_CLEAN:-null}" >> $GITHUB_OUTPUT + + if [ "$OVERALL_UPDATE_NEEDED" = false ]; then + echo "đŸŽ¯ No updates needed - workflow will exit cleanly" + fi + + - name: 🔍 Check existing PRs + if: steps.update-analysis.outputs.overall_update_needed == 'true' + id: pr-analysis + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TARGET_IOS="${{ steps.update-analysis.outputs.ios_new_version }}" + TARGET_ANDROID="${{ steps.update-analysis.outputs.android_new_version }}" + IOS_NEEDS_UPDATE="${{ steps.update-analysis.outputs.ios_needs_update }}" + ANDROID_NEEDS_UPDATE="${{ steps.update-analysis.outputs.android_needs_update }}" + + echo "🔍 Checking for existing dependency PRs..." + echo " Target: iOS=$TARGET_IOS, Android=$TARGET_ANDROID" + + # Find existing PRs + EXISTING_PRS=$(gh pr list --state open --search "update Intercom SDK dependencies" --json number,title,headRefName 2>/dev/null || echo "[]") + + if [ "$EXISTING_PRS" = "[]" ] || [ "$(echo "$EXISTING_PRS" | jq 'length')" -eq 0 ]; then + echo "✅ No existing PRs found - proceeding with update" + echo "should_create_pr=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "📋 Found existing dependency PRs:" + echo "$EXISTING_PRS" | jq -r '.[] | " PR #\(.number): \(.title)"' + + SHOULD_CREATE_PR=true + + # Analyze each existing PR + for pr_data in $(echo "$EXISTING_PRS" | jq -r '.[] | @base64'); do + PR_INFO=$(echo "$pr_data" | base64 --decode) + PR_NUMBER=$(echo "$PR_INFO" | jq -r '.number') + PR_TITLE=$(echo "$PR_INFO" | jq -r '.title') + PR_BRANCH=$(echo "$PR_INFO" | jq -r '.headRefName') + + echo "🔍 Analyzing PR #$PR_NUMBER: $PR_TITLE" + + # Check if this PR already targets our exact versions + IOS_MATCHES=true + ANDROID_MATCHES=true + + if [ "$IOS_NEEDS_UPDATE" = "true" ]; then + if echo "$PR_TITLE" | grep -q "iOS:.*→"; then + EXISTING_IOS_TARGET=$(echo "$PR_TITLE" | sed -n 's/.*iOS:[^→]*→ *\([^,)]*\).*/\1/p' | tr -d ' ') + if [ "$EXISTING_IOS_TARGET" != "$TARGET_IOS" ]; then + IOS_MATCHES=false + echo " 📱 iOS version mismatch: PR has $EXISTING_IOS_TARGET, need $TARGET_IOS" + fi + else + IOS_MATCHES=false + fi + fi + + if [ "$ANDROID_NEEDS_UPDATE" = "true" ]; then + if echo "$PR_TITLE" | grep -q "Android:.*→"; then + EXISTING_ANDROID_TARGET=$(echo "$PR_TITLE" | sed -n 's/.*Android:[^→]*→ *\([^,)]*\).*/\1/p' | tr -d ' ') + if [ "$EXISTING_ANDROID_TARGET" != "$TARGET_ANDROID" ]; then + ANDROID_MATCHES=false + echo " 🤖 Android version mismatch: PR has $EXISTING_ANDROID_TARGET, need $TARGET_ANDROID" + fi + else + ANDROID_MATCHES=false + fi + fi + + # Decision logic + if [ "$IOS_MATCHES" = true ] && [ "$ANDROID_MATCHES" = true ]; then + echo " đŸŽ¯ PR #$PR_NUMBER already targets correct versions - no action needed" + SHOULD_CREATE_PR=false + break + else + echo " đŸ—‘ī¸ Closing outdated PR #$PR_NUMBER" + gh pr close "$PR_NUMBER" --comment "Closing this PR as newer SDK versions are available. A new PR will be created with the latest versions." || true + + if [ "$PR_BRANCH" != "null" ] && [ "$PR_BRANCH" != "main" ]; then + echo " đŸ—‘ī¸ Deleting branch: $PR_BRANCH" + git push origin --delete "$PR_BRANCH" 2>/dev/null || true + fi + fi + done + + echo "should_create_pr=$SHOULD_CREATE_PR" >> $GITHUB_OUTPUT + + if [ "$SHOULD_CREATE_PR" = true ]; then + echo "🚀 Ready to create new PR" + else + echo "â­ī¸ Skipping PR creation - existing PR is current" + fi + + - name: 🔧 Setup Ruby for CocoaPods + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: đŸ› ī¸ Create update branch + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' + id: create-branch + run: | + # Configure git + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + + # Create unique branch + BRANCH_NAME="update-intercom-sdks-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$BRANCH_NAME" + + echo "đŸŒŋ Created branch: $BRANCH_NAME" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: 📝 Update dependency files + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' + id: update-files + run: | + CHANGES="" + PR_TITLE_PARTS="" + + # Update iOS if needed + if [ "${{ steps.update-analysis.outputs.ios_needs_update }}" = "true" ]; then + NEW_IOS="${{ steps.update-analysis.outputs.ios_new_version }}" + OLD_IOS="${{ steps.current-versions.outputs.ios_version }}" + + echo "📱 Updating iOS SDK: $OLD_IOS → $NEW_IOS" + sed -i '' "s/s\.dependency \"Intercom\", '~> [^']*'/s.dependency \"Intercom\", '~> $NEW_IOS'/" intercom-react-native.podspec + + CHANGES="$CHANGES\\n- Updated iOS SDK from $OLD_IOS to $NEW_IOS" + PR_TITLE_PARTS="iOS: $OLD_IOS → $NEW_IOS" + + git add intercom-react-native.podspec + fi + + # Update Android if needed + if [ "${{ steps.update-analysis.outputs.android_needs_update }}" = "true" ]; then + NEW_ANDROID="${{ steps.update-analysis.outputs.android_new_version }}" + OLD_ANDROID="${{ steps.current-versions.outputs.android_version }}" + + echo "🤖 Updating Android SDK: $OLD_ANDROID → $NEW_ANDROID" + sed -i '' "s/io\.intercom\.android:intercom-sdk:[^']*/io.intercom.android:intercom-sdk:$NEW_ANDROID/" android/build.gradle + sed -i '' "s/io\.intercom\.android:intercom-sdk-ui:[^']*/io.intercom.android:intercom-sdk-ui:$NEW_ANDROID/" android/build.gradle + + CHANGES="$CHANGES\\n- Updated Android SDK from $OLD_ANDROID to $NEW_ANDROID" + + if [ -n "$PR_TITLE_PARTS" ]; then + PR_TITLE_PARTS="$PR_TITLE_PARTS, Android: $OLD_ANDROID → $NEW_ANDROID" + else + PR_TITLE_PARTS="Android: $OLD_ANDROID → $NEW_ANDROID" + fi + + git add android/build.gradle + fi + + # Determine version bump type using semver script + echo "📈 Analyzing version bump requirements..." + VERSION_BUMP_TYPE=$(node semver.js analyze \ + "${{ steps.current-versions.outputs.ios_version }}" \ + "${{ steps.update-analysis.outputs.ios_new_version }}" \ + "${{ steps.current-versions.outputs.android_version }}" \ + "${{ steps.update-analysis.outputs.android_new_version }}") + + # Get current version and increment using semver script + echo "📈 Applying $VERSION_BUMP_TYPE version bump..." + CURRENT_VERSION=$(node semver.js current) + NEW_VERSION=$(node semver.js inc "$CURRENT_VERSION" "$VERSION_BUMP_TYPE") + + # Update package.json version using semver script + node semver.js update "$NEW_VERSION" + + echo "📈 Version bumped ($VERSION_BUMP_TYPE): $CURRENT_VERSION → $NEW_VERSION" + git add package.json semver.js + + # Update yarn.lock + echo "đŸ“Ļ Installing dependencies to update lockfile..." + yarn install --immutable + git add yarn.lock + + CHANGES="$CHANGES\\n- Bumped version from $CURRENT_VERSION to $NEW_VERSION ($VERSION_BUMP_TYPE)" + + # Store for next steps + echo "changes<> $GITHUB_OUTPUT + echo -e "$CHANGES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "pr_title_parts=$PR_TITLE_PARTS" >> $GITHUB_OUTPUT + + - name: đŸ“Ļ Update iOS Example Pod Dependencies + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' && steps.update-analysis.outputs.ios_needs_update == 'true' + run: | + # Find iOS example directories with Podfiles + IOS_EXAMPLE_DIRS=$(find examples -name "Podfile" -exec dirname {} \; | grep -E "(ios|example)" | head -5) + + for dir in $IOS_EXAMPLE_DIRS; do + if [ -f "$dir/Podfile" ]; then + echo "📱 Updating pods in $dir" + cd "$dir" + bundle exec pod install --repo-update || pod install --repo-update || echo "Pod install failed in $dir" + cd - > /dev/null + + # Add any updated Podfile.lock files + git add "$dir/Podfile.lock" 2>/dev/null || true + fi + done + + - name: 💾 Commit and push changes + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' + run: | + # Check if we have changes to commit + if git diff --cached --quiet; then + echo "❌ No changes to commit - this should not happen!" + exit 1 + fi + + echo "💾 Committing changes..." + git commit -m "chore: update Intercom SDK dependencies (${{ steps.update-files.outputs.pr_title_parts }}) + + ${{ steps.update-files.outputs.changes }} + - Updated lockfiles and example projects + + echo "🚀 Pushing branch: ${{ steps.create-branch.outputs.branch_name }}" + git push origin "${{ steps.create-branch.outputs.branch_name }}" + + - name: đŸŽ¯ Create Pull Request + if: steps.update-analysis.outputs.overall_update_needed == 'true' && steps.pr-analysis.outputs.should_create_pr == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "đŸŽ¯ Creating pull request..." + + # Build PR description + cat > pr_body.md << 'EOF' + ## 🔄 Automated Intercom SDK Dependency Update + + This PR updates the Intercom SDK dependencies to their latest versions. + + ### 📋 Changes Made: + ${{ steps.update-files.outputs.changes }} + + ### 📚 Release Notes: + EOF + + # Add iOS release notes if updated + if [ "${{ steps.update-analysis.outputs.ios_needs_update }}" = "true" ]; then + cat >> pr_body.md << 'EOF' + + #### 📱 iOS SDK ${{ steps.update-analysis.outputs.ios_new_version }} + [View Release Notes](https://github.com/intercom/intercom-ios/releases/tag/${{ steps.latest-versions.outputs.ios_version }}) + + ``` + ${{ steps.latest-versions.outputs.ios_changelog }} + ``` + EOF + fi + + # Add Android release notes if updated + if [ "${{ steps.update-analysis.outputs.android_needs_update }}" = "true" ]; then + cat >> pr_body.md << 'EOF' + + #### 🤖 Android SDK ${{ steps.update-analysis.outputs.android_new_version }} + [View Release Notes](https://github.com/intercom/intercom-android/releases/tag/${{ steps.latest-versions.outputs.android_version }}) + + ``` + ${{ steps.latest-versions.outputs.android_changelog }} + ``` + EOF + fi + + cat >> pr_body.md << 'EOF' + + --- + 🤖 This PR was automatically created by the [Update Dependencies workflow](.github/workflows/update-dependencies.yml). + EOF + + # Create PR + if gh pr create \ + --title "chore: update Intercom SDK dependencies (${{ steps.update-files.outputs.pr_title_parts }})" \ + --body-file pr_body.md \ + --head "${{ steps.create-branch.outputs.branch_name }}" \ + --base "main"; then + echo "✅ Pull request created successfully!" + else + echo "❌ Failed to create pull request" + echo "📋 Branch pushed: ${{ steps.create-branch.outputs.branch_name }}" + echo "📋 Title: chore: update Intercom SDK dependencies (${{ steps.update-files.outputs.pr_title_parts }})" + exit 1 + fi + + - name: 📊 Summary + if: always() + run: | + echo "## 📊 Workflow Summary" + + if [ "${{ steps.update-analysis.outputs.overall_update_needed }}" != "true" ]; then + echo "â„šī¸ No dependency updates needed - all SDKs are current" + echo " 📱 iOS: ${{ steps.current-versions.outputs.ios_version }}" + echo " 🤖 Android: ${{ steps.current-versions.outputs.android_version }}" + elif [ "${{ steps.pr-analysis.outputs.should_create_pr }}" != "true" ]; then + echo "â­ī¸ Updates available but existing PR already covers latest versions" + elif [ "${{ job.status }}" = "success" ]; then + echo "✅ Successfully created dependency update PR!" + echo " 📋 Title: chore: update Intercom SDK dependencies (${{ steps.update-files.outputs.pr_title_parts }})" + echo " đŸŒŋ Branch: ${{ steps.create-branch.outputs.branch_name }}" + else + echo "❌ Workflow failed - check logs above" + fi \ No newline at end of file diff --git a/semver.js b/semver.js new file mode 100644 index 00000000..1aaa5fb2 --- /dev/null +++ b/semver.js @@ -0,0 +1,158 @@ +#!/usr/bin/env node + +const semver = require('semver'); + +// Parse command line arguments +const args = process.argv.slice(2); +const command = args[0]; + +function showHelp() { + console.log(` +Usage: node semver.js [arguments] + +Commands: + diff - Compare two versions and return difference type + inc - Increment version by type (major|minor|patch) + current - Get current version from package.json + +Examples: + node semver.js diff 19.1.2 19.2.0 # Returns: minor + node semver.js inc 1.0.0 major # Returns: 2.0.0 + node semver.js current # Returns: 1.0.0 +`); +} + +function getCurrentVersion() { + try { + const pkg = require('./package.json'); + return pkg.version; + } catch (error) { + console.error('Error reading package.json:', error.message); + process.exit(1); + } +} + +function updatePackageVersion(newVersion) { + try { + const fs = require('fs'); + const pkg = require('./package.json'); + pkg.version = newVersion; + fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + return newVersion; + } catch (error) { + console.error('Error updating package.json:', error.message); + process.exit(1); + } +} + +switch (command) { + case 'diff': { + const [version1, version2] = args.slice(1); + if (!version1 || !version2) { + console.error('Error: diff requires two versions'); + process.exit(1); + } + + // Clean versions (remove 'v' prefix if present) + const clean1 = version1.replace(/^v/, ''); + const clean2 = version2.replace(/^v/, ''); + + try { + const diff = semver.diff(clean1, clean2); + console.log(diff || 'patch'); // Default to patch if no diff + } catch (error) { + console.error('Error comparing versions:', error.message); + process.exit(1); + } + break; + } + + case 'inc': { + const [version, type] = args.slice(1); + if (!version || !type) { + console.error('Error: inc requires version and type (major|minor|patch)'); + process.exit(1); + } + + try { + const newVersion = semver.inc(version, type); + console.log(newVersion); + } catch (error) { + console.error('Error incrementing version:', error.message); + process.exit(1); + } + break; + } + + case 'current': { + const currentVersion = getCurrentVersion(); + console.log(currentVersion); + break; + } + + case 'update': { + const [newVersion] = args.slice(1); + if (!newVersion) { + console.error('Error: update requires new version'); + process.exit(1); + } + + const result = updatePackageVersion(newVersion); + console.log(result); + break; + } + + case 'analyze': { + // Special command for workflow: analyze multiple version changes and determine bump type + const iosOld = args[1]; + const iosNew = args[2]; + const androidOld = args[3]; + const androidNew = args[4]; + + let bumpType = 'patch'; // Default + + if (iosOld && iosNew && iosOld !== 'null' && iosNew !== 'null') { + const iosDiff = semver.diff( + iosOld.replace(/^v/, ''), + iosNew.replace(/^v/, '') + ); + console.error(`📱 iOS: ${iosOld} → ${iosNew} (${iosDiff})`); + + if (iosDiff === 'major') { + bumpType = 'major'; + } else if (iosDiff === 'minor' && bumpType !== 'major') { + bumpType = 'minor'; + } + } + + if ( + androidOld && + androidNew && + androidOld !== 'null' && + androidNew !== 'null' + ) { + const androidDiff = semver.diff( + androidOld.replace(/^v/, ''), + androidNew.replace(/^v/, '') + ); + console.error( + `🤖 Android: ${androidOld} → ${androidNew} (${androidDiff})` + ); + + if (androidDiff === 'major') { + bumpType = 'major'; + } else if (androidDiff === 'minor' && bumpType !== 'major') { + bumpType = 'minor'; + } + } + + console.error(`📈 Determined bump type: ${bumpType}`); + console.log(bumpType); + break; + } + + default: + console.error(`Unknown command: ${command}`); + showHelp(); + process.exit(1); +}