542 lines
12 KiB
Markdown
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 MyApp.xcworkspace \
|
|
-scheme MyApp \
|
|
-configuration Release \
|
|
-destination generic/platform=iOS \
|
|
-archivePath $PWD/build/MyApp.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: "MyApp.xcodeproj"
|
|
)
|
|
|
|
build_app(
|
|
scheme: "MyApp",
|
|
export_method: "app-store",
|
|
export_options: {
|
|
provisioningProfiles: {
|
|
"com.myapp.app" => "match AppStore com.myapp.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: "MyApp",
|
|
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.myapp.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
|
|
``` |