Release #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | |
| # 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 <support@appsflyer.com> | |
| 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<<EOF" >> $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<<EOF" >> $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": "<!here>\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": "<!here>\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<<EOF" >> "$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<<EOF" >> "$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": "<!here>\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": "<!here>\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 |