From 169be508464b3891d96d5f1cc0723c6d750cd5a1 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 14 May 2026 15:17:14 +0300 Subject: [PATCH 01/11] Refine release workflows and update `package.json` bugs URL Configure release workflows to explicitly use `https://registry.npmjs.org` for package fetching and disable package manager cache. This ensures consistent and reliable builds by preventing reliance on implicit defaults and avoiding potential stale cache issues. Update the `bugs.url` in `package.json` to reflect the current repository name. --- .github/workflows/production-release.yml | 2 ++ .github/workflows/rc-release.yml | 2 ++ package.json | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 65ce2e45..660e509a 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -168,6 +168,8 @@ jobs: uses: actions/setup-node@v6 with: node-version: 24 + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false - name: Publish to npm if: needs.validate-release.outputs.is_dry_run != 'true' diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 11bbda85..67882558 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -466,6 +466,8 @@ jobs: 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 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", From cb614d8a68259575a979ac012803f58e7b8677b5 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 14 May 2026 15:41:25 +0300 Subject: [PATCH 02/11] Clear NODE_AUTH_TOKEN for npm publish steps Explicitly set the `NODE_AUTH_TOKEN` environment variable to an empty string during `npm publish` in both production and RC release workflows. This prevents `npm` from attempting to use any inherited or implicitly available authentication tokens, ensuring consistent and predictable publishing of public packages. --- .github/workflows/production-release.yml | 1 + .github/workflows/rc-release.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 660e509a..fc478f61 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -175,6 +175,7 @@ jobs: if: needs.validate-release.outputs.is_dry_run != 'true' env: VERSION: ${{ needs.validate-release.outputs.version }} + NODE_AUTH_TOKEN: '' run: | echo "Publishing version $VERSION to npm..." npm publish diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 67882558..e25cb60e 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -482,6 +482,8 @@ jobs: - name: Publish RC to npm if: ${{ needs.validate-release.outputs.is_dry_run != 'true' }} + env: + NODE_AUTH_TOKEN: '' run: npm publish --tag rc # =========================================================================== From b2deb1b8c6ad861a59d4024e89a471accc0d05b1 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Thu, 14 May 2026 15:43:46 +0300 Subject: [PATCH 03/11] Enable provenance for npm publishing This adds the `--provenance` flag to `npm publish` commands in both production and RC release workflows. This enhances supply chain security by attaching verifiable build information to published packages. --- .github/workflows/production-release.yml | 2 +- .github/workflows/rc-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index fc478f61..ab8e6c3e 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -178,7 +178,7 @@ jobs: NODE_AUTH_TOKEN: '' run: | echo "Publishing version $VERSION to npm..." - npm publish + npm publish --provenance echo "Published $VERSION to npm" - name: Dry-run publish diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index e25cb60e..a3d7b628 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -484,7 +484,7 @@ jobs: if: ${{ needs.validate-release.outputs.is_dry_run != 'true' }} env: NODE_AUTH_TOKEN: '' - run: npm publish --tag rc + run: npm publish --tag rc --provenance # =========================================================================== # Create Pre-Release Tag From 4aca5fd3b7a533769aa8c19965d1787ed17227df Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Sun, 17 May 2026 14:24:27 +0300 Subject: [PATCH 04/11] Unify RC and production release workflows Consolidates `rc-release.yml` and `production-release.yml` into a single `release.yml` workflow. This unification addresses npm OIDC Trusted Publishing requirements, which often restrict publishing to a single workflow file per package. It streamlines the release process by centralizing both Release Candidate (RC) and production npm publication logic, improving maintainability and consistency. --- .github/workflows/android-e2e.yml | 2 +- .github/workflows/ios-e2e.yml | 2 +- .github/workflows/lint-test-build.yml | 4 +- .github/workflows/production-release.yml | 508 -------- .github/workflows/promote-release.yml | 4 +- .github/workflows/rc-release.yml | 858 ------------- .github/workflows/rc-smoke.yml | 4 +- .github/workflows/release.yml | 1418 ++++++++++++++++++++++ RELEASE_USER_MANUAL.md | 23 +- 9 files changed, 1438 insertions(+), 1385 deletions(-) delete mode 100644 .github/workflows/production-release.yml delete mode 100644 .github/workflows/rc-release.yml create mode 100644 .github/workflows/release.yml 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 ab8e6c3e..00000000 --- a/.github/workflows/production-release.yml +++ /dev/null @@ -1,508 +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 - registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - - - name: Publish to npm - if: needs.validate-release.outputs.is_dry_run != 'true' - env: - VERSION: ${{ needs.validate-release.outputs.version }} - NODE_AUTH_TOKEN: '' - run: | - echo "Publishing version $VERSION to npm..." - npm publish --provenance - 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 a3d7b628..00000000 --- a/.github/workflows/rc-release.yml +++ /dev/null @@ -1,858 +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' - registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - - - 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' }} - env: - NODE_AUTH_TOKEN: '' - run: npm publish --tag rc --provenance - - # =========================================================================== - # 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..42b62a3f 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: 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 From 455125347dc4d56041bf9371e4492b5497ac1fb1 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Sun, 17 May 2026 15:36:51 +0300 Subject: [PATCH 05/11] Execute scenario runner script with bash Ensures consistent execution of the `af-scenario-runner.sh` script by explicitly invoking `bash`. This prevents potential issues if the default shell in the CI environment differs or lacks necessary features. --- .github/workflows/rc-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rc-smoke.yml b/.github/workflows/rc-smoke.yml index 42b62a3f..307ec825 100644 --- a/.github/workflows/rc-smoke.yml +++ b/.github/workflows/rc-smoke.yml @@ -382,7 +382,7 @@ jobs: 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 + bash ./scripts/af-scenario-runner.sh --platform android --plan .af-smoke/rc-test-plan.json - name: Upload smoke reports if: always() From 4985760f5820787387ec2a2db11c64e2240c1c06 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Sun, 17 May 2026 15:58:12 +0300 Subject: [PATCH 06/11] Improve Android RC smoke test configuration and workflow Adjusts the Android test plan to use updated package and activity names and increases wait times for enhanced stability. Refines the CI workflow by switching to a direct Gradle build command for the Android example app and adds `checks: write` permissions for better status reporting. --- .af-smoke/rc-test-plan.json | 8 ++++---- .github/workflows/rc-smoke.yml | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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/rc-smoke.yml b/.github/workflows/rc-smoke.yml index 307ec825..9d24832d 100644 --- a/.github/workflows/rc-smoke.yml +++ b/.github/workflows/rc-smoke.yml @@ -380,8 +380,7 @@ 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 .. + 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 @@ -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 From 00461ca4839e037a2baac56f3dee3b3fedd9e887 Mon Sep 17 00:00:00 2001 From: AmitLY21 Date: Mon, 18 May 2026 10:41:57 +0300 Subject: [PATCH 07/11] Change AppsFlyer Purchase Connector Version Reverts the AppsFlyer `purchase-connector` dependency in `android/build.gradle` from `2.2.1` to `2.2.0`. --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7dd94df2df9bf3f86521687662446c4c133466c4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 May 2026 07:44:33 +0000 Subject: [PATCH 08/11] chore: prepare RC 6.18.0-rc3 (iOS 6.18.0, Android 6.18.0) --- CHANGELOG.md | 8 ++++++++ README.md | 4 ++-- android/build.gradle | 2 +- .../com/appsflyer/reactnative/RNAppsFlyerConstants.java | 2 +- ios/RNAppsFlyer.h | 2 +- package.json | 2 +- react-native-appsflyer.podspec | 6 +++--- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15eb426..e24a23da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 6.18.0-rc3 + Release date: *2026-05-18* + +### Changes +- Android SDK 6.18.0 +- iOS SDK 6.18.0 +- TODO: Add specific changes before merging + ## 6.17.9 Release date: *2026-03-31* diff --git a/README.md b/README.md index b11facb5..dc6be33d 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar ### This plugin is built for -- Android AppsFlyer SDK **v6.17.6** -- iOS AppsFlyer SDK **v6.17.9** +- Android AppsFlyer SDK **v6.18.0** +- iOS AppsFlyer SDK **v6.18.0** - Minimum tested with React-Native **v0.62.0** (older versions might be supported) ## Release Updates diff --git a/android/build.gradle b/android/build.gradle index 2a168cfd..fc55248f 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -70,7 +70,7 @@ repositories { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.10" // Add Kotlin standard library implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" - api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.17.6')}" + api "com.appsflyer:af-android-sdk:${safeExtGet('appsflyerVersion', '6.18.0')}" implementation "com.android.installreferrer:installreferrer:${safeExtGet('installReferrerVersion', '2.2')}" if (includeConnector){ implementation 'com.appsflyer:purchase-connector:2.2.0' diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java index d39fdc7f..c661e5c8 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java @@ -6,7 +6,7 @@ public class RNAppsFlyerConstants { - final static String PLUGIN_VERSION = "6.17.9"; + final static String PLUGIN_VERSION = "6.18.0-rc3"; final static String NO_DEVKEY_FOUND = "No 'devKey' found or its empty"; final static String UNKNOWN_ERROR = "AF Unknown Error"; final static String SUCCESS = "Success"; diff --git a/ios/RNAppsFlyer.h b/ios/RNAppsFlyer.h index e16136a5..0541dd83 100755 --- a/ios/RNAppsFlyer.h +++ b/ios/RNAppsFlyer.h @@ -22,7 +22,7 @@ @end -static NSString *const kAppsFlyerPluginVersion = @"6.17.9"; +static NSString *const kAppsFlyerPluginVersion = @"6.18.0-rc3"; static NSString *const NO_DEVKEY_FOUND = @"No 'devKey' found or its empty"; static NSString *const NO_APPID_FOUND = @"No 'appId' found or its empty"; static NSString *const NO_EVENT_NAME_FOUND = @"No 'eventName' found or its empty"; diff --git a/package.json b/package.json index 48c198cc..96294b69 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "6.17.9", + "version": "6.18.0-rc3", "description": "React Native Appsflyer plugin", "main": "index.js", "types": "index.d.ts", diff --git a/react-native-appsflyer.podspec b/react-native-appsflyer.podspec index 572ff89f..c2da2019 100644 --- a/react-native-appsflyer.podspec +++ b/react-native-appsflyer.podspec @@ -30,19 +30,19 @@ Pod::Spec.new do |s| # AppsFlyerPurchaseConnector if defined?($AppsFlyerPurchaseConnector) && ($AppsFlyerPurchaseConnector == true) Pod::UI.puts "#{s.name}: Including PurchaseConnector." - s.dependency 'PurchaseConnector', '6.17.9' + s.dependency 'PurchaseConnector', '6.18.1' end # AppsFlyerFramework if defined?($RNAppsFlyerStrictMode) && ($RNAppsFlyerStrictMode == true) Pod::UI.puts "#{s.name}: Using AppsFlyerFramework/Strict mode" - s.dependency 'AppsFlyerFramework/Strict', '6.17.9' + s.dependency 'AppsFlyerFramework/Strict', '6.18.0' s.xcconfig = {'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) AFSDK_NO_IDFA=1' } else if !defined?($RNAppsFlyerStrictMode) Pod::UI.puts "#{s.name}: Using default AppsFlyerFramework. You may require App Tracking Transparency. Not allowed for Kids apps." Pod::UI.puts "#{s.name}: You may set variable `$RNAppsFlyerStrictMode=true` in Podfile to use strict mode for kids apps." end - s.dependency 'AppsFlyerFramework', '6.17.9' + s.dependency 'AppsFlyerFramework', '6.18.0' end end From acef1ba923448c216d56f02ba70fbe1b44ec5912 Mon Sep 17 00:00:00 2001 From: Amit Levy <118281047+al-af@users.noreply.github.com> Date: Mon, 18 May 2026 12:56:02 +0300 Subject: [PATCH 09/11] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e24a23da..cdc9304c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changes - Android SDK 6.18.0 - iOS SDK 6.18.0 -- TODO: Add specific changes before merging +- Android Purchase connector to v2.2.0 supporting only Billing Library 8 ## 6.17.9 Release date: *2026-03-31* From d0fd0dc15095f2fa57630dc0557b0f263d1960dd Mon Sep 17 00:00:00 2001 From: Amit Levy <118281047+al-af@users.noreply.github.com> Date: Mon, 18 May 2026 13:06:30 +0300 Subject: [PATCH 10/11] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc6be33d..0623f6d0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ To do so, please follow [this article](https://support.appsflyer.com/hc/en-us/ar - Minimum tested with React-Native **v0.62.0** (older versions might be supported) ## Release Updates +- Starting with version `6.18.0`, Android Purchase Connector: Updated to purchase-connector:2.2.0 with Billing Library 8 support, apps using Billing Library 7 APIs directly must migrate - Starting with version `6.17.1` the plugin supports the Purchase Connector for validating and measuring Subscription and In-app purchase events. Integration guide can be found [here](https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/blob/master/Docs/RN_PurchaseConnector.md). From f4fe75e479533f2ae57ee388d11ff1ba69890ca8 Mon Sep 17 00:00:00 2001 From: "Amit.kremer" Date: Tue, 19 May 2026 07:25:04 +0000 Subject: [PATCH 11/11] chore: prepare production release 6.18.0 (from 6.18.0-rc3) --- .../java/com/appsflyer/reactnative/RNAppsFlyerConstants.java | 2 +- ios/RNAppsFlyer.h | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java index c661e5c8..ae00bb23 100755 --- a/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java +++ b/android/src/main/java/com/appsflyer/reactnative/RNAppsFlyerConstants.java @@ -6,7 +6,7 @@ public class RNAppsFlyerConstants { - final static String PLUGIN_VERSION = "6.18.0-rc3"; + final static String PLUGIN_VERSION = "6.18.0"; final static String NO_DEVKEY_FOUND = "No 'devKey' found or its empty"; final static String UNKNOWN_ERROR = "AF Unknown Error"; final static String SUCCESS = "Success"; diff --git a/ios/RNAppsFlyer.h b/ios/RNAppsFlyer.h index 0541dd83..5482b7ec 100755 --- a/ios/RNAppsFlyer.h +++ b/ios/RNAppsFlyer.h @@ -22,7 +22,7 @@ @end -static NSString *const kAppsFlyerPluginVersion = @"6.18.0-rc3"; +static NSString *const kAppsFlyerPluginVersion = @"6.18.0"; static NSString *const NO_DEVKEY_FOUND = @"No 'devKey' found or its empty"; static NSString *const NO_APPID_FOUND = @"No 'appId' found or its empty"; static NSString *const NO_EVENT_NAME_FOUND = @"No 'eventName' found or its empty"; diff --git a/package.json b/package.json index 96294b69..ac48d8d8 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-appsflyer", - "version": "6.18.0-rc3", + "version": "6.18.0", "description": "React Native Appsflyer plugin", "main": "index.js", "types": "index.d.ts",