RC - Release Candidate #8
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
| # ============================================================================= | |
| # 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@v4 | |
| 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@v4 | |
| with: | |
| node-version: '20' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Validate package (dry-run) | |
| run: npm pack --dry-run | |
| - name: RC dry-run active -- skipping publish | |
| if: ${{ needs.validate-release.outputs.is_dry_run == 'true' }} | |
| run: | | |
| echo "RC dry_run is true -- will not publish to npm." | |
| - name: Publish RC to npm | |
| if: ${{ needs.validate-release.outputs.is_dry_run != 'true' }} | |
| run: npm publish --tag rc | |
| # =========================================================================== | |
| # Create Pre-Release Tag | |
| # =========================================================================== | |
| create-prerelease: | |
| name: Create Pre-Release | |
| runs-on: ubuntu-latest | |
| needs: [validate-release, prepare-branch, publish-rc] | |
| if: always() && needs.validate-release.outputs.is_rc == 'true' && needs.publish-rc.result == 'success' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.prepare-branch.outputs.release_branch }} | |
| - name: Generate release notes | |
| id: release-notes | |
| env: | |
| VERSION: ${{ needs.validate-release.outputs.version }} | |
| IOS_VER: ${{ needs.validate-release.outputs.ios_sdk_version }} | |
| AND_VER: ${{ needs.validate-release.outputs.android_sdk_version }} | |
| run: | | |
| cat > release_notes.md << EOF | |
| # AppsFlyer React Native Plugin - Release Candidate $VERSION | |
| ## Release Candidate for Testing | |
| This is a pre-release version for QA testing. Do not use in production. | |
| ## Testing Instructions | |
| \`\`\`bash | |
| npm install react-native-appsflyer@${VERSION} --save | |
| \`\`\` | |
| ## SDK Versions | |
| - Android AppsFlyer SDK: ${AND_VER} | |
| - iOS AppsFlyer SDK: ${IOS_VER} | |
| --- | |
| **Note**: This is a pre-release and should not be used in production applications. | |
| EOF | |
| - name: Create GitHub Pre-Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.validate-release.outputs.version }} | |
| name: Release Candidate ${{ needs.validate-release.outputs.version }} | |
| body_path: release_notes.md | |
| draft: false | |
| prerelease: true | |
| generate_release_notes: false | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # =========================================================================== | |
| # Open PR to master | |
| # =========================================================================== | |
| open-pr: | |
| name: Open PR to master | |
| runs-on: ubuntu-latest | |
| needs: [validate-release, prepare-branch, publish-rc] | |
| if: always() && needs.publish-rc.result == 'success' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.prepare-branch.outputs.release_branch }} | |
| - name: Create or update PR | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const version = '${{ needs.validate-release.outputs.base_version }}'; | |
| const rcVersion = '${{ needs.validate-release.outputs.version }}'; | |
| const head = '${{ needs.prepare-branch.outputs.release_branch }}'; | |
| const base = 'master'; | |
| const androidVersion = '${{ needs.validate-release.outputs.android_sdk_version }}'; | |
| const iosVersion = '${{ needs.validate-release.outputs.ios_sdk_version }}'; | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${context.repo.owner}:${head}` | |
| }); | |
| const body = [ | |
| `### Release ${version}`, | |
| '', | |
| `- Android SDK: ${androidVersion}`, | |
| `- iOS SDK: ${iosVersion}`, | |
| '', | |
| '```bash', | |
| `npm install react-native-appsflyer@${rcVersion} --save`, | |
| '```', | |
| '', | |
| 'This PR was opened by the RC workflow.' | |
| ].join('\n'); | |
| if (prs.length > 0) { | |
| const pr = prs[0]; | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| title: `Release ${version}`, | |
| body | |
| }); | |
| core.setOutput('pr_number', pr.number); | |
| } else { | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| head, | |
| base, | |
| title: `Release ${version}`, | |
| body, | |
| maintainer_can_modify: true | |
| }); | |
| core.setOutput('pr_number', pr.number); | |
| } | |
| # =========================================================================== | |
| # Notify Team | |
| # =========================================================================== | |
| notify-team: | |
| name: Notify Team | |
| runs-on: ubuntu-latest | |
| needs: | |
| - validate-release | |
| - prepare-branch | |
| - run-ci | |
| - run-e2e-ios | |
| - run-e2e-android | |
| - pre-publish-gate | |
| - validate-jira | |
| - publish-rc | |
| - create-prerelease | |
| if: always() | |
| steps: | |
| - name: Determine status and failed stage | |
| id: status | |
| env: | |
| VALIDATE_RESULT: ${{ needs.validate-release.result }} | |
| PREPARE_RESULT: ${{ needs.prepare-branch.result }} | |
| GATE_RESULT: ${{ needs.pre-publish-gate.result }} | |
| JIRA_RESULT: ${{ needs.validate-jira.result }} | |
| PUBLISH_RESULT: ${{ needs.publish-rc.result }} | |
| PRERELEASE_RESULT: ${{ needs.create-prerelease.result }} | |
| run: | | |
| set -euo pipefail | |
| ok() { [[ "$1" == "success" || "$1" == "skipped" ]]; } | |
| if [[ "$VALIDATE_RESULT" == "success" \ | |
| && "$PREPARE_RESULT" == "success" \ | |
| && "$GATE_RESULT" == "success" ]] \ | |
| && ok "$JIRA_RESULT" \ | |
| && ok "$PUBLISH_RESULT" \ | |
| && ok "$PRERELEASE_RESULT"; then | |
| echo "success=true" >> "$GITHUB_OUTPUT" | |
| echo "failed_stage=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "success=false" >> "$GITHUB_OUTPUT" | |
| if [[ "$VALIDATE_RESULT" != "success" ]]; then | |
| echo "failed_stage=validate-release" >> "$GITHUB_OUTPUT" | |
| elif [[ "$PREPARE_RESULT" != "success" ]]; then | |
| echo "failed_stage=prepare-branch" >> "$GITHUB_OUTPUT" | |
| elif [[ "$GATE_RESULT" != "success" ]]; then | |
| echo "failed_stage=pre-publish-gate" >> "$GITHUB_OUTPUT" | |
| elif ! ok "$JIRA_RESULT"; then | |
| echo "failed_stage=validate-jira" >> "$GITHUB_OUTPUT" | |
| elif ! ok "$PUBLISH_RESULT"; then | |
| echo "failed_stage=publish-rc" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "failed_stage=create-prerelease" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Format pre-publish leg results | |
| id: legs | |
| env: | |
| CI_RESULT: ${{ needs.run-ci.result }} | |
| E2E_IOS_RESULT: ${{ needs.run-e2e-ios.result }} | |
| E2E_ANDROID_RESULT: ${{ needs.run-e2e-android.result }} | |
| run: | | |
| set -euo pipefail | |
| icon_for() { | |
| local kind="$1" result="$2" | |
| case "$result" in | |
| success) echo ":white_check_mark:" ;; | |
| skipped) | |
| if [[ "$kind" == "ci" ]]; then echo ":fast_forward:"; else echo ":x:"; fi ;; | |
| failure) echo ":x:" ;; | |
| cancelled) echo ":no_entry_sign:" ;; | |
| *) echo ":grey_question:" ;; | |
| esac | |
| } | |
| CI_ICON=$(icon_for ci "$CI_RESULT") | |
| IOS_ICON=$(icon_for e2e "$E2E_IOS_RESULT") | |
| ANDROID_ICON=$(icon_for e2e "$E2E_ANDROID_RESULT") | |
| { | |
| echo "ci_icon=$CI_ICON" | |
| echo "ios_icon=$IOS_ICON" | |
| echo "android_icon=$ANDROID_ICON" | |
| echo "ci_result=$CI_RESULT" | |
| echo "e2e_ios_result=$E2E_IOS_RESULT" | |
| echo "e2e_android_result=$E2E_ANDROID_RESULT" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Checkout release branch | |
| if: steps.status.outputs.success == 'true' | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ needs.prepare-branch.outputs.release_branch }} | |
| - name: Extract SDK versions and changelog | |
| id: extract-info | |
| if: steps.status.outputs.success == 'true' | |
| run: | | |
| VERSION="${{ needs.validate-release.outputs.version }}" | |
| BASE_VERSION=$(echo "$VERSION" | sed 's/-rc[0-9]*$//') | |
| echo "android_sdk=${{ needs.validate-release.outputs.android_sdk_version }}" >> $GITHUB_OUTPUT | |
| echo "ios_sdk=${{ needs.validate-release.outputs.ios_sdk_version }}" >> $GITHUB_OUTPUT | |
| # Extract changelog for this version | |
| if [ -f "CHANGELOG.md" ]; then | |
| CHANGELOG=$(awk "/## $VERSION/,/^## [0-9]/" CHANGELOG.md | grep "^-" | sed 's/^- //' | head -5) | |
| if [ -z "$CHANGELOG" ]; then | |
| CHANGELOG="Check CHANGELOG.md for details" | |
| fi | |
| else | |
| CHANGELOG="Check release notes for details" | |
| fi | |
| echo "changelog<<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-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<<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-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": "<!here>\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 |