diff --git a/.af-smoke/rc-test-plan.json b/.af-smoke/rc-test-plan.json index 0ef45db4..47ec8482 100644 --- a/.af-smoke/rc-test-plan.json +++ b/.af-smoke/rc-test-plan.json @@ -11,8 +11,8 @@ "config": { "android": { - "package_name": "com.appsflyer.qa.reactnative", - "activity": ".MainActivity", + "package_name": "com.appsflyer.engagement", + "activity": "com.appsflyer.qa.reactnative.MainActivity", "apk_path": "example_rc_smoke/android/app/build/outputs/apk/debug/app-debug.apk", "build_cmd": "cd example_rc_smoke/android && ./gradlew assembleDebug" }, @@ -30,7 +30,7 @@ "scenario_ref": "SMOKE-001", "description": "Fresh install using the npm-pinned RC build. Validates SDK startup, install conversion data, pre/post-start API markers, and standard events.", "requires_fresh_install": true, - "wait_after_launch_sec": 30, + "wait_after_launch_sec": 60, "checks": [ { "id": "sdk_started", @@ -125,7 +125,7 @@ "scenario_ref": "SMOKE-003", "description": "Fresh install of the RC build, then deep link while app is in foreground. Verifies SDK start and deep link callback.", "requires_fresh_install": true, - "wait_after_launch_sec": 30, + "wait_after_launch_sec": 60, "wait_after_trigger_sec": 15, "deep_link_url": "afqa-reactnative://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test", "pre_actions": { diff --git a/.github/workflows/android-e2e.yml b/.github/workflows/android-e2e.yml index 5e8a7d19..89ee44d8 100644 --- a/.github/workflows/android-e2e.yml +++ b/.github/workflows/android-e2e.yml @@ -10,7 +10,7 @@ # reactivecircus/android-emulator-runner (KVM on ubuntu-latest). # # Triggers: -# - workflow_call from rc-release.yml (the gate before publish-rc) +# - workflow_call from release.yml (the gate before publish-to-npm) # - workflow_dispatch for manual reruns # - Weekly cron (Sunday 03:00 UTC) to catch SDK / RN drift # diff --git a/.github/workflows/ios-e2e.yml b/.github/workflows/ios-e2e.yml index c91d0733..148fd9cb 100644 --- a/.github/workflows/ios-e2e.yml +++ b/.github/workflows/ios-e2e.yml @@ -9,7 +9,7 @@ # the unified scripts/af-scenario-runner.sh on an iOS simulator. # # Triggers: -# - workflow_call from rc-release.yml (the gate before publish-rc) +# - workflow_call from release.yml (the gate before publish-to-npm) # - workflow_dispatch for manual reruns and ad-hoc validation # - Weekly cron (Sunday 02:00 UTC) to catch SDK / RN drift # diff --git a/.github/workflows/lint-test-build.yml b/.github/workflows/lint-test-build.yml index 7ef9ed9a..7c1181bf 100644 --- a/.github/workflows/lint-test-build.yml +++ b/.github/workflows/lint-test-build.yml @@ -4,7 +4,7 @@ # # Purpose: Validates code quality, runs unit tests, and builds the demo app # in release mode for both Android and iOS on every PR and push to -# development/master. Also reusable from rc-release.yml. +# development/master. Also reusable from release.yml. # # What it does: # 1. Lints + runs Jest unit tests with coverage. @@ -15,7 +15,7 @@ # - Pull requests to development or master branches # - Direct pushes to development or master branches # - Manual workflow dispatch for testing -# - workflow_call (rc-release.yml) +# - workflow_call (release.yml) # # ============================================================================= diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml deleted file mode 100644 index 65ce2e45..00000000 --- a/.github/workflows/production-release.yml +++ /dev/null @@ -1,505 +0,0 @@ -# ============================================================================= -# Production Release Workflow - Publish to npm -# ============================================================================= -# -# Purpose: Publishes the React Native plugin to npm after a release PR is -# merged to master. -# -# Flow: -# 1. Validates the merge is from a release branch -# 2. Publishes to npm with the `latest` tag -# 3. Verifies the package is live on npm (retry loop, max 120s) -# 4. Creates a GitHub release with release notes -# 5. Fetches Jira tickets for the fix version -# 6. Notifies team via Slack -# -# Triggers: -# - Pull request closed (merged) to master branch from releases/* branches -# - Manual workflow dispatch (for republishing or testing) -# -# ============================================================================= - -name: Production Release - Publish to npm - -on: - pull_request: - types: - - closed - branches: - - master - - workflow_dispatch: - inputs: - version: - description: 'Version to release (must match package.json)' - required: true - type: string - dry_run: - description: 'Dry run (do not actually publish)' - required: false - type: boolean - default: false - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - # =========================================================================== - # Job 1: Validate Release - # =========================================================================== - validate-release: - name: Validate Release - runs-on: ubuntu-latest - - if: >- - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'releases/')) - - outputs: - version: ${{ steps.get-version.outputs.version }} - is_valid: ${{ steps.validate.outputs.is_valid }} - is_dry_run: ${{ steps.dry-run.outputs.value }} - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 20 - - - name: Resolve dry_run across trigger paths - id: dry-run - env: - EVENT_NAME: ${{ github.event_name }} - DISPATCH_DRY: ${{ github.event.inputs.dry_run }} - run: | - set -euo pipefail - if [[ "$EVENT_NAME" == "workflow_dispatch" && "$DISPATCH_DRY" == "true" ]]; then - echo "value=true" >> "$GITHUB_OUTPUT" - else - echo "value=false" >> "$GITHUB_OUTPUT" - fi - - - name: Validate release source - id: validate - env: - EVENT_NAME: ${{ github.event_name }} - PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - echo "Manual run - skipping branch validation" - echo "is_valid=true" >> "$GITHUB_OUTPUT" - else - SOURCE_BRANCH="$PR_HEAD_REF" - echo "Source branch: $SOURCE_BRANCH" - - if [[ $SOURCE_BRANCH =~ ^releases/ ]]; then - echo "Valid release branch: $SOURCE_BRANCH" - echo "is_valid=true" >> "$GITHUB_OUTPUT" - else - echo "::error::Not a release branch: $SOURCE_BRANCH" - echo "is_valid=false" >> "$GITHUB_OUTPUT" - exit 1 - fi - fi - - - name: Get version from package.json - id: get-version - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_VERSION: ${{ github.event.inputs.version }} - run: | - if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - VERSION="$INPUT_VERSION" - echo "Using provided version: $VERSION" - else - VERSION=$(node -p "require('./package.json').version") - echo "Extracted version from package.json: $VERSION" - fi - - # Validate version format (X.Y.Z, no -rc suffix for production) - if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Valid production version format: $VERSION" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - else - echo "::error::Invalid production version format: $VERSION (expected X.Y.Z)" - exit 1 - fi - - - name: Check if tag already exists - env: - VERSION: ${{ steps.get-version.outputs.version }} - DRY_RUN: ${{ steps.dry-run.outputs.value }} - run: | - git fetch --tags - if git rev-parse "v$VERSION" >/dev/null 2>&1 || git rev-parse "$VERSION" >/dev/null 2>&1; then - echo "::warning::Tag for $VERSION already exists" - if [[ "$DRY_RUN" != "true" ]]; then - echo "::error::Cannot create duplicate release" - exit 1 - fi - fi - echo "Tag $VERSION does not exist - safe to proceed" - - # =========================================================================== - # Job 2: Publish to npm - # =========================================================================== - publish-to-npm: - name: Publish to npm - runs-on: ubuntu-latest - needs: [validate-release] - if: needs.validate-release.result == 'success' && needs.validate-release.outputs.is_valid == 'true' - permissions: - contents: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - - - name: Publish to npm - if: needs.validate-release.outputs.is_dry_run != 'true' - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - echo "Publishing version $VERSION to npm..." - npm publish - echo "Published $VERSION to npm" - - - name: Dry-run publish - if: needs.validate-release.outputs.is_dry_run == 'true' - run: | - echo "DRY RUN - would publish $(node -p "require('./package.json').version") to npm" - npm pack --dry-run - - - name: Verify publication - if: needs.validate-release.outputs.is_dry_run != 'true' - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - MAX_WAIT=120 - POLL=15 - ELAPSED=0 - while (( ELAPSED < MAX_WAIT )); do - if npm view "react-native-appsflyer@$VERSION" version 2>/dev/null | grep -Fxq "$VERSION"; then - echo "Verified: $VERSION is live on npm" - break - fi - echo "Waiting for npm propagation (${ELAPSED}s / ${MAX_WAIT}s)..." - sleep "$POLL" - ELAPSED=$(( ELAPSED + POLL )) - done - if (( ELAPSED >= MAX_WAIT )); then - echo "::warning::npm propagation exceeded ${MAX_WAIT}s --- verify manually on npmjs.com" - fi - - # =========================================================================== - # Job 3: Create GitHub Release - # =========================================================================== - create-github-release: - name: Create GitHub Release - runs-on: ubuntu-latest - needs: [validate-release, publish-to-npm] - if: needs.publish-to-npm.result == 'success' && needs.validate-release.outputs.is_dry_run != 'true' - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Extract release notes from CHANGELOG - id: changelog - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - echo "Extracting release notes for version $VERSION from CHANGELOG.md" - - if [ -f "CHANGELOG.md" ]; then - RELEASE_NOTES=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | sed '1d;$d') - - if [ -z "$RELEASE_NOTES" ]; then - echo "::warning::Could not find release notes for $VERSION in CHANGELOG.md" - RELEASE_NOTES="Release version $VERSION. See [CHANGELOG.md](CHANGELOG.md) for details." - fi - else - RELEASE_NOTES="Release version $VERSION." - fi - - echo "$RELEASE_NOTES" > release_notes.md - - - name: Build release notes - env: - VERSION: ${{ needs.validate-release.outputs.version }} - REPO: ${{ github.repository }} - run: | - cat > final_release_notes.md << EOF - # AppsFlyer React Native Plugin v$VERSION - - ## Installation - - \`\`\`bash - npm install react-native-appsflyer@$VERSION - \`\`\` - - Then for iOS: - \`\`\`bash - cd ios && pod install - \`\`\` - - ## Changes in This Release - - $(cat release_notes.md) - - ## Documentation - - - [Installation Guide](https://github.com/$REPO/blob/master/Docs/Installation.md) - - [API Documentation](https://github.com/$REPO/blob/master/Docs/API.md) - - [Deep Linking Guide](https://github.com/$REPO/blob/master/Docs/DeepLink.md) - - [Expo Integration](https://github.com/$REPO/blob/master/Docs/ExpoIntegration.md) - - ## Links - - - [npm Package](https://www.npmjs.com/package/react-native-appsflyer/v/$VERSION) - - [GitHub Repository](https://github.com/$REPO) - - [AppsFlyer Developer Hub](https://dev.appsflyer.com/) - - ## Support - - For issues and questions, please contact - EOF - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - gh release create "v$VERSION" \ - --title "v$VERSION" \ - --notes-file final_release_notes.md \ - --latest - - # =========================================================================== - # Job 4: Notify Team - # =========================================================================== - notify-team: - name: Notify Team - runs-on: ubuntu-latest - needs: [validate-release, publish-to-npm, create-github-release] - if: >- - always() && - needs.validate-release.outputs.is_dry_run != 'true' && - ( - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'releases/')) - ) - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 20 - - - name: Extract SDK versions and changelog - id: extract-info - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - # Extract Android SDK fallback version from build.gradle - ANDROID_SDK_VERSION=$(grep "af-android-sdk" android/build.gradle | grep -oP "'[0-9][^']*'" | tr -d "'" | head -1) - echo "android_sdk=$ANDROID_SDK_VERSION" >> "$GITHUB_OUTPUT" - - # Extract iOS SDK version from podspec - IOS_SDK_VERSION=$(grep "AppsFlyerFramework'" react-native-appsflyer.podspec | grep -oP "'~> \K[^']*" | head -1) - if [ -z "$IOS_SDK_VERSION" ]; then - IOS_SDK_VERSION=$(grep "AppsFlyerFramework'" react-native-appsflyer.podspec | grep -oP "'\K[0-9][^']*" | head -1) - fi - echo "ios_sdk=$IOS_SDK_VERSION" >> "$GITHUB_OUTPUT" - - # Extract changelog for this version - if [ -f "CHANGELOG.md" ]; then - CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- /- /' | head -5) - if [ -z "$CHANGELOG" ]; then - CHANGELOG="- Check CHANGELOG.md for details" - fi - else - CHANGELOG="- Check release notes for details" - fi - - echo "changelog<> "$GITHUB_OUTPUT" - echo "$CHANGELOG" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - - - name: Fetch Jira tickets - id: jira-tickets - continue-on-error: true - env: - VERSION: ${{ needs.validate-release.outputs.version }} - CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} - CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} - CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} - run: | - set +e - JIRA_FIX_VERSION="React Native SDK v$VERSION" - - echo "Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" - - if [[ -z "$CI_JIRA_EMAIL" ]] || [[ -z "$CI_JIRA_TOKEN" ]]; then - echo "::warning::Jira credentials not configured" - echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" - exit 0 - fi - - JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" - - JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" - ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) - - RESPONSE=$(curl -s -w "\n%{http_code}" \ - -u "$CI_JIRA_EMAIL:$CI_JIRA_TOKEN" \ - -H "Accept: application/json" \ - -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") - - HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [[ "$HTTP_CODE" != "200" ]]; then - echo "::warning::Jira API request failed with status $HTTP_CODE" - echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" - exit 0 - fi - - TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "- https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) - - if [ -z "$TICKETS" ]; then - echo "No linked tickets found for version: $JIRA_FIX_VERSION" - echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" - else - echo "Found Jira tickets:" - echo "$TICKETS" - echo "tickets<> "$GITHUB_OUTPUT" - echo "$TICKETS" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - fi - - - name: Determine status and failed stage - id: status - env: - VALIDATE_RESULT: ${{ needs.validate-release.result }} - PUBLISH_RESULT: ${{ needs.publish-to-npm.result }} - RELEASE_RESULT: ${{ needs.create-github-release.result }} - run: | - set -euo pipefail - if [[ "$VALIDATE_RESULT" == "success" \ - && "$PUBLISH_RESULT" == "success" \ - && "$RELEASE_RESULT" == "success" ]]; then - echo "success=true" >> "$GITHUB_OUTPUT" - echo "failed_stage=" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "success=false" >> "$GITHUB_OUTPUT" - if [[ "$VALIDATE_RESULT" != "success" ]]; then - echo "failed_stage=validate-release" >> "$GITHUB_OUTPUT" - elif [[ "$PUBLISH_RESULT" != "success" ]]; then - echo "failed_stage=publish-to-npm" >> "$GITHUB_OUTPUT" - else - echo "failed_stage=create-github-release" >> "$GITHUB_OUTPUT" - fi - - - name: Send Slack success notification - if: steps.status.outputs.success == 'true' - uses: slackapi/slack-github-action@v1 - with: - payload: | - { - "text": "\n:react::react::react::react::react::react::react::react::react::react::react::react:\n\n*React Native:*\nnpm install react-native-appsflyer@${{ needs.validate-release.outputs.version }} is published to Production.\n\n:white_check_mark: rc-smoke/npm passed before promotion (verified by promote-release.yml).\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/master\n:github: Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n:npm: https://www.npmjs.com/package/react-native-appsflyer/v/${{ needs.validate-release.outputs.version }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDKs:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n:react::react::react::react::react::react::react::react::react::react::react::react:" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} - - - name: Send Slack failure notification - if: steps.status.outputs.success == 'false' - uses: slackapi/slack-github-action@v1 - with: - payload: | - { - "text": "\n:warning: *React Native production release failed at `${{ steps.status.outputs.failed_stage }}`*\n\nVersion: ${{ needs.validate-release.outputs.version }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n*Stage results:*\n- validate-release: ${{ needs.validate-release.result }}\n- publish-to-npm: ${{ needs.publish-to-npm.result }}\n- create-github-release: ${{ needs.create-github-release.result }}" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} - - # =========================================================================== - # Job 5: Production Release Summary - # =========================================================================== - release-summary: - name: Release Summary - runs-on: ubuntu-latest - needs: [validate-release, publish-to-npm, create-github-release] - if: >- - always() && ( - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - startsWith(github.event.pull_request.head.ref, 'releases/')) - ) - - steps: - - name: Display Release Summary - env: - VERSION: ${{ needs.validate-release.outputs.version }} - DRY_RUN: ${{ needs.validate-release.outputs.is_dry_run }} - VALIDATE_RESULT: ${{ needs.validate-release.result }} - PUBLISH_RESULT: ${{ needs.publish-to-npm.result }} - RELEASE_RESULT: ${{ needs.create-github-release.result }} - REPO: ${{ github.repository }} - run: | - echo "=========================================" - echo "Production Release Summary" - echo "=========================================" - echo "Version: $VERSION" - echo "Dry Run: $DRY_RUN" - echo "-----------------------------------------" - echo "Validation: $VALIDATE_RESULT" - echo "npm Publish: $PUBLISH_RESULT" - echo "GitHub Release: $RELEASE_RESULT" - echo "=========================================" - - if [[ "$DRY_RUN" == "true" ]]; then - echo "This was a DRY RUN - no actual publishing occurred" - exit 0 - fi - - if [[ "$VALIDATE_RESULT" == "success" ]] && \ - [[ "$PUBLISH_RESULT" == "success" ]] && \ - [[ "$RELEASE_RESULT" == "success" ]]; then - echo "" - echo "Production Release Completed Successfully!" - echo "" - echo "Version $VERSION is now live!" - echo "" - echo "npm: https://www.npmjs.com/package/react-native-appsflyer/v/$VERSION" - echo "GitHub: https://github.com/$REPO/releases/tag/v$VERSION" - else - echo "" - echo "Production Release Failed" - echo "Check the logs above for details and retry if necessary" - exit 1 - fi diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml index 315120a2..bbaca6f2 100644 --- a/.github/workflows/promote-release.yml +++ b/.github/workflows/promote-release.yml @@ -17,7 +17,7 @@ # - Strips -rcN from all version files # - Commits changes to the release branch # 4. Human reviews and manually merges the PR -# 5. production-release.yml triggers on merge +# 5. release.yml triggers on merge (production path) # # ============================================================================= @@ -236,7 +236,7 @@ jobs: with: payload: | { - "text": "\n:warning: *React Native promote-release blocked*\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: ${{ github.event.pull_request.head.ref }}\n\nThe promote workflow could not prepare the release branch for production. Common causes:\n- `rc-smoke/npm` is missing or red on the PR head SHA --- bump to `rcN+1` and rerun rc-release.\n- The version-strip push was rejected (branch protection or stale ref).\n\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + "text": "\n:warning: *React Native promote-release blocked*\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: ${{ github.event.pull_request.head.ref }}\n\nThe promote workflow could not prepare the release branch for production. Common causes:\n- `rc-smoke/npm` is missing or red on the PR head SHA --- bump to `rcN+1` and rerun release.yml (RC type).\n- The version-strip push was rejected (branch protection or stale ref).\n\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml deleted file mode 100644 index 11bbda85..00000000 --- a/.github/workflows/rc-release.yml +++ /dev/null @@ -1,854 +0,0 @@ -# ============================================================================= -# RC (Release Candidate) Workflow - Pre-Production Release -# ============================================================================= -# -# Purpose: Creates a release candidate for QA testing before production release. -# -# What it does: -# 1. Validates the RC version format and inputs -# 2. Runs full CI pipeline (Jest + ESLint + release builds) -# 3. Creates a release branch with version bumps across 5+ files -# 4. Runs iOS + Android E2E tests on the release branch -# 5. Validates Jira fix version exists -# 6. Publishes to npm with --tag rc -# 7. Creates GitHub pre-release + PR to master -# 8. Notifies team via Slack -# -# Version format: X.Y.Z-rcN (e.g., 6.18.0-rc1) -# -# Triggers: -# - Manual workflow dispatch with required parameters -# -# ============================================================================= - -name: RC - Release Candidate - -on: - workflow_dispatch: - inputs: - rn_version: - description: 'React Native plugin version for this RC (e.g., 6.18.0-rc1)' - required: true - type: string - ios_sdk_version: - description: 'iOS native AppsFlyer SDK version (e.g., 6.18.0)' - required: true - type: string - android_sdk_version: - description: 'Android native AppsFlyer SDK version (e.g., 6.18.0)' - required: true - type: string - base_branch: - description: 'Base branch to create the release branch from' - required: false - default: development - type: string - pc_version: - description: 'PurchaseConnector iOS version override (leave empty to auto-fetch latest from GitHub)' - required: false - default: '' - type: string - skip_unit: - description: 'Skip unit tests and linting inside Lint, Test & Build' - required: false - type: boolean - default: false - skip_builds: - description: 'Skip Android + iOS release builds inside Lint, Test & Build' - required: false - type: boolean - default: false - skip_e2e: - description: 'Skip RC-E2E iOS + Android jobs (blocks publish-rc)' - required: false - type: boolean - default: false - dry_run: - description: 'Do not publish RC to npm (still opens PR and creates prerelease)' - required: false - type: boolean - default: true - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # =========================================================================== - # Job 1: Validate Inputs & Compute Branch - # =========================================================================== - - validate-release: - name: Validate Inputs & Compute Branch - runs-on: ubuntu-latest - - outputs: - version: ${{ steps.compute.outputs.version }} - base_version: ${{ steps.compute.outputs.base_version }} - is_rc: ${{ steps.compute.outputs.is_rc }} - is_valid: ${{ steps.compute.outputs.is_valid }} - base_branch: ${{ steps.compute.outputs.base_branch }} - release_branch: ${{ steps.compute.outputs.release_branch }} - ios_sdk_version: ${{ steps.compute.outputs.ios_sdk_version }} - android_sdk_version: ${{ steps.compute.outputs.android_sdk_version }} - is_dry_run: ${{ steps.dry-run.outputs.value }} - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Resolve dry_run - id: dry-run - env: - EVENT_NAME: ${{ github.event_name }} - DISPATCH_DRY: ${{ github.event.inputs.dry_run }} - run: | - set -euo pipefail - case "$EVENT_NAME" in - workflow_dispatch) - if [[ "$DISPATCH_DRY" == "true" ]]; then - echo "value=true" >> "$GITHUB_OUTPUT" - else - echo "value=false" >> "$GITHUB_OUTPUT" - fi - ;; - *) - echo "value=false" >> "$GITHUB_OUTPUT" - ;; - esac - - - name: Validate and compute - id: compute - env: - VERSION: ${{ github.event.inputs.rn_version }} - IOS_VER: ${{ github.event.inputs.ios_sdk_version }} - AND_VER: ${{ github.event.inputs.android_sdk_version }} - BASE_BRANCH_INPUT: ${{ github.event.inputs.base_branch }} - run: | - set -euo pipefail - - if [[ -z "$VERSION" || -z "$IOS_VER" || -z "$AND_VER" ]]; then - echo "Missing required inputs"; exit 1 - fi - - # RN version format: X.Y.Z-rcN (no +build) - if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+$ ]]; then - echo "rn_version must be X.Y.Z-rcN (e.g., 6.18.0-rc1)"; exit 1 - fi - if [[ ! $IOS_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "ios_sdk_version must be X.Y.Z"; exit 1 - fi - if [[ ! $AND_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "android_sdk_version must be X.Y.Z"; exit 1 - fi - - # Compute base version (remove -rcN) - BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') - - MAJOR_MINOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+') - MAJOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+') - RELEASE_BRANCH="releases/${MAJOR}.x.x/${MAJOR_MINOR}.x/${VERSION}" - - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT - echo "is_rc=true" >> $GITHUB_OUTPUT - echo "is_valid=true" >> $GITHUB_OUTPUT - echo "base_branch=$BASE_BRANCH_INPUT" >> $GITHUB_OUTPUT - echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT - echo "ios_sdk_version=$IOS_VER" >> $GITHUB_OUTPUT - echo "android_sdk_version=$AND_VER" >> $GITHUB_OUTPUT - - # =========================================================================== - # Job 2: Lint, Test & Build (reusable) - # =========================================================================== - - run-ci: - name: Lint, Test & Build - needs: validate-release - if: ${{ needs.validate-release.outputs.is_valid == 'true' }} - uses: ./.github/workflows/lint-test-build.yml - with: - skip_unit: ${{ github.event.inputs.skip_unit == 'true' }} - skip_builds: ${{ github.event.inputs.skip_builds == 'true' }} - secrets: inherit - - # =========================================================================== - # Job 3: Create/Update Release Branch and Apply Changes - # =========================================================================== - - prepare-branch: - name: Create Release Branch & Apply Changes - runs-on: ubuntu-latest - needs: [validate-release] - if: always() && needs.validate-release.outputs.is_valid == 'true' - outputs: - release_branch: ${{ steps.push.outputs.release_branch }} - steps: - - name: Checkout base branch - uses: actions/checkout@v5 - with: - ref: ${{ needs.validate-release.outputs.base_branch }} - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - - - name: Create release branch - id: branch - run: | - set -e - REL_BRANCH="${{ needs.validate-release.outputs.release_branch }}" - echo "Target release branch: $REL_BRANCH" - if git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then - echo "Branch already exists on remote. Checking it out." - git fetch origin "$REL_BRANCH":"$REL_BRANCH" - git checkout "$REL_BRANCH" - else - git checkout -b "$REL_BRANCH" - fi - - - name: Update package.json version - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - echo "Setting package.json version to $VERSION" - npm version "$VERSION" --no-git-tag-version - grep '"version"' package.json - - - name: Update Android SDK fallback in build.gradle - env: - AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} - run: | - sed -i.bak "s/af-android-sdk:\${safeExtGet('appsflyerVersion', '[^']*')}/af-android-sdk:\${safeExtGet('appsflyerVersion', '${AND_VER}')}/" android/build.gradle - rm -f android/build.gradle.bak - grep "af-android-sdk:" android/build.gradle - - - name: Update iOS SDK deps in podspec - env: - IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} - PC_VER_INPUT: ${{ github.event.inputs.pc_version }} - run: | - # AppsFlyerFramework (default) - sed -i.bak "s/'AppsFlyerFramework', '[^']*'/'AppsFlyerFramework', '${IOS_VER}'/" react-native-appsflyer.podspec - # AppsFlyerFramework/Strict - sed -i.bak "s|'AppsFlyerFramework/Strict', '[^']*'|'AppsFlyerFramework/Strict', '${IOS_VER}'|" react-native-appsflyer.podspec - - # PurchaseConnector (conditional dep, has its own version) - if grep -q "PurchaseConnector" react-native-appsflyer.podspec; then - if [[ -n "${PC_VER_INPUT:-}" ]]; then - PC_VER="$PC_VER_INPUT" - echo "Using manual PurchaseConnector version override: $PC_VER" - else - PC_VER=$(curl -s "https://api.github.com/repos/AppsFlyerSDK/appsflyer-apple-purchase-connector/releases/latest" | jq -r '.tag_name') - if [[ -n "$PC_VER" && "$PC_VER" != "null" ]]; then - echo "Auto-fetched PurchaseConnector version: $PC_VER" - fi - fi - if [[ -z "$PC_VER" || "$PC_VER" == "null" ]]; then - echo "::error::Could not fetch latest PurchaseConnector version and no pc_version input provided." - exit 1 - fi - sed -i.bak "s/'PurchaseConnector', '[^']*'/'PurchaseConnector', '${PC_VER}'/" react-native-appsflyer.podspec - fi - - rm -f react-native-appsflyer.podspec.bak - echo "Updated podspec lines:" - grep -n "AppsFlyerFramework\|PurchaseConnector" react-native-appsflyer.podspec || true - - - name: Update plugin version constants - env: - VERSION: ${{ needs.validate-release.outputs.version }} - run: | - echo "Updating PLUGIN_VERSION constants to: $VERSION" - - # Android - RNAppsFlyerConstants.java - sed -i.bak "s/PLUGIN_VERSION = \"[^\"]*\"/PLUGIN_VERSION = \"${VERSION}\"/" android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java - rm -f android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java.bak - echo "Android:" && grep "PLUGIN_VERSION" android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java - - # iOS - RNAppsFlyer.h - sed -i.bak "s/kAppsFlyerPluginVersion[[:space:]]*= @\"[^\"]*\"/kAppsFlyerPluginVersion = @\"${VERSION}\"/" ios/RNAppsFlyer.h - rm -f ios/RNAppsFlyer.h.bak - echo "iOS:" && grep "kAppsFlyerPluginVersion" ios/RNAppsFlyer.h - - - name: Update README SDK version badges - env: - IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} - AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} - run: | - sed -i.bak -E "s/Android AppsFlyer SDK \*\*v[0-9.]+\*\*/Android AppsFlyer SDK **v${AND_VER}**/" README.md - sed -i.bak -E "s/iOS AppsFlyer SDK \*\*v[0-9.]+\*\*/iOS AppsFlyer SDK **v${IOS_VER}**/" README.md - rm -f README.md.bak - grep -n "AppsFlyer SDK \*\*v" README.md - - - name: Update CHANGELOG.md - env: - VERSION: ${{ needs.validate-release.outputs.version }} - IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} - AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} - run: | - CHANGELOG_ENTRY="## ${VERSION}\n Release date: *$(date +%Y-%m-%d)*\n\n### Changes\n- Android SDK ${AND_VER}\n- iOS SDK ${IOS_VER}\n- TODO: Add specific changes before merging\n" - printf '%b\n' "$CHANGELOG_ENTRY" | cat - CHANGELOG.md > CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.md - head -10 CHANGELOG.md - - - name: Commit & push changes - id: push - env: - VERSION: ${{ needs.validate-release.outputs.version }} - IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} - AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} - run: | - set -e - REL_BRANCH='${{ needs.validate-release.outputs.release_branch }}' - git config user.email "github-actions[bot]@users.noreply.github.com" - git config user.name "github-actions[bot]" - if [[ -n $(git status -s) ]]; then - git add -f package.json react-native-appsflyer.podspec README.md CHANGELOG.md \ - ios/RNAppsFlyer.h \ - android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java \ - android/build.gradle - git commit -m "chore: prepare RC ${VERSION} (iOS ${IOS_VER}, Android ${AND_VER})" - git push --set-upstream origin "$REL_BRANCH" - else - echo "No changes to commit" - if ! git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then - git push --set-upstream origin "$REL_BRANCH" - fi - fi - echo "release_branch=$REL_BRANCH" >> $GITHUB_OUTPUT - - # =========================================================================== - # Stage RC-E2E: iOS - # =========================================================================== - - run-e2e-ios: - name: RC-E2E iOS - needs: [validate-release, prepare-branch] - if: ${{ needs.validate-release.outputs.is_valid == 'true' && github.event.inputs.skip_e2e != 'true' }} - uses: ./.github/workflows/ios-e2e.yml - with: - ref: ${{ needs.prepare-branch.outputs.release_branch }} - secrets: inherit - - # =========================================================================== - # Stage RC-E2E: Android - # =========================================================================== - - run-e2e-android: - name: RC-E2E Android - needs: [validate-release, prepare-branch] - if: ${{ needs.validate-release.outputs.is_valid == 'true' && github.event.inputs.skip_e2e != 'true' }} - uses: ./.github/workflows/android-e2e.yml - with: - ref: ${{ needs.prepare-branch.outputs.release_branch }} - secrets: inherit - - # =========================================================================== - # Pre-publish gate: aggregate run-ci + RC-E2E iOS + RC-E2E Android - # =========================================================================== - - pre-publish-gate: - name: Pre-publish Gate - runs-on: ubuntu-latest - needs: [validate-release, run-ci, run-e2e-ios, run-e2e-android] - if: always() && needs.validate-release.outputs.is_valid == 'true' - outputs: - passed: ${{ steps.aggregate.outputs.passed }} - ci_result: ${{ steps.aggregate.outputs.ci_result }} - e2e_ios_result: ${{ steps.aggregate.outputs.e2e_ios_result }} - e2e_android_result: ${{ steps.aggregate.outputs.e2e_android_result }} - steps: - - name: Aggregate pre-publish results - id: aggregate - env: - CI_RESULT: ${{ needs.run-ci.result }} - E2E_IOS_RESULT: ${{ needs.run-e2e-ios.result }} - E2E_ANDROID_RESULT: ${{ needs.run-e2e-android.result }} - run: | - set -euo pipefail - echo "Lint, Test & Build : $CI_RESULT" - echo "RC-E2E iOS : $E2E_IOS_RESULT" - echo "RC-E2E Android : $E2E_ANDROID_RESULT" - - ci_ok=false - ios_ok=false - android_ok=false - - if [[ "$CI_RESULT" == "success" || "$CI_RESULT" == "skipped" ]]; then - ci_ok=true - fi - if [[ "$E2E_IOS_RESULT" == "success" || "$E2E_IOS_RESULT" == "skipped" ]]; then - ios_ok=true - fi - if [[ "$E2E_ANDROID_RESULT" == "success" || "$E2E_ANDROID_RESULT" == "skipped" ]]; then - android_ok=true - fi - - { - echo "ci_result=$CI_RESULT" - echo "e2e_ios_result=$E2E_IOS_RESULT" - echo "e2e_android_result=$E2E_ANDROID_RESULT" - } >> "$GITHUB_OUTPUT" - - if $ci_ok && $ios_ok && $android_ok; then - echo "passed=true" >> "$GITHUB_OUTPUT" - echo "Pre-publish gate passed" - exit 0 - fi - - echo "passed=false" >> "$GITHUB_OUTPUT" - echo "Pre-publish gate failed" - $ci_ok || echo " - Lint, Test & Build did not pass ($CI_RESULT)" - $ios_ok || echo " - RC-E2E iOS did not pass ($E2E_IOS_RESULT)" - $android_ok || echo " - RC-E2E Android did not pass ($E2E_ANDROID_RESULT)" - exit 1 - - # =========================================================================== - # Validate Jira fix version (gates publish-rc) - # =========================================================================== - - validate-jira: - name: Validate Jira Fix Version - runs-on: ubuntu-latest - needs: [validate-release, pre-publish-gate] - if: always() && needs.pre-publish-gate.result == 'success' && needs.pre-publish-gate.outputs.passed == 'true' - steps: - - name: Verify Jira fix version exists - env: - CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} - CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} - CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} - BASE_VERSION: ${{ needs.validate-release.outputs.base_version }} - run: | - set -euo pipefail - JIRA_FIX_VERSION="React Native SDK v${BASE_VERSION}" - JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" - - echo "Looking for Jira fix version: $JIRA_FIX_VERSION" - - if [[ -z "${CI_JIRA_EMAIL:-}" || -z "${CI_JIRA_TOKEN:-}" ]]; then - echo "::warning::Jira credentials not configured, skipping validation" - exit 0 - fi - - RESPONSE=$(curl -s -u "${CI_JIRA_EMAIL}:${CI_JIRA_TOKEN}" \ - "https://${JIRA_DOMAIN}/rest/api/2/project/SDKRC/versions" \ - | jq -r ".[] | select(.name == \"$JIRA_FIX_VERSION\") | .name") - - if [[ -z "$RESPONSE" ]]; then - echo "::error::Jira fix version '$JIRA_FIX_VERSION' not found. Create it before publishing." - exit 1 - fi - echo "Jira fix version found: $RESPONSE" - - # =========================================================================== - # Publish RC to npm - # =========================================================================== - - publish-rc: - name: Publish RC to npm - runs-on: ubuntu-latest - needs: [validate-release, prepare-branch, pre-publish-gate, validate-jira] - if: always() && needs.pre-publish-gate.result == 'success' && needs.pre-publish-gate.outputs.passed == 'true' && (needs.validate-jira.result == 'success' || needs.validate-jira.result == 'skipped') - permissions: - contents: read - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.prepare-branch.outputs.release_branch }} - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: '24' - - - name: Install dependencies - run: npm install - - - name: Validate package (dry-run) - run: npm pack --dry-run - - - name: RC dry-run active -- skipping publish - if: ${{ needs.validate-release.outputs.is_dry_run == 'true' }} - run: | - echo "RC dry_run is true -- will not publish to npm." - - - name: Publish RC to npm - if: ${{ needs.validate-release.outputs.is_dry_run != 'true' }} - run: npm publish --tag rc - - # =========================================================================== - # Create Pre-Release Tag - # =========================================================================== - - create-prerelease: - name: Create Pre-Release - runs-on: ubuntu-latest - needs: [validate-release, prepare-branch, publish-rc] - if: always() && needs.validate-release.outputs.is_rc == 'true' && needs.publish-rc.result == 'success' - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.prepare-branch.outputs.release_branch }} - - - name: Generate release notes - id: release-notes - env: - VERSION: ${{ needs.validate-release.outputs.version }} - IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} - AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} - run: | - cat > release_notes.md << EOF - # AppsFlyer React Native Plugin - Release Candidate $VERSION - - ## Release Candidate for Testing - - This is a pre-release version for QA testing. Do not use in production. - - ## Testing Instructions - - \`\`\`bash - npm install react-native-appsflyer@${VERSION} --save - \`\`\` - - ## SDK Versions - - - Android AppsFlyer SDK: ${AND_VER} - - iOS AppsFlyer SDK: ${IOS_VER} - - --- - - **Note**: This is a pre-release and should not be used in production applications. - EOF - - - name: Create GitHub Pre-Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.validate-release.outputs.version }} - name: Release Candidate ${{ needs.validate-release.outputs.version }} - body_path: release_notes.md - draft: false - prerelease: true - generate_release_notes: false - token: ${{ secrets.GITHUB_TOKEN }} - - # =========================================================================== - # Open PR to master - # =========================================================================== - - open-pr: - name: Open PR to master - runs-on: ubuntu-latest - needs: [validate-release, prepare-branch, publish-rc] - if: always() && needs.publish-rc.result == 'success' - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.prepare-branch.outputs.release_branch }} - - - name: Create or update PR - uses: actions/github-script@v7 - with: - script: | - const version = '${{ needs.validate-release.outputs.base_version }}'; - const rcVersion = '${{ needs.validate-release.outputs.version }}'; - const head = '${{ needs.prepare-branch.outputs.release_branch }}'; - const base = 'master'; - const androidVersion = '${{ needs.validate-release.outputs.android_sdk_version }}'; - const iosVersion = '${{ needs.validate-release.outputs.ios_sdk_version }}'; - - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${head}` - }); - - const body = [ - `### Release ${version}`, - '', - `- Android SDK: ${androidVersion}`, - `- iOS SDK: ${iosVersion}`, - '', - '```bash', - `npm install react-native-appsflyer@${rcVersion} --save`, - '```', - '', - 'This PR was opened by the RC workflow.' - ].join('\n'); - - if (prs.length > 0) { - const pr = prs[0]; - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - title: `Release ${version}`, - body - }); - core.setOutput('pr_number', pr.number); - } else { - const { data: pr } = await github.rest.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - head, - base, - title: `Release ${version}`, - body, - maintainer_can_modify: true - }); - core.setOutput('pr_number', pr.number); - } - - # =========================================================================== - # Notify Team - # =========================================================================== - - notify-team: - name: Notify Team - runs-on: ubuntu-latest - needs: - - validate-release - - prepare-branch - - run-ci - - run-e2e-ios - - run-e2e-android - - pre-publish-gate - - validate-jira - - publish-rc - - create-prerelease - if: always() - - steps: - - name: Determine status and failed stage - id: status - env: - VALIDATE_RESULT: ${{ needs.validate-release.result }} - PREPARE_RESULT: ${{ needs.prepare-branch.result }} - GATE_RESULT: ${{ needs.pre-publish-gate.result }} - JIRA_RESULT: ${{ needs.validate-jira.result }} - PUBLISH_RESULT: ${{ needs.publish-rc.result }} - PRERELEASE_RESULT: ${{ needs.create-prerelease.result }} - run: | - set -euo pipefail - ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; } - - if [[ "$VALIDATE_RESULT" == "success" \ - && "$PREPARE_RESULT" == "success" \ - && "$GATE_RESULT" == "success" ]] \ - && ok "$JIRA_RESULT" \ - && ok "$PUBLISH_RESULT" \ - && ok "$PRERELEASE_RESULT"; then - echo "success=true" >> "$GITHUB_OUTPUT" - echo "failed_stage=" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "success=false" >> "$GITHUB_OUTPUT" - if [[ "$VALIDATE_RESULT" != "success" ]]; then - echo "failed_stage=validate-release" >> "$GITHUB_OUTPUT" - elif [[ "$PREPARE_RESULT" != "success" ]]; then - echo "failed_stage=prepare-branch" >> "$GITHUB_OUTPUT" - elif [[ "$GATE_RESULT" != "success" ]]; then - echo "failed_stage=pre-publish-gate" >> "$GITHUB_OUTPUT" - elif ! ok "$JIRA_RESULT"; then - echo "failed_stage=validate-jira" >> "$GITHUB_OUTPUT" - elif ! ok "$PUBLISH_RESULT"; then - echo "failed_stage=publish-rc" >> "$GITHUB_OUTPUT" - else - echo "failed_stage=create-prerelease" >> "$GITHUB_OUTPUT" - fi - - - name: Format pre-publish leg results - id: legs - env: - CI_RESULT: ${{ needs.run-ci.result }} - E2E_IOS_RESULT: ${{ needs.run-e2e-ios.result }} - E2E_ANDROID_RESULT: ${{ needs.run-e2e-android.result }} - run: | - set -euo pipefail - icon_for() { - local kind="$1" result="$2" - case "$result" in - success) echo ":white_check_mark:" ;; - skipped) - if [[ "$kind" == "ci" ]]; then echo ":fast_forward:"; else echo ":x:"; fi ;; - failure) echo ":x:" ;; - cancelled) echo ":no_entry_sign:" ;; - *) echo ":grey_question:" ;; - esac - } - CI_ICON=$(icon_for ci "$CI_RESULT") - IOS_ICON=$(icon_for e2e "$E2E_IOS_RESULT") - ANDROID_ICON=$(icon_for e2e "$E2E_ANDROID_RESULT") - { - echo "ci_icon=$CI_ICON" - echo "ios_icon=$IOS_ICON" - echo "android_icon=$ANDROID_ICON" - echo "ci_result=$CI_RESULT" - echo "e2e_ios_result=$E2E_IOS_RESULT" - echo "e2e_android_result=$E2E_ANDROID_RESULT" - } >> "$GITHUB_OUTPUT" - - - name: Checkout release branch - if: steps.status.outputs.success == 'true' - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ needs.prepare-branch.outputs.release_branch }} - - - name: Extract SDK versions and changelog - id: extract-info - if: steps.status.outputs.success == 'true' - run: | - VERSION="${{ needs.validate-release.outputs.version }}" - BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') - - echo "android_sdk=${{ needs.validate-release.outputs.android_sdk_version }}" >> $GITHUB_OUTPUT - echo "ios_sdk=${{ needs.validate-release.outputs.ios_sdk_version }}" >> $GITHUB_OUTPUT - - # Extract changelog for this version - if [ -f "CHANGELOG.md" ]; then - CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- //' | head -5) - if [ -z "$CHANGELOG" ]; then - CHANGELOG="Check CHANGELOG.md for details" - fi - else - CHANGELOG="Check release notes for details" - fi - - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Fetch Jira tickets - id: jira-tickets - if: steps.status.outputs.success == 'true' - continue-on-error: true - env: - CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} - CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} - CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} - run: | - set +e - VERSION="${{ needs.validate-release.outputs.version }}" - BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') - JIRA_FIX_VERSION="React Native SDK v$BASE_VERSION" - JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" - - echo "Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" - - if [[ -z "${CI_JIRA_EMAIL:-}" ]] || [[ -z "${CI_JIRA_TOKEN:-}" ]]; then - echo "Jira credentials not configured" - echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT - exit 0 - fi - - JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" - ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) - - RESPONSE=$(curl -s -w "\n%{http_code}" \ - -u "${CI_JIRA_EMAIL}:${CI_JIRA_TOKEN}" \ - -H "Accept: application/json" \ - -H "Content-Type: application/json" \ - "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") - - HTTP_CODE=$(echo "$RESPONSE" | tail -n1) - BODY=$(echo "$RESPONSE" | sed '$d') - - if [[ "$HTTP_CODE" != "200" ]]; then - echo "Jira API request failed with status $HTTP_CODE" - echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT - exit 0 - fi - - TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) - - if [ -z "$TICKETS" ]; then - echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT - else - echo "tickets<> $GITHUB_OUTPUT - echo "$TICKETS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - fi - - - name: Send Slack notification (Success) - if: steps.status.outputs.success == 'true' - uses: slackapi/slack-github-action@v1 - with: - payload: | - { - "text": "\n:react::react::react::react::react::react::react::react::react::react::react::react:\n\n${{ needs.validate-release.outputs.is_dry_run == 'true' && ':test_tube: *[DRY RUN]* ' || '' }}*React Native Release Candidate:*\nreact-native-appsflyer: ${{ needs.validate-release.outputs.version }} ${{ needs.validate-release.outputs.is_dry_run == 'true' && 'pipeline completed (not published to npm)' || 'is ready for QA testing' }}.\n\n*Pre-publish checks:*\n${{ steps.legs.outputs.ci_icon }} Lint, Test & Build: ${{ steps.legs.outputs.ci_result }}\n${{ steps.legs.outputs.ios_icon }} RC-E2E iOS: ${{ steps.legs.outputs.e2e_ios_result }}\n${{ steps.legs.outputs.android_icon }} RC-E2E Android: ${{ steps.legs.outputs.e2e_android_result }}\n${{ needs.validate-release.outputs.is_dry_run != 'true' && format('\n*Testing Instructions:*\n```\nnpm install react-native-appsflyer@{0} --save\n```', needs.validate-release.outputs.version) || '' }}\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/${{ github.ref_name }}\n:github: Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n${{ needs.validate-release.outputs.is_dry_run != 'true' && format(':github: Release: https://github.com/{0}/releases/tag/{1}', github.repository, needs.validate-release.outputs.version) || '' }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDKs:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n:react::react::react::react::react::react::react::react::react::react::react::react:" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} - - - name: Send failure notification - if: steps.status.outputs.success == 'false' - uses: slackapi/slack-github-action@v1 - with: - payload: | - { - "text": "\n:warning: *${{ needs.validate-release.outputs.is_dry_run == 'true' && '[DRY RUN] ' || '' }}React Native RC failed at `${{ steps.status.outputs.failed_stage }}`*\n\nVersion: ${{ needs.validate-release.outputs.version }}\nBranch: ${{ github.ref_name }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n*Pre-publish checks:*\n${{ steps.legs.outputs.ci_icon }} Lint, Test & Build: ${{ steps.legs.outputs.ci_result }}\n${{ steps.legs.outputs.ios_icon }} RC-E2E iOS: ${{ steps.legs.outputs.e2e_ios_result }}\n${{ steps.legs.outputs.android_icon }} RC-E2E Android: ${{ steps.legs.outputs.e2e_android_result }}\n\n*Downstream stages:*\n- prepare-branch: ${{ needs.prepare-branch.result }}\n- pre-publish-gate: ${{ needs.pre-publish-gate.result }}\n- validate-jira: ${{ needs.validate-jira.result }}\n- publish-rc: ${{ needs.publish-rc.result }}\n- create-prerelease: ${{ needs.create-prerelease.result }}" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} - - # =========================================================================== - # RC Summary - # =========================================================================== - - rc-summary: - name: RC Summary - runs-on: ubuntu-latest - needs: [validate-release, run-ci, prepare-branch, run-e2e-ios, run-e2e-android, pre-publish-gate, validate-jira, publish-rc, create-prerelease] - if: always() - - steps: - - name: Display RC Summary - run: | - echo "=========================================" - echo "RC Release Summary" - echo "=========================================" - echo "Version: ${{ needs.validate-release.outputs.version }}" - echo "Dry Run: ${{ needs.validate-release.outputs.is_dry_run }}" - echo "-----------------------------------------" - echo "RC-PREP validate: ${{ needs.validate-release.result }}" - echo "Lint, Test & Build: ${{ needs.run-ci.result }}" - echo "RC-PREP branch: ${{ needs.prepare-branch.result }}" - echo "RC-E2E iOS: ${{ needs.run-e2e-ios.result }}" - echo "RC-E2E Android: ${{ needs.run-e2e-android.result }}" - echo "Pre-publish gate: ${{ needs.pre-publish-gate.result }}" - echo "Validate Jira: ${{ needs.validate-jira.result }}" - echo "RC-PUBLISH: ${{ needs.publish-rc.result }}" - echo "Pre-release tag + PR: ${{ needs.create-prerelease.result }}" - echo "=========================================" - - ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; } - - FAIL=0 - # validate-release and pre-publish-gate must succeed (never legitimately skipped) - [[ "${{ needs.validate-release.result }}" == "success" ]] || FAIL=1 - [[ "${{ needs.pre-publish-gate.result }}" == "success" ]] || FAIL=1 - # downstream jobs may be skipped (dry-run, skip_e2e, etc.) - ok "${{ needs.run-ci.result }}" || FAIL=1 - ok "${{ needs.prepare-branch.result }}" || FAIL=1 - ok "${{ needs.validate-jira.result }}" || FAIL=1 - ok "${{ needs.publish-rc.result }}" || FAIL=1 - ok "${{ needs.create-prerelease.result }}" || FAIL=1 - - if [[ "$FAIL" == "0" ]]; then - echo "RC Release Process Completed Successfully" - else - echo "RC Release Process Failed" - echo "Check the logs above for details" - exit 1 - fi diff --git a/.github/workflows/rc-smoke.yml b/.github/workflows/rc-smoke.yml index c0bd9806..9d24832d 100644 --- a/.github/workflows/rc-smoke.yml +++ b/.github/workflows/rc-smoke.yml @@ -4,7 +4,7 @@ # # Stage: RC-SMOKE in the RC pipeline. # -# Fires automatically after rc-release.yml ("RC - Release Candidate") completes +# Fires automatically after release.yml ("Release" RC path) completes # with conclusion: success. Synthesizes example_rc_smoke/ from the demo app # with react-native-appsflyer pinned to the RC version from npm, runs # SMOKE-001/002/003 via af-scenario-runner.sh on both platforms, and posts a @@ -22,7 +22,7 @@ name: RC Smoke - npm artifact on: workflow_run: - workflows: ["RC - Release Candidate"] + workflows: ["Release"] types: [completed] workflow_dispatch: inputs: @@ -380,9 +380,8 @@ jobs: set -e adb shell 'getprop net.dns1; getprop net.dns2' || true adb shell 'nslookup oyoxfj.conversions.appsflyersdk.com 2>&1 | head -5' || { sleep 10; adb shell 'nslookup oyoxfj.conversions.appsflyersdk.com 2>&1 | head -5'; } || { echo "::error::Emulator DNS cannot resolve AppsFlyer hosts"; exit 1; } - cd example_rc_smoke/android && cp ../../example/android/gradlew . && cd ../.. - cd example_rc_smoke && npx react-native build-android --mode=debug && cd .. - ./scripts/af-scenario-runner.sh --platform android --plan .af-smoke/rc-test-plan.json + cd example_rc_smoke/android && cp ../../example/android/gradlew . && ./gradlew assembleDebug && cd ../.. + bash ./scripts/af-scenario-runner.sh --platform android --plan .af-smoke/rc-test-plan.json - name: Upload smoke reports if: always() @@ -401,6 +400,8 @@ jobs: needs: [resolve, smoke-ios, smoke-android] if: always() && needs.resolve.outputs.should_run == 'true' && needs.resolve.outputs.head_sha != '' runs-on: ubuntu-latest + permissions: + checks: write steps: - name: Post check-run uses: actions/github-script@v7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..72c6fcda --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,1418 @@ +# ============================================================================= +# Unified Release Workflow - RC and Production +# ============================================================================= +# +# Purpose: Single workflow for both RC and production npm releases. +# npm OIDC Trusted Publishing allows only one workflow file per package. +# +# RC path (workflow_dispatch with release_type=rc): +# 1. Validates RC version format and inputs +# 2. Runs full CI pipeline (Jest + ESLint + release builds) +# 3. Creates release branch with version bumps across 5+ files +# 4. Runs iOS + Android E2E tests on the release branch +# 5. Validates Jira fix version exists +# 6. Publishes to npm with --tag rc +# 7. Creates GitHub pre-release + PR to master +# 8. Notifies team via Slack +# +# Production path (PR merge to master OR workflow_dispatch with release_type=production): +# 1. Validates the merge is from a release branch +# 2. Publishes to npm with the `latest` tag +# 3. Verifies the package is live on npm (retry loop, max 120s) +# 4. Creates a GitHub release with release notes +# 5. Fetches Jira tickets for the fix version +# 6. Notifies team via Slack +# +# ============================================================================= + +name: Release + +on: + pull_request: + types: + - closed + branches: + - master + + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + type: choice + options: + - rc + - production + rn_version: + description: 'Plugin version (e.g., 6.18.0-rc1 for RC, 6.18.0 for production)' + required: true + type: string + ios_sdk_version: + description: 'iOS native AppsFlyer SDK version (e.g., 6.18.0) — RC only' + required: false + type: string + android_sdk_version: + description: 'Android native AppsFlyer SDK version (e.g., 6.18.0) — RC only' + required: false + type: string + base_branch: + description: 'Base branch to create the release branch from — RC only' + required: false + default: development + type: string + pc_version: + description: 'PurchaseConnector iOS version override (leave empty to auto-fetch) — RC only' + required: false + default: '' + type: string + skip_unit: + description: 'Skip unit tests and linting — RC only' + required: false + type: boolean + default: false + skip_builds: + description: 'Skip Android + iOS release builds — RC only' + required: false + type: boolean + default: false + skip_e2e: + description: 'Skip E2E iOS + Android jobs — RC only' + required: false + type: boolean + default: false + dry_run: + description: 'Do not publish to npm' + required: false + type: boolean + default: true + +concurrency: + group: release-${{ github.workflow }}-${{ inputs.release_type || 'production' }} + cancel-in-progress: false + +jobs: + # =========================================================================== + # Job 0: Route to RC or Production path + # =========================================================================== + + determine-release-type: + name: Determine Release Type + runs-on: ubuntu-latest + outputs: + release_type: ${{ steps.route.outputs.release_type }} + is_valid_trigger: ${{ steps.route.outputs.is_valid_trigger }} + steps: + - name: Route release + id: route + env: + EVENT_NAME: ${{ github.event_name }} + PR_MERGED: ${{ github.event.pull_request.merged }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + INPUT_RELEASE_TYPE: ${{ inputs.release_type }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "pull_request" ]]; then + if [[ "$PR_MERGED" == "true" ]] && [[ "$PR_HEAD_REF" == releases/* ]]; then + echo "release_type=production" >> "$GITHUB_OUTPUT" + echo "is_valid_trigger=true" >> "$GITHUB_OUTPUT" + echo "PR merged from release branch — production path" + else + echo "release_type=none" >> "$GITHUB_OUTPUT" + echo "is_valid_trigger=false" >> "$GITHUB_OUTPUT" + echo "PR not merged or not from release branch — skipping" + fi + else + echo "release_type=$INPUT_RELEASE_TYPE" >> "$GITHUB_OUTPUT" + echo "is_valid_trigger=true" >> "$GITHUB_OUTPUT" + echo "Manual dispatch — $INPUT_RELEASE_TYPE path" + fi + + # =========================================================================== + # RC Job 1: Validate RC Inputs & Compute Branch + # =========================================================================== + + validate-rc: + name: Validate RC Inputs & Compute Branch + runs-on: ubuntu-latest + needs: [determine-release-type] + if: needs.determine-release-type.outputs.release_type == 'rc' + + outputs: + version: ${{ steps.compute.outputs.version }} + base_version: ${{ steps.compute.outputs.base_version }} + is_rc: ${{ steps.compute.outputs.is_rc }} + is_valid: ${{ steps.compute.outputs.is_valid }} + base_branch: ${{ steps.compute.outputs.base_branch }} + release_branch: ${{ steps.compute.outputs.release_branch }} + ios_sdk_version: ${{ steps.compute.outputs.ios_sdk_version }} + android_sdk_version: ${{ steps.compute.outputs.android_sdk_version }} + is_dry_run: ${{ steps.dry-run.outputs.value }} + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Resolve dry_run + id: dry-run + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_DRY: ${{ github.event.inputs.dry_run }} + run: | + set -euo pipefail + case "$EVENT_NAME" in + workflow_dispatch) + if [[ "$DISPATCH_DRY" == "true" ]]; then + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + ;; + *) + echo "value=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Validate and compute + id: compute + env: + VERSION: ${{ github.event.inputs.rn_version }} + IOS_VER: ${{ github.event.inputs.ios_sdk_version }} + AND_VER: ${{ github.event.inputs.android_sdk_version }} + BASE_BRANCH_INPUT: ${{ github.event.inputs.base_branch }} + run: | + set -euo pipefail + + if [[ -z "$VERSION" || -z "$IOS_VER" || -z "$AND_VER" ]]; then + echo "Missing required inputs"; exit 1 + fi + + # RN version format: X.Y.Z-rcN (no +build) + if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+$ ]]; then + echo "rn_version must be X.Y.Z-rcN (e.g., 6.18.0-rc1)"; exit 1 + fi + if [[ ! $IOS_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ios_sdk_version must be X.Y.Z"; exit 1 + fi + if [[ ! $AND_VER =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "android_sdk_version must be X.Y.Z"; exit 1 + fi + + # Compute base version (remove -rcN) + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + + MAJOR_MINOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+\.[0-9]+') + MAJOR=$(echo "$BASE_VERSION" | grep -oE '^[0-9]+') + RELEASE_BRANCH="releases/${MAJOR}.x.x/${MAJOR_MINOR}.x/${VERSION}" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "base_version=$BASE_VERSION" >> $GITHUB_OUTPUT + echo "is_rc=true" >> $GITHUB_OUTPUT + echo "is_valid=true" >> $GITHUB_OUTPUT + echo "base_branch=$BASE_BRANCH_INPUT" >> $GITHUB_OUTPUT + echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT + echo "ios_sdk_version=$IOS_VER" >> $GITHUB_OUTPUT + echo "android_sdk_version=$AND_VER" >> $GITHUB_OUTPUT + + # =========================================================================== + # RC Job 2: Lint, Test & Build (reusable) + # =========================================================================== + + run-ci: + name: Lint, Test & Build + needs: [determine-release-type, validate-rc] + if: >- + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_valid == 'true' + uses: ./.github/workflows/lint-test-build.yml + with: + skip_unit: ${{ github.event.inputs.skip_unit == 'true' }} + skip_builds: ${{ github.event.inputs.skip_builds == 'true' }} + secrets: inherit + + # =========================================================================== + # RC Job 3: Create/Update Release Branch and Apply Changes + # =========================================================================== + + prepare-branch: + name: Create Release Branch & Apply Changes + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_valid == 'true' + outputs: + release_branch: ${{ steps.push.outputs.release_branch }} + steps: + - name: Checkout base branch + uses: actions/checkout@v5 + with: + ref: ${{ needs.validate-rc.outputs.base_branch }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Create release branch + id: branch + run: | + set -e + REL_BRANCH="${{ needs.validate-rc.outputs.release_branch }}" + echo "Target release branch: $REL_BRANCH" + if git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then + echo "Branch already exists on remote. Checking it out." + git fetch origin "$REL_BRANCH":"$REL_BRANCH" + git checkout "$REL_BRANCH" + else + git checkout -b "$REL_BRANCH" + fi + + - name: Update package.json version + env: + VERSION: ${{ needs.validate-rc.outputs.version }} + run: | + echo "Setting package.json version to $VERSION" + npm version "$VERSION" --no-git-tag-version + grep '"version"' package.json + + - name: Update Android SDK fallback in build.gradle + env: + AND_VER: ${{ needs.validate-rc.outputs.android_sdk_version }} + run: | + sed -i.bak "s/af-android-sdk:\${safeExtGet('appsflyerVersion', '[^']*')}/af-android-sdk:\${safeExtGet('appsflyerVersion', '${AND_VER}')}/" android/build.gradle + rm -f android/build.gradle.bak + grep "af-android-sdk:" android/build.gradle + + - name: Update iOS SDK deps in podspec + env: + IOS_VER: ${{ needs.validate-rc.outputs.ios_sdk_version }} + PC_VER_INPUT: ${{ github.event.inputs.pc_version }} + run: | + # AppsFlyerFramework (default) + sed -i.bak "s/'AppsFlyerFramework', '[^']*'/'AppsFlyerFramework', '${IOS_VER}'/" react-native-appsflyer.podspec + # AppsFlyerFramework/Strict + sed -i.bak "s|'AppsFlyerFramework/Strict', '[^']*'|'AppsFlyerFramework/Strict', '${IOS_VER}'|" react-native-appsflyer.podspec + + # PurchaseConnector (conditional dep, has its own version) + if grep -q "PurchaseConnector" react-native-appsflyer.podspec; then + if [[ -n "${PC_VER_INPUT:-}" ]]; then + PC_VER="$PC_VER_INPUT" + echo "Using manual PurchaseConnector version override: $PC_VER" + else + PC_VER=$(curl -s "https://api.github.com/repos/AppsFlyerSDK/appsflyer-apple-purchase-connector/releases/latest" | jq -r '.tag_name') + if [[ -n "$PC_VER" && "$PC_VER" != "null" ]]; then + echo "Auto-fetched PurchaseConnector version: $PC_VER" + fi + fi + if [[ -z "$PC_VER" || "$PC_VER" == "null" ]]; then + echo "::error::Could not fetch latest PurchaseConnector version and no pc_version input provided." + exit 1 + fi + sed -i.bak "s/'PurchaseConnector', '[^']*'/'PurchaseConnector', '${PC_VER}'/" react-native-appsflyer.podspec + fi + + rm -f react-native-appsflyer.podspec.bak + echo "Updated podspec lines:" + grep -n "AppsFlyerFramework\|PurchaseConnector" react-native-appsflyer.podspec || true + + - name: Update plugin version constants + env: + VERSION: ${{ needs.validate-rc.outputs.version }} + run: | + echo "Updating PLUGIN_VERSION constants to: $VERSION" + + # Android - RNAppsFlyerConstants.java + sed -i.bak "s/PLUGIN_VERSION = \"[^\"]*\"/PLUGIN_VERSION = \"${VERSION}\"/" android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java + rm -f android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java.bak + echo "Android:" && grep "PLUGIN_VERSION" android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java + + # iOS - RNAppsFlyer.h + sed -i.bak "s/kAppsFlyerPluginVersion[[:space:]]*= @\"[^\"]*\"/kAppsFlyerPluginVersion = @\"${VERSION}\"/" ios/RNAppsFlyer.h + rm -f ios/RNAppsFlyer.h.bak + echo "iOS:" && grep "kAppsFlyerPluginVersion" ios/RNAppsFlyer.h + + - name: Update README SDK version badges + env: + IOS_VER: ${{ needs.validate-rc.outputs.ios_sdk_version }} + AND_VER: ${{ needs.validate-rc.outputs.android_sdk_version }} + run: | + sed -i.bak -E "s/Android AppsFlyer SDK \*\*v[0-9.]+\*\*/Android AppsFlyer SDK **v${AND_VER}**/" README.md + sed -i.bak -E "s/iOS AppsFlyer SDK \*\*v[0-9.]+\*\*/iOS AppsFlyer SDK **v${IOS_VER}**/" README.md + rm -f README.md.bak + grep -n "AppsFlyer SDK \*\*v" README.md + + - name: Update CHANGELOG.md + env: + VERSION: ${{ needs.validate-rc.outputs.version }} + IOS_VER: ${{ needs.validate-rc.outputs.ios_sdk_version }} + AND_VER: ${{ needs.validate-rc.outputs.android_sdk_version }} + run: | + CHANGELOG_ENTRY="## ${VERSION}\n Release date: *$(date +%Y-%m-%d)*\n\n### Changes\n- Android SDK ${AND_VER}\n- iOS SDK ${IOS_VER}\n- TODO: Add specific changes before merging\n" + printf '%b\n' "$CHANGELOG_ENTRY" | cat - CHANGELOG.md > CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.md + head -10 CHANGELOG.md + + - name: Commit & push changes + id: push + env: + VERSION: ${{ needs.validate-rc.outputs.version }} + IOS_VER: ${{ needs.validate-rc.outputs.ios_sdk_version }} + AND_VER: ${{ needs.validate-rc.outputs.android_sdk_version }} + run: | + set -e + REL_BRANCH='${{ needs.validate-rc.outputs.release_branch }}' + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + if [[ -n $(git status -s) ]]; then + git add -f package.json react-native-appsflyer.podspec README.md CHANGELOG.md \ + ios/RNAppsFlyer.h \ + android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java \ + android/build.gradle + git commit -m "chore: prepare RC ${VERSION} (iOS ${IOS_VER}, Android ${AND_VER})" + git push --set-upstream origin "$REL_BRANCH" + else + echo "No changes to commit" + if ! git ls-remote --exit-code --heads origin "$REL_BRANCH" >/dev/null 2>&1; then + git push --set-upstream origin "$REL_BRANCH" + fi + fi + echo "release_branch=$REL_BRANCH" >> $GITHUB_OUTPUT + + # =========================================================================== + # RC Job 4: E2E iOS + # =========================================================================== + + run-e2e-ios: + name: RC-E2E iOS + needs: [determine-release-type, validate-rc, prepare-branch] + if: >- + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_valid == 'true' && + github.event.inputs.skip_e2e != 'true' + uses: ./.github/workflows/ios-e2e.yml + with: + ref: ${{ needs.prepare-branch.outputs.release_branch }} + secrets: inherit + + # =========================================================================== + # RC Job 5: E2E Android + # =========================================================================== + + run-e2e-android: + name: RC-E2E Android + needs: [determine-release-type, validate-rc, prepare-branch] + if: >- + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_valid == 'true' && + github.event.inputs.skip_e2e != 'true' + uses: ./.github/workflows/android-e2e.yml + with: + ref: ${{ needs.prepare-branch.outputs.release_branch }} + secrets: inherit + + # =========================================================================== + # RC Job 6: Pre-publish gate + # =========================================================================== + + pre-publish-gate: + name: Pre-publish Gate + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, run-ci, run-e2e-ios, run-e2e-android] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_valid == 'true' + outputs: + passed: ${{ steps.aggregate.outputs.passed }} + ci_result: ${{ steps.aggregate.outputs.ci_result }} + e2e_ios_result: ${{ steps.aggregate.outputs.e2e_ios_result }} + e2e_android_result: ${{ steps.aggregate.outputs.e2e_android_result }} + steps: + - name: Aggregate pre-publish results + id: aggregate + env: + CI_RESULT: ${{ needs.run-ci.result }} + E2E_IOS_RESULT: ${{ needs.run-e2e-ios.result }} + E2E_ANDROID_RESULT: ${{ needs.run-e2e-android.result }} + run: | + set -euo pipefail + echo "Lint, Test & Build : $CI_RESULT" + echo "RC-E2E iOS : $E2E_IOS_RESULT" + echo "RC-E2E Android : $E2E_ANDROID_RESULT" + + ci_ok=false + ios_ok=false + android_ok=false + + if [[ "$CI_RESULT" == "success" || "$CI_RESULT" == "skipped" ]]; then + ci_ok=true + fi + if [[ "$E2E_IOS_RESULT" == "success" || "$E2E_IOS_RESULT" == "skipped" ]]; then + ios_ok=true + fi + if [[ "$E2E_ANDROID_RESULT" == "success" || "$E2E_ANDROID_RESULT" == "skipped" ]]; then + android_ok=true + fi + + { + echo "ci_result=$CI_RESULT" + echo "e2e_ios_result=$E2E_IOS_RESULT" + echo "e2e_android_result=$E2E_ANDROID_RESULT" + } >> "$GITHUB_OUTPUT" + + if $ci_ok && $ios_ok && $android_ok; then + echo "passed=true" >> "$GITHUB_OUTPUT" + echo "Pre-publish gate passed" + exit 0 + fi + + echo "passed=false" >> "$GITHUB_OUTPUT" + echo "Pre-publish gate failed" + $ci_ok || echo " - Lint, Test & Build did not pass ($CI_RESULT)" + $ios_ok || echo " - RC-E2E iOS did not pass ($E2E_IOS_RESULT)" + $android_ok || echo " - RC-E2E Android did not pass ($E2E_ANDROID_RESULT)" + exit 1 + + # =========================================================================== + # RC Job 7: Validate Jira fix version + # =========================================================================== + + validate-jira: + name: Validate Jira Fix Version + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, pre-publish-gate] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' && + needs.pre-publish-gate.result == 'success' && + needs.pre-publish-gate.outputs.passed == 'true' + steps: + - name: Verify Jira fix version exists + env: + CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} + CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} + CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} + BASE_VERSION: ${{ needs.validate-rc.outputs.base_version }} + run: | + set -euo pipefail + JIRA_FIX_VERSION="React Native SDK v${BASE_VERSION}" + JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" + + echo "Looking for Jira fix version: $JIRA_FIX_VERSION" + + if [[ -z "${CI_JIRA_EMAIL:-}" || -z "${CI_JIRA_TOKEN:-}" ]]; then + echo "::warning::Jira credentials not configured, skipping validation" + exit 0 + fi + + RESPONSE=$(curl -s -u "${CI_JIRA_EMAIL}:${CI_JIRA_TOKEN}" \ + "https://${JIRA_DOMAIN}/rest/api/3/project/SDKRC/versions" \ + | jq -r ".[] | select(.name == \"$JIRA_FIX_VERSION\") | .name") + + if [[ -z "$RESPONSE" ]]; then + echo "::error::Jira fix version '$JIRA_FIX_VERSION' not found. Create it before publishing." + exit 1 + fi + echo "Jira fix version found: $RESPONSE" + + # =========================================================================== + # Production Job 1: Validate Production Release + # =========================================================================== + + validate-production: + name: Validate Production Release + runs-on: ubuntu-latest + needs: [determine-release-type] + if: >- + needs.determine-release-type.outputs.release_type == 'production' && + needs.determine-release-type.outputs.is_valid_trigger == 'true' + + outputs: + version: ${{ steps.get-version.outputs.version }} + is_valid: ${{ steps.validate.outputs.is_valid }} + is_dry_run: ${{ steps.dry-run.outputs.value }} + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Resolve dry_run across trigger paths + id: dry-run + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_DRY: ${{ github.event.inputs.dry_run }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "workflow_dispatch" && "$DISPATCH_DRY" == "true" ]]; then + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + + - name: Validate release source + id: validate + env: + EVENT_NAME: ${{ github.event_name }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + echo "Manual run - skipping branch validation" + echo "is_valid=true" >> "$GITHUB_OUTPUT" + else + SOURCE_BRANCH="$PR_HEAD_REF" + echo "Source branch: $SOURCE_BRANCH" + + if [[ $SOURCE_BRANCH =~ ^releases/ ]]; then + echo "Valid release branch: $SOURCE_BRANCH" + echo "is_valid=true" >> "$GITHUB_OUTPUT" + else + echo "::error::Not a release branch: $SOURCE_BRANCH" + echo "is_valid=false" >> "$GITHUB_OUTPUT" + exit 1 + fi + fi + + - name: Get version from package.json + id: get-version + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ github.event.inputs.rn_version }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + VERSION="$INPUT_VERSION" + echo "Using provided version: $VERSION" + else + VERSION=$(node -p "require('./package.json').version") + echo "Extracted version from package.json: $VERSION" + fi + + # Validate version format (X.Y.Z, no -rc suffix for production) + if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Valid production version format: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + else + echo "::error::Invalid production version format: $VERSION (expected X.Y.Z)" + exit 1 + fi + + - name: Check if tag already exists + env: + VERSION: ${{ steps.get-version.outputs.version }} + DRY_RUN: ${{ steps.dry-run.outputs.value }} + run: | + git fetch --tags + if git rev-parse "v$VERSION" >/dev/null 2>&1 || git rev-parse "$VERSION" >/dev/null 2>&1; then + echo "::warning::Tag for $VERSION already exists" + if [[ "$DRY_RUN" != "true" ]]; then + echo "::error::Cannot create duplicate release" + exit 1 + fi + fi + echo "Tag $VERSION does not exist - safe to proceed" + + # =========================================================================== + # Shared: Publish to npm (RC with --tag rc, Production with latest) + # =========================================================================== + + publish-to-npm: + name: Publish to npm + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, validate-production, prepare-branch, pre-publish-gate, validate-jira] + if: >- + !cancelled() && + ( + (needs.determine-release-type.outputs.release_type == 'rc' && + needs.pre-publish-gate.result == 'success' && + needs.pre-publish-gate.outputs.passed == 'true' && + (needs.validate-jira.result == 'success' || needs.validate-jira.result == 'skipped')) + || + (needs.determine-release-type.outputs.release_type == 'production' && + needs.validate-production.result == 'success' && + needs.validate-production.outputs.is_valid == 'true') + ) + permissions: + contents: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ needs.determine-release-type.outputs.release_type == 'rc' && needs.prepare-branch.outputs.release_branch || '' }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false + + - name: Install dependencies + run: npm install + + - name: Verify version consistency + env: + RELEASE_TYPE: ${{ needs.determine-release-type.outputs.release_type }} + RC_VERSION: ${{ needs.validate-rc.outputs.version }} + PROD_VERSION: ${{ needs.validate-production.outputs.version }} + run: | + ACTUAL=$(node -p "require('./package.json').version") + if [[ "$RELEASE_TYPE" == "rc" ]]; then + EXPECTED="$RC_VERSION" + else + EXPECTED="$PROD_VERSION" + fi + if [[ "$ACTUAL" != "$EXPECTED" ]]; then + echo "::error::Version mismatch: package.json has '$ACTUAL' but expected '$EXPECTED'" + exit 1 + fi + echo "Version consistent: $ACTUAL" + + - name: Validate package (dry-run) + run: npm pack --dry-run + + - name: Determine publish parameters + id: publish-params + env: + RELEASE_TYPE: ${{ needs.determine-release-type.outputs.release_type }} + RC_DRY_RUN: ${{ needs.validate-rc.outputs.is_dry_run }} + PROD_DRY_RUN: ${{ needs.validate-production.outputs.is_dry_run }} + run: | + if [[ "$RELEASE_TYPE" == "rc" ]]; then + echo "npm_tag=rc" >> "$GITHUB_OUTPUT" + echo "is_dry_run=${RC_DRY_RUN:-false}" >> "$GITHUB_OUTPUT" + else + echo "npm_tag=latest" >> "$GITHUB_OUTPUT" + echo "is_dry_run=${PROD_DRY_RUN:-false}" >> "$GITHUB_OUTPUT" + fi + + - name: Dry-run — skipping publish + if: steps.publish-params.outputs.is_dry_run == 'true' + run: | + echo "DRY RUN — would publish $(node -p "require('./package.json').version") to npm" + npm pack --dry-run + + - name: Publish to npm + if: steps.publish-params.outputs.is_dry_run != 'true' + env: + NODE_AUTH_TOKEN: '' + run: | + VERSION=$(node -p "require('./package.json').version") + TAG="${{ steps.publish-params.outputs.npm_tag }}" + echo "Publishing $VERSION to npm with tag: $TAG" + if [[ "$TAG" == "rc" ]]; then + npm publish --tag rc --provenance + else + npm publish --provenance + fi + echo "Published $VERSION to npm" + + - name: Verify publication (production only) + if: >- + needs.determine-release-type.outputs.release_type == 'production' && + steps.publish-params.outputs.is_dry_run != 'true' + env: + VERSION: ${{ needs.validate-production.outputs.version }} + run: | + MAX_WAIT=120 + POLL=15 + ELAPSED=0 + while (( ELAPSED < MAX_WAIT )); do + if npm view "react-native-appsflyer@$VERSION" version 2>/dev/null | grep -Fxq "$VERSION"; then + echo "Verified: $VERSION is live on npm" + break + fi + echo "Waiting for npm propagation (${ELAPSED}s / ${MAX_WAIT}s)..." + sleep "$POLL" + ELAPSED=$(( ELAPSED + POLL )) + done + if (( ELAPSED >= MAX_WAIT )); then + echo "::warning::npm propagation exceeded ${MAX_WAIT}s --- verify manually on npmjs.com" + fi + + # =========================================================================== + # RC Job 8: Create Pre-Release Tag + # =========================================================================== + + create-prerelease: + name: Create Pre-Release + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, prepare-branch, publish-to-npm] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' && + needs.validate-rc.outputs.is_rc == 'true' && + needs.publish-to-npm.result == 'success' + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-branch.outputs.release_branch }} + + - name: Generate release notes + id: release-notes + env: + VERSION: ${{ needs.validate-rc.outputs.version }} + IOS_VER: ${{ needs.validate-rc.outputs.ios_sdk_version }} + AND_VER: ${{ needs.validate-rc.outputs.android_sdk_version }} + run: | + cat > release_notes.md << EOF + # AppsFlyer React Native Plugin - Release Candidate $VERSION + + ## Release Candidate for Testing + + This is a pre-release version for QA testing. Do not use in production. + + ## Testing Instructions + + \`\`\`bash + npm install react-native-appsflyer@${VERSION} --save + \`\`\` + + ## SDK Versions + + - Android AppsFlyer SDK: ${AND_VER} + - iOS AppsFlyer SDK: ${IOS_VER} + + --- + + **Note**: This is a pre-release and should not be used in production applications. + EOF + + - name: Create GitHub Pre-Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.validate-rc.outputs.version }} + name: Release Candidate ${{ needs.validate-rc.outputs.version }} + body_path: release_notes.md + draft: false + prerelease: true + generate_release_notes: false + token: ${{ secrets.GITHUB_TOKEN }} + + # =========================================================================== + # RC Job 9: Open PR to master + # =========================================================================== + + open-pr: + name: Open PR to master + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, prepare-branch, publish-to-npm] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' && + needs.publish-to-npm.result == 'success' + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-branch.outputs.release_branch }} + + - name: Create or update PR + uses: actions/github-script@v7 + with: + script: | + const version = '${{ needs.validate-rc.outputs.base_version }}'; + const rcVersion = '${{ needs.validate-rc.outputs.version }}'; + const head = '${{ needs.prepare-branch.outputs.release_branch }}'; + const base = 'master'; + const androidVersion = '${{ needs.validate-rc.outputs.android_sdk_version }}'; + const iosVersion = '${{ needs.validate-rc.outputs.ios_sdk_version }}'; + + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${head}` + }); + + const body = [ + `### Release ${version}`, + '', + `- Android SDK: ${androidVersion}`, + `- iOS SDK: ${iosVersion}`, + '', + '```bash', + `npm install react-native-appsflyer@${rcVersion} --save`, + '```', + '', + 'This PR was opened by the RC workflow.' + ].join('\n'); + + if (prs.length > 0) { + const pr = prs[0]; + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + title: `Release ${version}`, + body + }); + core.setOutput('pr_number', pr.number); + } else { + const { data: pr } = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + head, + base, + title: `Release ${version}`, + body, + maintainer_can_modify: true + }); + core.setOutput('pr_number', pr.number); + } + + # =========================================================================== + # Production Job 2: Create GitHub Release + # =========================================================================== + + create-github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [determine-release-type, validate-production, publish-to-npm] + if: >- + needs.determine-release-type.outputs.release_type == 'production' && + needs.publish-to-npm.result == 'success' && + needs.validate-production.outputs.is_dry_run != 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Extract release notes from CHANGELOG + id: changelog + env: + VERSION: ${{ needs.validate-production.outputs.version }} + run: | + echo "Extracting release notes for version $VERSION from CHANGELOG.md" + + if [ -f "CHANGELOG.md" ]; then + RELEASE_NOTES=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | sed '1d;$d') + + if [ -z "$RELEASE_NOTES" ]; then + echo "::warning::Could not find release notes for $VERSION in CHANGELOG.md" + RELEASE_NOTES="Release version $VERSION. See [CHANGELOG.md](CHANGELOG.md) for details." + fi + else + RELEASE_NOTES="Release version $VERSION." + fi + + echo "$RELEASE_NOTES" > release_notes.md + + - name: Build release notes + env: + VERSION: ${{ needs.validate-production.outputs.version }} + REPO: ${{ github.repository }} + run: | + cat > final_release_notes.md << EOF + # AppsFlyer React Native Plugin v$VERSION + + ## Installation + + \`\`\`bash + npm install react-native-appsflyer@$VERSION + \`\`\` + + Then for iOS: + \`\`\`bash + cd ios && pod install + \`\`\` + + ## Changes in This Release + + $(cat release_notes.md) + + ## Documentation + + - [Installation Guide](https://github.com/$REPO/blob/master/Docs/Installation.md) + - [API Documentation](https://github.com/$REPO/blob/master/Docs/API.md) + - [Deep Linking Guide](https://github.com/$REPO/blob/master/Docs/DeepLink.md) + - [Expo Integration](https://github.com/$REPO/blob/master/Docs/ExpoIntegration.md) + + ## Links + + - [npm Package](https://www.npmjs.com/package/react-native-appsflyer/v/$VERSION) + - [GitHub Repository](https://github.com/$REPO) + - [AppsFlyer Developer Hub](https://dev.appsflyer.com/) + + ## Support + + For issues and questions, please contact + EOF + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.validate-production.outputs.version }} + run: | + gh release create "v$VERSION" \ + --title "v$VERSION" \ + --notes-file final_release_notes.md \ + --latest + + # =========================================================================== + # RC Notify Team + # =========================================================================== + + notify-team-rc: + name: Notify Team (RC) + runs-on: ubuntu-latest + needs: + - determine-release-type + - validate-rc + - prepare-branch + - run-ci + - run-e2e-ios + - run-e2e-android + - pre-publish-gate + - validate-jira + - publish-to-npm + - create-prerelease + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' + + steps: + - name: Determine status and failed stage + id: status + env: + VALIDATE_RESULT: ${{ needs.validate-rc.result }} + PREPARE_RESULT: ${{ needs.prepare-branch.result }} + GATE_RESULT: ${{ needs.pre-publish-gate.result }} + JIRA_RESULT: ${{ needs.validate-jira.result }} + PUBLISH_RESULT: ${{ needs.publish-to-npm.result }} + PRERELEASE_RESULT: ${{ needs.create-prerelease.result }} + run: | + set -euo pipefail + ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; } + + if [[ "$VALIDATE_RESULT" == "success" \ + && "$PREPARE_RESULT" == "success" \ + && "$GATE_RESULT" == "success" ]] \ + && ok "$JIRA_RESULT" \ + && ok "$PUBLISH_RESULT" \ + && ok "$PRERELEASE_RESULT"; then + echo "success=true" >> "$GITHUB_OUTPUT" + echo "failed_stage=" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "success=false" >> "$GITHUB_OUTPUT" + if [[ "$VALIDATE_RESULT" != "success" ]]; then + echo "failed_stage=validate-rc" >> "$GITHUB_OUTPUT" + elif [[ "$PREPARE_RESULT" != "success" ]]; then + echo "failed_stage=prepare-branch" >> "$GITHUB_OUTPUT" + elif [[ "$GATE_RESULT" != "success" ]]; then + echo "failed_stage=pre-publish-gate" >> "$GITHUB_OUTPUT" + elif ! ok "$JIRA_RESULT"; then + echo "failed_stage=validate-jira" >> "$GITHUB_OUTPUT" + elif ! ok "$PUBLISH_RESULT"; then + echo "failed_stage=publish-to-npm" >> "$GITHUB_OUTPUT" + else + echo "failed_stage=create-prerelease" >> "$GITHUB_OUTPUT" + fi + + - name: Format pre-publish leg results + id: legs + env: + CI_RESULT: ${{ needs.run-ci.result }} + E2E_IOS_RESULT: ${{ needs.run-e2e-ios.result }} + E2E_ANDROID_RESULT: ${{ needs.run-e2e-android.result }} + run: | + set -euo pipefail + icon_for() { + local kind="$1" result="$2" + case "$result" in + success) echo ":white_check_mark:" ;; + skipped) + if [[ "$kind" == "ci" ]]; then echo ":fast_forward:"; else echo ":x:"; fi ;; + failure) echo ":x:" ;; + cancelled) echo ":no_entry_sign:" ;; + *) echo ":grey_question:" ;; + esac + } + CI_ICON=$(icon_for ci "$CI_RESULT") + IOS_ICON=$(icon_for e2e "$E2E_IOS_RESULT") + ANDROID_ICON=$(icon_for e2e "$E2E_ANDROID_RESULT") + { + echo "ci_icon=$CI_ICON" + echo "ios_icon=$IOS_ICON" + echo "android_icon=$ANDROID_ICON" + echo "ci_result=$CI_RESULT" + echo "e2e_ios_result=$E2E_IOS_RESULT" + echo "e2e_android_result=$E2E_ANDROID_RESULT" + } >> "$GITHUB_OUTPUT" + + - name: Checkout release branch + if: steps.status.outputs.success == 'true' + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ needs.prepare-branch.outputs.release_branch }} + + - name: Extract SDK versions and changelog + id: extract-info + if: steps.status.outputs.success == 'true' + run: | + VERSION="${{ needs.validate-rc.outputs.version }}" + + echo "android_sdk=${{ needs.validate-rc.outputs.android_sdk_version }}" >> $GITHUB_OUTPUT + echo "ios_sdk=${{ needs.validate-rc.outputs.ios_sdk_version }}" >> $GITHUB_OUTPUT + + # Extract changelog for this version + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- //' | head -5) + if [ -z "$CHANGELOG" ]; then + CHANGELOG="Check CHANGELOG.md for details" + fi + else + CHANGELOG="Check release notes for details" + fi + + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Fetch Jira tickets + id: jira-tickets + if: steps.status.outputs.success == 'true' + continue-on-error: true + env: + CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} + CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} + CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} + run: | + set +e + VERSION="${{ needs.validate-rc.outputs.version }}" + BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') + JIRA_FIX_VERSION="React Native SDK v$BASE_VERSION" + JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" + + echo "Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" + + if [[ -z "${CI_JIRA_EMAIL:-}" ]] || [[ -z "${CI_JIRA_TOKEN:-}" ]]; then + echo "Jira credentials not configured" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT + exit 0 + fi + + JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" + ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) + + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -u "${CI_JIRA_EMAIL}:${CI_JIRA_TOKEN}" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [[ "$HTTP_CODE" != "200" ]]; then + echo "Jira API request failed with status $HTTP_CODE" + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT + exit 0 + fi + + TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) + + if [ -z "$TICKETS" ]; then + echo "tickets=No assigned fix version found" >> $GITHUB_OUTPUT + else + echo "tickets<> $GITHUB_OUTPUT + echo "$TICKETS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: Send Slack notification (Success) + if: steps.status.outputs.success == 'true' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:react::react::react::react::react::react::react::react::react::react::react::react:\n\n${{ needs.validate-rc.outputs.is_dry_run == 'true' && ':test_tube: *[DRY RUN]* ' || '' }}*React Native Release Candidate:*\nreact-native-appsflyer: ${{ needs.validate-rc.outputs.version }} ${{ needs.validate-rc.outputs.is_dry_run == 'true' && 'pipeline completed (not published to npm)' || 'is ready for QA testing' }}.\n\n*Pre-publish checks:*\n${{ steps.legs.outputs.ci_icon }} Lint, Test & Build: ${{ steps.legs.outputs.ci_result }}\n${{ steps.legs.outputs.ios_icon }} RC-E2E iOS: ${{ steps.legs.outputs.e2e_ios_result }}\n${{ steps.legs.outputs.android_icon }} RC-E2E Android: ${{ steps.legs.outputs.e2e_android_result }}\n${{ needs.validate-rc.outputs.is_dry_run != 'true' && format('\n*Testing Instructions:*\n```\nnpm install react-native-appsflyer@{0} --save\n```', needs.validate-rc.outputs.version) || '' }}\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/${{ github.ref_name }}\n:github: Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n${{ needs.validate-rc.outputs.is_dry_run != 'true' && format(':github: Release: https://github.com/{0}/releases/tag/{1}', github.repository, needs.validate-rc.outputs.version) || '' }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDKs:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n:react::react::react::react::react::react::react::react::react::react::react::react:" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} + + - name: Send failure notification + if: steps.status.outputs.success == 'false' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:warning: *${{ needs.validate-rc.outputs.is_dry_run == 'true' && '[DRY RUN] ' || '' }}React Native RC failed at `${{ steps.status.outputs.failed_stage }}`*\n\nVersion: ${{ needs.validate-rc.outputs.version }}\nBranch: ${{ github.ref_name }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n*Pre-publish checks:*\n${{ steps.legs.outputs.ci_icon }} Lint, Test & Build: ${{ steps.legs.outputs.ci_result }}\n${{ steps.legs.outputs.ios_icon }} RC-E2E iOS: ${{ steps.legs.outputs.e2e_ios_result }}\n${{ steps.legs.outputs.android_icon }} RC-E2E Android: ${{ steps.legs.outputs.e2e_android_result }}\n\n*Downstream stages:*\n- prepare-branch: ${{ needs.prepare-branch.result }}\n- pre-publish-gate: ${{ needs.pre-publish-gate.result }}\n- validate-jira: ${{ needs.validate-jira.result }}\n- publish-to-npm: ${{ needs.publish-to-npm.result }}\n- create-prerelease: ${{ needs.create-prerelease.result }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} + + # =========================================================================== + # Production Notify Team + # =========================================================================== + + notify-team-production: + name: Notify Team (Production) + runs-on: ubuntu-latest + needs: [determine-release-type, validate-production, publish-to-npm, create-github-release] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'production' && + needs.validate-production.outputs.is_dry_run != 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + + - name: Extract SDK versions and changelog + id: extract-info + env: + VERSION: ${{ needs.validate-production.outputs.version }} + run: | + # Extract Android SDK fallback version from build.gradle + ANDROID_SDK_VERSION=$(grep "af-android-sdk" android/build.gradle | grep -oP "'[0-9][^']*'" | tr -d "'" | head -1) + echo "android_sdk=$ANDROID_SDK_VERSION" >> "$GITHUB_OUTPUT" + + # Extract iOS SDK version from podspec + IOS_SDK_VERSION=$(grep "AppsFlyerFramework'" react-native-appsflyer.podspec | grep -oP "'~> \K[^']*" | head -1) + if [ -z "$IOS_SDK_VERSION" ]; then + IOS_SDK_VERSION=$(grep "AppsFlyerFramework'" react-native-appsflyer.podspec | grep -oP "'\K[0-9][^']*" | head -1) + fi + echo "ios_sdk=$IOS_SDK_VERSION" >> "$GITHUB_OUTPUT" + + # Extract changelog for this version + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- /- /' | head -5) + if [ -z "$CHANGELOG" ]; then + CHANGELOG="- Check CHANGELOG.md for details" + fi + else + CHANGELOG="- Check release notes for details" + fi + + echo "changelog<> "$GITHUB_OUTPUT" + echo "$CHANGELOG" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Fetch Jira tickets + id: jira-tickets + continue-on-error: true + env: + VERSION: ${{ needs.validate-production.outputs.version }} + CI_JIRA_EMAIL: ${{ secrets.CI_JIRA_EMAIL }} + CI_JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} + CI_JIRA_DOMAIN: ${{ secrets.CI_JIRA_DOMAIN }} + run: | + set +e + JIRA_FIX_VERSION="React Native SDK v$VERSION" + + echo "Looking for Jira tickets with fix version: $JIRA_FIX_VERSION" + + if [[ -z "$CI_JIRA_EMAIL" ]] || [[ -z "$CI_JIRA_TOKEN" ]]; then + echo "::warning::Jira credentials not configured" + echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" + exit 0 + fi + + JIRA_DOMAIN="${CI_JIRA_DOMAIN:-appsflyer.atlassian.net}" + + JQL_QUERY="fixVersion=\"${JIRA_FIX_VERSION}\"" + ENCODED_JQL=$(echo "$JQL_QUERY" | jq -sRr @uri) + + RESPONSE=$(curl -s -w "\n%{http_code}" \ + -u "$CI_JIRA_EMAIL:$CI_JIRA_TOKEN" \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + "https://${JIRA_DOMAIN}/rest/api/3/search/jql?jql=${ENCODED_JQL}&fields=key,summary&maxResults=20") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [[ "$HTTP_CODE" != "200" ]]; then + echo "::warning::Jira API request failed with status $HTTP_CODE" + echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" + exit 0 + fi + + TICKETS=$(echo "$BODY" | jq -r '.issues[]? | "- https://'"${JIRA_DOMAIN}"'/browse/\(.key) - \(.fields.summary)"' 2>/dev/null | head -10) + + if [ -z "$TICKETS" ]; then + echo "No linked tickets found for version: $JIRA_FIX_VERSION" + echo "tickets=No assigned fix version found" >> "$GITHUB_OUTPUT" + else + echo "Found Jira tickets:" + echo "$TICKETS" + echo "tickets<> "$GITHUB_OUTPUT" + echo "$TICKETS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Determine status and failed stage + id: status + env: + VALIDATE_RESULT: ${{ needs.validate-production.result }} + PUBLISH_RESULT: ${{ needs.publish-to-npm.result }} + RELEASE_RESULT: ${{ needs.create-github-release.result }} + run: | + set -euo pipefail + if [[ "$VALIDATE_RESULT" == "success" \ + && "$PUBLISH_RESULT" == "success" \ + && "$RELEASE_RESULT" == "success" ]]; then + echo "success=true" >> "$GITHUB_OUTPUT" + echo "failed_stage=" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "success=false" >> "$GITHUB_OUTPUT" + if [[ "$VALIDATE_RESULT" != "success" ]]; then + echo "failed_stage=validate-production" >> "$GITHUB_OUTPUT" + elif [[ "$PUBLISH_RESULT" != "success" ]]; then + echo "failed_stage=publish-to-npm" >> "$GITHUB_OUTPUT" + else + echo "failed_stage=create-github-release" >> "$GITHUB_OUTPUT" + fi + + - name: Send Slack success notification + if: steps.status.outputs.success == 'true' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:react::react::react::react::react::react::react::react::react::react::react::react:\n\n*React Native:*\nnpm install react-native-appsflyer@${{ needs.validate-production.outputs.version }} is published to Production.\n\n:white_check_mark: rc-smoke/npm passed before promotion (verified by promote-release.yml).\n\n*Sources:*\n:github: https://github.com/${{ github.repository }}/tree/master\n:github: Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n:npm: https://www.npmjs.com/package/react-native-appsflyer/v/${{ needs.validate-production.outputs.version }}\n\n*Changes and fixes:*\n${{ steps.extract-info.outputs.changelog }}\n\n*Linked tickets and issues:*\n${{ steps.jira-tickets.outputs.tickets }}\n\n*Native SDKs:*\n:android: ${{ steps.extract-info.outputs.android_sdk }}\n:apple: ${{ steps.extract-info.outputs.ios_sdk }}\n\n:react::react::react::react::react::react::react::react::react::react::react::react:" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} + + - name: Send Slack failure notification + if: steps.status.outputs.success == 'false' + uses: slackapi/slack-github-action@v1 + with: + payload: | + { + "text": "\n:warning: *React Native production release failed at `${{ steps.status.outputs.failed_stage }}`*\n\nVersion: ${{ needs.validate-production.outputs.version }}\nRun: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n*Stage results:*\n- validate-production: ${{ needs.validate-production.result }}\n- publish-to-npm: ${{ needs.publish-to-npm.result }}\n- create-github-release: ${{ needs.create-github-release.result }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_HOOK }} + + # =========================================================================== + # RC Summary + # =========================================================================== + + rc-summary: + name: RC Summary + runs-on: ubuntu-latest + needs: [determine-release-type, validate-rc, run-ci, prepare-branch, run-e2e-ios, run-e2e-android, pre-publish-gate, validate-jira, publish-to-npm, create-prerelease] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'rc' + + steps: + - name: Display RC Summary + run: | + echo "=========================================" + echo "RC Release Summary" + echo "=========================================" + echo "Version: ${{ needs.validate-rc.outputs.version }}" + echo "Dry Run: ${{ needs.validate-rc.outputs.is_dry_run }}" + echo "-----------------------------------------" + echo "RC-PREP validate: ${{ needs.validate-rc.result }}" + echo "Lint, Test & Build: ${{ needs.run-ci.result }}" + echo "RC-PREP branch: ${{ needs.prepare-branch.result }}" + echo "RC-E2E iOS: ${{ needs.run-e2e-ios.result }}" + echo "RC-E2E Android: ${{ needs.run-e2e-android.result }}" + echo "Pre-publish gate: ${{ needs.pre-publish-gate.result }}" + echo "Validate Jira: ${{ needs.validate-jira.result }}" + echo "Publish to npm: ${{ needs.publish-to-npm.result }}" + echo "Pre-release tag + PR: ${{ needs.create-prerelease.result }}" + echo "=========================================" + + ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; } + + FAIL=0 + # validate-rc and pre-publish-gate must succeed (never legitimately skipped) + [[ "${{ needs.validate-rc.result }}" == "success" ]] || FAIL=1 + [[ "${{ needs.pre-publish-gate.result }}" == "success" ]] || FAIL=1 + # downstream jobs may be skipped (dry-run, skip_e2e, etc.) + ok "${{ needs.run-ci.result }}" || FAIL=1 + ok "${{ needs.prepare-branch.result }}" || FAIL=1 + ok "${{ needs.validate-jira.result }}" || FAIL=1 + ok "${{ needs.publish-to-npm.result }}" || FAIL=1 + ok "${{ needs.create-prerelease.result }}" || FAIL=1 + + if [[ "$FAIL" == "0" ]]; then + echo "RC Release Process Completed Successfully" + else + echo "RC Release Process Failed" + echo "Check the logs above for details" + exit 1 + fi + + # =========================================================================== + # Production Release Summary + # =========================================================================== + + release-summary: + name: Release Summary + runs-on: ubuntu-latest + needs: [determine-release-type, validate-production, publish-to-npm, create-github-release] + if: >- + !cancelled() && + needs.determine-release-type.outputs.release_type == 'production' + + steps: + - name: Display Release Summary + env: + VERSION: ${{ needs.validate-production.outputs.version }} + DRY_RUN: ${{ needs.validate-production.outputs.is_dry_run }} + VALIDATE_RESULT: ${{ needs.validate-production.result }} + PUBLISH_RESULT: ${{ needs.publish-to-npm.result }} + RELEASE_RESULT: ${{ needs.create-github-release.result }} + REPO: ${{ github.repository }} + run: | + echo "=========================================" + echo "Production Release Summary" + echo "=========================================" + echo "Version: $VERSION" + echo "Dry Run: $DRY_RUN" + echo "-----------------------------------------" + echo "Validation: $VALIDATE_RESULT" + echo "npm Publish: $PUBLISH_RESULT" + echo "GitHub Release: $RELEASE_RESULT" + echo "=========================================" + + if [[ "$DRY_RUN" == "true" ]]; then + echo "This was a DRY RUN - no actual publishing occurred" + exit 0 + fi + + if [[ "$VALIDATE_RESULT" == "success" ]] && \ + [[ "$PUBLISH_RESULT" == "success" ]] && \ + [[ "$RELEASE_RESULT" == "success" ]]; then + echo "" + echo "Production Release Completed Successfully!" + echo "" + echo "Version $VERSION is now live!" + echo "" + echo "npm: https://www.npmjs.com/package/react-native-appsflyer/v/$VERSION" + echo "GitHub: https://github.com/$REPO/releases/tag/v$VERSION" + else + echo "" + echo "Production Release Failed" + echo "Check the logs above for details and retry if necessary" + exit 1 + fi diff --git a/RELEASE_USER_MANUAL.md b/RELEASE_USER_MANUAL.md index 0db94b4a..222b10d5 100644 --- a/RELEASE_USER_MANUAL.md +++ b/RELEASE_USER_MANUAL.md @@ -12,9 +12,9 @@ Step-by-step guide for cutting, validating, and shipping a React Native plugin r ## Workflow overview ``` -rc-release.yml ─── lint-test-build.yml ───┐ - ios-e2e.yml ───────────┤ - android-e2e.yml ───────┤ +release.yml (RC) ── lint-test-build.yml ───┐ + ios-e2e.yml ──────────┤ + android-e2e.yml ──────┤ ▼ publish RC to npm create pre-release + PR @@ -28,20 +28,21 @@ rc-release.yml ─── lint-test-build.yml ───┐ │ Human merges PR to master │ - production-release.yml + release.yml (production) (publishes to npm @latest) ``` ## 1. Cut a release candidate -1. Go to **Actions** > **RC - Release Candidate** > **Run workflow**. +1. Go to **Actions** > **Release** > **Run workflow**. Select `release_type: rc`. 2. Fill in the required inputs: | Input | Required | Example | Description | |-------|----------|---------|-------------| -| `rn_version` | Yes | `6.18.0-rc1` | Plugin version. Must match `X.Y.Z-rcN` format. | -| `ios_sdk_version` | Yes | `6.18.0` | iOS native AppsFlyer SDK version to pin. | -| `android_sdk_version` | Yes | `6.18.0` | Android native AppsFlyer SDK version to pin. | +| `release_type` | Yes | `rc` | Release type (`rc` or `production`). | +| `rn_version` | Yes | `6.18.0-rc1` | Plugin version. Must match `X.Y.Z-rcN` format for RC. | +| `ios_sdk_version` | No | `6.18.0` | iOS native AppsFlyer SDK version to pin — RC only. | +| `android_sdk_version` | No | `6.18.0` | Android native AppsFlyer SDK version to pin — RC only. | | `base_branch` | No | `development` | Branch to cut the release from (default: `development`). | | `pc_version` | No | `6.15.2` | PurchaseConnector iOS version override. Leave empty to auto-fetch latest from GitHub. | | `skip_unit` | No | `false` | Skip Jest + ESLint inside Lint, Test & Build. | @@ -95,7 +96,7 @@ When the `pass QA ready for deploy` label is applied: When the PR merges to master: -1. `production-release.yml` triggers automatically. +1. `release.yml` triggers automatically (production path). 2. It publishes to npm with the `latest` tag. 3. Creates a GitHub release (not pre-release). 4. Notifies Slack. @@ -148,12 +149,12 @@ Check `.af-e2e/reports` artifacts in the workflow run. E2E failures block npm pu - Re-run the failed E2E job from the Actions UI, or - Cut a new RC if code changes are needed. -### production-release.yml did not trigger after merge +### release.yml (production) did not trigger after merge The workflow triggers on `pull_request: closed` to `master` from `releases/*` branches. If it didn't fire: - Verify the PR was merged (not just closed). - Verify the source branch matched `releases/*`. -- Use the manual dispatch: Actions > Production Release > Run workflow, enter the version. +- Use the manual dispatch: Actions > Release > Run workflow, select `production` type, enter the version. ## Version files reference diff --git a/android/build.gradle b/android/build.gradle index 5d58ec05..2a168cfd 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -73,6 +73,6 @@ dependencies { api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.17.6')}" implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.2')}" if (includeConnector){ - implementation 'com.appsflyer:purchase-connector:2.2.1' + implementation 'com.appsflyer:purchase-connector:2.2.0' } } \ No newline at end of file diff --git a/package.json b/package.json index 7a2d6b7b..48c198cc 100755 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "appsflyer" ], "bugs": { - "url": "https://github.com/AppsFlyerSDK/react-native-appsflyer/issues" + "url": "https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/issues" }, "devDependencies": { "@babel/preset-env": "^7.26.9",