Skip to content

Release

Release #5

Workflow file for this run

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