react-native-sop-docs/09-ci-cd-pipeline/github-actions-fastlane.md

542 lines
12 KiB
Markdown

# CI/CD Pipeline
## GitHub Actions Setup
### Basic Workflow
**.github/workflows/ci.yml:**
```yaml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run linter
run: yarn lint
- name: Run tests
run: yarn test --coverage
- name: Type check
run: yarn type-check
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build-android:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Build Android APK
run: |
cd android
./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: android-apk
path: android/app/build/outputs/apk/release/app-release.apk
build-ios:
needs: test
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Build iOS
run: |
cd ios
xcodebuild -workspace SaayamApp.xcworkspace \
-scheme SaayamApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath $PWD/build/SaayamApp.xcarchive \
archive
```
### Advanced Workflow with Fastlane
**.github/workflows/deploy.yml:**
```yaml
name: Deploy to Stores
on:
push:
tags:
- 'v*'
jobs:
deploy-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Decode Keystore
env:
ENCODED_STRING: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo $ENCODED_STRING | base64 -d > android/app/keystore.jks
- name: Deploy to Play Store
env:
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
cd android
bundle exec fastlane deploy
deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
bundler-cache: true
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Deploy to App Store
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
run: |
cd ios
bundle exec fastlane deploy
```
## Fastlane Setup
### Installation
```bash
# Install Fastlane
sudo gem install fastlane
# Initialize Fastlane for iOS
cd ios && fastlane init
# Initialize Fastlane for Android
cd android && fastlane init
```
### iOS Fastfile
**ios/fastlane/Fastfile:**
```ruby
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
setup_ci if ENV['CI']
match(
type: "appstore",
readonly: true
)
increment_build_number(
xcodeproj: "SaayamApp.xcodeproj"
)
build_app(
scheme: "SaayamApp",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.saayam.app" => "match AppStore com.saayam.app"
}
}
)
upload_to_testflight(
skip_waiting_for_build_processing: true
)
slack(
message: "Successfully uploaded a new build to TestFlight! 🚀",
channel: "#releases"
)
end
desc "Deploy to App Store"
lane :deploy do
setup_ci if ENV['CI']
match(
type: "appstore",
readonly: true
)
build_app(
scheme: "SaayamApp",
export_method: "app-store"
)
upload_to_app_store(
force: true,
reject_if_possible: true,
skip_metadata: false,
skip_screenshots: false,
submit_for_review: true,
automatic_release: false,
submission_information: {
add_id_info_limits_tracking: true,
add_id_info_serves_ads: false,
add_id_info_tracks_action: true,
add_id_info_tracks_install: true,
add_id_info_uses_idfa: true,
content_rights_has_rights: true,
content_rights_contains_third_party_content: true,
export_compliance_platform: 'ios',
export_compliance_compliance_required: false,
export_compliance_encryption_updated: false,
export_compliance_app_type: nil,
export_compliance_uses_encryption: false,
export_compliance_is_exempt: false,
export_compliance_contains_third_party_cryptography: false,
export_compliance_contains_proprietary_cryptography: false,
export_compliance_available_on_french_store: false
}
)
slack(
message: "Successfully deployed to App Store! 🎉",
channel: "#releases"
)
end
desc "Create screenshots"
lane :screenshots do
capture_screenshots
upload_to_app_store(skip_binary_upload: true)
end
error do |lane, exception|
slack(
message: "Error in #{lane}: #{exception.message}",
success: false,
channel: "#releases"
)
end
end
```
### Android Fastfile
**android/fastlane/Fastfile:**
```ruby
default_platform(:android)
platform :android do
desc "Build and upload to Play Store Internal Testing"
lane :beta do
gradle(
task: "clean bundleRelease",
project_dir: "."
)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
slack(
message: "Successfully uploaded a new build to Play Store Internal Testing! 🚀",
channel: "#releases"
)
end
desc "Deploy to Play Store"
lane :deploy do
gradle(
task: "clean bundleRelease",
project_dir: "."
)
upload_to_play_store(
track: 'production',
release_status: 'completed',
skip_upload_metadata: false,
skip_upload_changelogs: false,
skip_upload_images: false,
skip_upload_screenshots: false
)
slack(
message: "Successfully deployed to Play Store! 🎉",
channel: "#releases"
)
end
desc "Create screenshots"
lane :screenshots do
capture_android_screenshots
upload_to_play_store(skip_upload_apk: true)
end
error do |lane, exception|
slack(
message: "Error in #{lane}: #{exception.message}",
success: false,
channel: "#releases"
)
end
end
```
## Environment Configuration
### Secrets Management
**GitHub Secrets to configure:**
- `KEYSTORE_BASE64`: Base64 encoded Android keystore
- `KEYSTORE_PASSWORD`: Android keystore password
- `KEY_ALIAS`: Android key alias
- `KEY_PASSWORD`: Android key password
- `GOOGLE_PLAY_SERVICE_ACCOUNT`: Google Play service account JSON
- `MATCH_PASSWORD`: iOS certificates password
- `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`: Apple app-specific password
- `FASTLANE_SESSION`: Fastlane session for Apple ID
- `SLACK_URL`: Slack webhook URL for notifications
### Match Setup (iOS)
```bash
# Initialize match
fastlane match init
# Generate certificates
fastlane match development
fastlane match appstore
```
**Matchfile:**
```ruby
git_url("https://github.com/your-org/certificates")
storage_mode("git")
type("development")
app_identifier(["com.saayam.app"])
username("your-apple-id@example.com")
```
## Branch Protection
### GitHub Branch Protection Rules
```yaml
# .github/branch-protection.yml
protection_rules:
main:
required_status_checks:
strict: true
contexts:
- "test"
- "build-android"
- "build-ios"
enforce_admins: true
required_pull_request_reviews:
required_approving_review_count: 2
dismiss_stale_reviews: true
require_code_owner_reviews: true
restrictions:
users: []
teams: ["core-team"]
```
## Automated Testing in CI
### E2E Testing in CI
**.github/workflows/e2e.yml:**
```yaml
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
e2e-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
pod install
- name: Build for Detox
run: detox build --configuration ios.sim.release
- name: Run Detox tests
run: detox test --configuration ios.sim.release --cleanup
e2e-android:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-29
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run Detox tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: |
detox build --configuration android.emu.release
detox test --configuration android.emu.release --cleanup
```