Skip to content

RC - Release Candidate #8

RC - Release Candidate

RC - Release Candidate #8

Workflow file for this run

# =============================================================================
# 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