diff --git a/.github/MOBILE_DEPLOYMENT.md b/.github/MOBILE_DEPLOYMENT.md new file mode 100644 index 000000000..9fedff017 --- /dev/null +++ b/.github/MOBILE_DEPLOYMENT.md @@ -0,0 +1,217 @@ +# Mobile Deployment Guide + +This guide covers the automated mobile deployment pipeline for iOS and Android apps. + +## ๐Ÿš€ Quick Start + +### Automatic Deployments + +Deployments happen automatically when PRs are merged: + +- **Merge to `dev`** โ†’ Deploy to internal testing +- **Merge to `main`** โ†’ Deploy to production + +To skip deployment, add `[skip-deploy]` to your PR title or add the `no-deploy` label. + +### Manual Deployments + +1. Go to [Actions](../../../actions) โ†’ "Mobile App Deployments" +2. Click "Run workflow" +3. Select options: + - Platform: ios / android / both + - Test mode: Check to build without uploading + - Deployment track: internal / production + - Version bump: build / patch / minor / major + +## ๐Ÿ“‹ How It Works + +### Branch Strategy + +``` +main (production) + โ†‘ + โ””โ”€โ”€ dev (internal testing) + โ†‘ + โ””โ”€โ”€ feature/* (no auto-deploy) +``` + +### Version Management + +Versions are controlled by PR labels: + +- `version:major` - Bump major version (1.0.0 โ†’ 2.0.0) +- `version:minor` - Bump minor version (1.0.0 โ†’ 1.1.0) +- `version:patch` - Bump patch version (1.0.0 โ†’ 1.0.1) [default for main] +- No label on dev - Only increment build number + +### Deployment Tracks + +| Branch | Track | iOS Target | Android Target | +|--------|-------|------------|----------------| +| dev | internal | TestFlight Internal | Play Store Internal | +| main | production | App Store | Play Store Production | + +## ๐Ÿ—๏ธ Architecture + +### Workflows + +1. **`mobile-deploy.yml`** - Main deployment workflow + - Handles both manual and automated deployments + - Builds and uploads to app stores + - Creates git tags for production releases + +2. **`mobile-deploy-auto.yml`** - PR merge trigger + - Detects merged PRs + - Determines deployment parameters + - Calls main deployment workflow + +### Version Storage + +- `app/version.json` - Tracks build numbers +- `app/package.json` - Semantic version +- Native files auto-sync during build + +### Caching Strategy + +Build times are optimized with caching: +- Yarn dependencies +- Ruby gems +- CocoaPods (iOS) +- Gradle (Android) +- Android NDK + +Average build times with cache: iOS ~15min, Android ~10min + +## ๐Ÿ”ง Configuration + +### Required Secrets + +#### iOS +- `IOS_APP_IDENTIFIER` - Bundle ID +- `IOS_TEAM_ID` - Apple Team ID +- `IOS_CONNECT_KEY_ID` - App Store Connect API Key ID +- `IOS_CONNECT_ISSUER_ID` - API Key Issuer ID +- `IOS_CONNECT_API_KEY_BASE64` - API Key (base64) +- `IOS_DIST_CERT_BASE64` - Distribution certificate +- `IOS_PROV_PROFILE_BASE64` - Provisioning profile +- `IOS_P12_PASSWORD` - Certificate password + +#### Android +- `ANDROID_PACKAGE_NAME` - Package name +- `ANDROID_KEYSTORE` - Keystore file (base64) +- `ANDROID_KEYSTORE_PASSWORD` - Keystore password +- `ANDROID_KEY_ALIAS` - Key alias +- `ANDROID_KEY_PASSWORD` - Key password +- `ANDROID_PLAY_STORE_JSON_KEY` - Service account key + +#### Notifications +- `SLACK_API_TOKEN` - For deployment notifications +- `SLACK_CHANNEL_ID` - Channel for build uploads + +### Environment Variables + +Set in workflow files: +```yaml +NODE_VERSION: 18 +RUBY_VERSION: 3.2 +JAVA_VERSION: 17 +ANDROID_API_LEVEL: 35 +ANDROID_NDK_VERSION: 26.1.10909125 +``` + +## ๐Ÿท๏ธ Git Tags & Releases + +### Automatic Tags (Production Only) + +When deploying to production, creates: +- `v2.5.5` - Main version tag +- `v2.5.5-ios-148` - iOS with build number +- `v2.5.5-android-82` - Android with build number + +### GitHub Releases + +Automatically created for production deployments with: +- Changelog from commits +- Build information +- Links to app stores + +## ๐Ÿšจ Troubleshooting + +### Common Issues + +#### "Play Store upload failed: Insufficient permissions" +The service account needs permissions in Google Play Console. The build file is saved locally and can be uploaded manually. + +#### Cache not working +- Check if lock files changed (`yarn.lock`, `Gemfile.lock`) +- Cache keys include version numbers that can be bumped +- First build on new branch may be slower + +#### iOS build fails with provisioning profile error +- Ensure secrets are up to date +- Check certificate expiration +- Verify bundle ID matches + +#### Version conflicts +- `version.json` tracks the source of truth +- Always higher than store versions +- Automatically incremented each build + +### Build Failures + +1. Check the workflow logs in GitHub Actions +2. Look for the specific error in the failed step +3. Most issues are related to: + - Expired certificates/profiles + - Missing secrets + - Network timeouts (retry usually helps) + +## ๐Ÿ“Š Monitoring + +### Slack Notifications + +Successful deployments post to Slack with: +- Platform and version info +- Download links for the builds +- Deployment track (internal/production) + +### Deployment History + +View all deployments: +1. Go to [Actions](../../../actions) +2. Filter by workflow: "Mobile App Deployments" +3. Check run history and logs + +## ๐Ÿ” Security + +- All secrets are stored in GitHub Secrets +- Certificates are base64 encoded +- Build artifacts are uploaded to Slack (private channel) +- Production deployments only from protected branches + +## ๐Ÿ› ๏ธ Maintenance + +### Updating Workflows + +1. Test changes with `test_mode: true` +2. Use `workflow_dispatch` for manual testing +3. Monitor first automated run carefully + +### Cache Busting + +If builds are failing due to cache issues: +1. Increment cache version in workflow: + ```yaml + GH_CACHE_VERSION: v2 # Increment this + ``` + +### Certificate Renewal + +Before certificates expire: +1. Generate new certificates/profiles +2. Update GitHub Secrets +3. Test with manual deployment first + +--- + +For local development and manual release processes, see [`app/README.md`](../app/README.md) \ No newline at end of file diff --git a/.github/workflows/mobile-deploy-auto.yml b/.github/workflows/mobile-deploy-auto.yml new file mode 100644 index 000000000..277a4c7ab --- /dev/null +++ b/.github/workflows/mobile-deploy-auto.yml @@ -0,0 +1,94 @@ +name: Mobile Auto Deploy + +on: + pull_request: + types: [closed] + branches: [main, dev] + paths: + - 'app/**' + - '!app/**/*.md' + - '!app/docs/**' + +jobs: + check-and-deploy: + if: github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'no-deploy') + runs-on: ubuntu-latest + outputs: + should_deploy: ${{ steps.check.outputs.should_deploy }} + deployment_track: ${{ steps.check.outputs.deployment_track }} + version_bump: ${{ steps.check.outputs.version_bump }} + platforms: ${{ steps.check.outputs.platforms }} + + steps: + - name: Check deployment conditions + id: check + run: | + echo "๐Ÿ” Checking deployment conditions..." + + # Skip if PR has skip-deploy in title or body + if [[ "${{ github.event.pull_request.title }}" =~ \[skip-deploy\] ]] || + [[ "${{ github.event.pull_request.body }}" =~ \[skip-deploy\] ]]; then + echo "should_deploy=false" >> $GITHUB_OUTPUT + echo "โญ๏ธ Skipping deployment due to [skip-deploy] flag" + exit 0 + fi + + # Determine deployment track based on target branch + if [[ "${{ github.base_ref }}" == "main" ]]; then + echo "deployment_track=production" >> $GITHUB_OUTPUT + echo "๐Ÿš€ Deployment track: production" + elif [[ "${{ github.base_ref }}" == "dev" ]]; then + echo "deployment_track=internal" >> $GITHUB_OUTPUT + echo "๐Ÿงช Deployment track: internal testing" + fi + + # Determine version bump from PR labels + labels="${{ join(github.event.pull_request.labels.*.name, ',') }}" + if [[ "$labels" =~ version:major ]]; then + echo "version_bump=major" >> $GITHUB_OUTPUT + echo "๐Ÿ“ˆ Version bump: major" + elif [[ "$labels" =~ version:minor ]]; then + echo "version_bump=minor" >> $GITHUB_OUTPUT + echo "๐Ÿ“ˆ Version bump: minor" + elif [[ "$labels" =~ version:patch ]] || [[ "${{ github.base_ref }}" == "main" ]]; then + echo "version_bump=patch" >> $GITHUB_OUTPUT + echo "๐Ÿ“ˆ Version bump: patch" + else + echo "version_bump=build" >> $GITHUB_OUTPUT + echo "๐Ÿ“ˆ Version bump: build only" + fi + + # Always deploy both platforms for now (can be enhanced later) + echo "platforms=both" >> $GITHUB_OUTPUT + echo "should_deploy=true" >> $GITHUB_OUTPUT + + - name: Log deployment info + if: steps.check.outputs.should_deploy == 'true' + run: | + echo "๐Ÿ“ฑ Auto-deployment triggered!" + echo "Branch: ${{ github.base_ref }}" + echo "Track: ${{ steps.check.outputs.deployment_track }}" + echo "Version bump: ${{ steps.check.outputs.version_bump }}" + echo "PR: #${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}" + echo "Merged by: ${{ github.event.pull_request.merged_by.login }}" + + deploy: + needs: check-and-deploy + if: needs.check-and-deploy.outputs.should_deploy == 'true' + uses: ./.github/workflows/mobile-deploy.yml + with: + platform: ${{ needs.check-and-deploy.outputs.platforms }} + deployment_track: ${{ needs.check-and-deploy.outputs.deployment_track }} + version_bump: ${{ needs.check-and-deploy.outputs.version_bump }} + auto_deploy: true + secrets: inherit + + notify-skip: + needs: check-and-deploy + if: needs.check-and-deploy.outputs.should_deploy == 'false' + runs-on: ubuntu-latest + steps: + - name: Notify skip + run: | + echo "๐Ÿ“ฑ Mobile deployment was skipped for this PR" + echo "To deploy manually, use the 'Run workflow' button on the Mobile App Deployments workflow" \ No newline at end of file diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 194940c20..e41932fa4 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -48,6 +48,46 @@ on: required: false type: boolean default: false + deployment_track: + description: "Deployment track (internal/production)" + required: false + type: choice + default: "internal" + options: + - internal + - production + version_bump: + description: "Version bump type" + required: false + type: choice + default: "build" + options: + - build + - patch + - minor + - major + + workflow_call: + inputs: + platform: + type: string + required: true + deployment_track: + type: string + required: false + default: "internal" + version_bump: + type: string + required: false + default: "build" + auto_deploy: + type: boolean + required: false + default: false + test_mode: + type: boolean + required: false + default: false jobs: build-ios: @@ -381,12 +421,30 @@ jobs: echo "Identities in build.keychain:" security find-identity -v -p codesigning build.keychain || echo "Failed to find identities in build.keychain" echo "--- Starting Fastlane ---" - if [ "${{ github.event.inputs.test_mode }}" = "true" ]; then + + # Determine deployment track and version bump + DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}" + VERSION_BUMP="${{ inputs.version_bump || 'build' }}" + TEST_MODE="${{ inputs.test_mode || false }}" + + echo "๐Ÿ“ฑ Deployment Configuration:" + echo " - Track: $DEPLOYMENT_TRACK" + echo " - Version Bump: $VERSION_BUMP" + echo " - Test Mode: $TEST_MODE" + + if [ "$TEST_MODE" = "true" ]; then echo "๐Ÿงช Running in TEST MODE - will skip upload to TestFlight" - bundle exec fastlane ios internal_test --verbose test_mode:true + bundle exec fastlane ios deploy_auto \ + deployment_track:$DEPLOYMENT_TRACK \ + version_bump:$VERSION_BUMP \ + test_mode:true \ + --verbose else - echo "๐Ÿš€ Uploading to App Store Connect/TestFlight..." - bundle exec fastlane ios internal_test --verbose + echo "๐Ÿš€ Deploying to App Store Connect..." + bundle exec fastlane ios deploy_auto \ + deployment_track:$DEPLOYMENT_TRACK \ + version_bump:$VERSION_BUMP \ + --verbose fi # Version updates moved to separate job to avoid race conditions @@ -666,12 +724,30 @@ jobs: SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }} run: | cd ${{ env.APP_PATH }} - if [ "${{ github.event.inputs.test_mode }}" = "true" ]; then + + # Determine deployment track and version bump + DEPLOYMENT_TRACK="${{ inputs.deployment_track || 'internal' }}" + VERSION_BUMP="${{ inputs.version_bump || 'build' }}" + TEST_MODE="${{ inputs.test_mode || false }}" + + echo "๐Ÿค– Deployment Configuration:" + echo " - Track: $DEPLOYMENT_TRACK" + echo " - Version Bump: $VERSION_BUMP" + echo " - Test Mode: $TEST_MODE" + + if [ "$TEST_MODE" = "true" ]; then echo "๐Ÿงช Running in TEST MODE - will skip upload to Play Store" - bundle exec fastlane android internal_test --verbose test_mode:true + bundle exec fastlane android deploy_auto \ + deployment_track:$DEPLOYMENT_TRACK \ + version_bump:$VERSION_BUMP \ + test_mode:true \ + --verbose else - echo "๐Ÿš€ Uploading to Google Play Internal Testing..." - bundle exec fastlane android internal_test --verbose + echo "๐Ÿš€ Deploying to Google Play Store..." + bundle exec fastlane android deploy_auto \ + deployment_track:$DEPLOYMENT_TRACK \ + version_bump:$VERSION_BUMP \ + --verbose fi # Version updates moved to separate job to avoid race conditions @@ -799,3 +875,100 @@ jobs: git push echo "โœ… Committed version file changes" fi + + # Create git tags after successful deployment + create-release-tags: + needs: [build-ios, build-android, update-version-files] + if: | + always() && + needs.update-version-files.result == 'success' && + (needs.build-ios.result == 'success' || needs.build-android.result == 'success') && + (inputs.deployment_track == 'production' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deployment_track == 'production')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config --global user.name "GitHub Actions" + git config --global user.email "actions@github.com" + + - name: Create and push tags + run: | + cd ${{ env.APP_PATH }} + + # Read current version info + VERSION=$(cat package.json | jq -r .version) + IOS_BUILD=$(cat version.json | jq -r .ios.build) + ANDROID_BUILD=$(cat version.json | jq -r .android.build) + + echo "๐Ÿ“ฆ Creating tags for version $VERSION" + + # Create main version tag + if ! git tag -l | grep -q "^v${VERSION}$"; then + git tag -a "v${VERSION}" -m "Release ${VERSION}" + echo "โœ… Created tag: v${VERSION}" + else + echo "โญ๏ธ Tag v${VERSION} already exists" + fi + + # Create platform-specific tags if deployments succeeded + if [ "${{ needs.build-ios.result }}" = "success" ]; then + TAG_NAME="v${VERSION}-ios-${IOS_BUILD}" + if ! git tag -l | grep -q "^${TAG_NAME}$"; then + git tag -a "${TAG_NAME}" -m "iOS Release ${VERSION} (Build ${IOS_BUILD})" + echo "โœ… Created tag: ${TAG_NAME}" + fi + fi + + if [ "${{ needs.build-android.result }}" = "success" ]; then + TAG_NAME="v${VERSION}-android-${ANDROID_BUILD}" + if ! git tag -l | grep -q "^${TAG_NAME}$"; then + git tag -a "${TAG_NAME}" -m "Android Release ${VERSION} (Build ${ANDROID_BUILD})" + echo "โœ… Created tag: ${TAG_NAME}" + fi + fi + + # Push all tags + git push origin --tags + echo "๐Ÿš€ Tags pushed to repository" + + - name: Generate changelog for release + id: changelog + run: | + cd ${{ env.APP_PATH }} + + # Find the previous version tag + PREV_TAG=$(git tag -l "v*" | grep -v "-" | sort -V | tail -2 | head -1 || echo "") + + # Generate simple changelog + echo "## What's Changed" > release_notes.md + echo "" >> release_notes.md + + if [ -n "$PREV_TAG" ]; then + git log --pretty=format:"- %s" ${PREV_TAG}..HEAD --no-merges | grep -v "^- Merge" >> release_notes.md + else + echo "Initial release" >> release_notes.md + fi + + echo "" >> release_notes.md + echo "## Build Information" >> release_notes.md + echo "- iOS Build: ${IOS_BUILD}" >> release_notes.md + echo "- Android Build: ${ANDROID_BUILD}" >> release_notes.md + + # Set output for next step + echo "version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.changelog.outputs.version }} + name: Release ${{ steps.changelog.outputs.version }} + body_path: ${{ env.APP_PATH }}/release_notes.md + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/app/README.md b/app/README.md index 136c72650..59ed98ca6 100644 --- a/app/README.md +++ b/app/README.md @@ -183,42 +183,156 @@ export DEVELOPMENT_TEAM="" ./scripts/build_ios_module.sh ``` -## Export a new release +## ๐Ÿš€ Deployment & Release -### Android - -#### Export as apk +### Quick Commands +```bash +# View current version info +node scripts/version.cjs status + +# Create a new release (interactive) +yarn release # Patch release (1.0.0 โ†’ 1.0.1) +yarn release:minor # Minor release (1.0.0 โ†’ 1.1.0) +yarn release:major # Major release (1.0.0 โ†’ 2.0.0) + +# Deploy manually (with prompts) +yarn mobile-deploy # Deploy both platforms +yarn mobile-deploy:ios # Deploy iOS only +yarn mobile-deploy:android # Deploy Android only + +# Version management +node scripts/version.cjs bump patch # Bump version +node scripts/version.cjs bump-build ios # Increment iOS build +node scripts/version.cjs bump-build android # Increment Android build ``` -cd android -./gradlew assembleRelease + +### Automated Deployments + +Deployments happen automatically when you merge PRs: + +1. **Merge to `dev`** โ†’ Deploys to internal testing +2. **Merge to `main`** โ†’ Deploys to production + +To control versions with PR labels: +- `version:major` - Major version bump +- `version:minor` - Minor version bump +- `version:patch` - Patch version bump (default for main) +- `no-deploy` - Skip deployment + +See [CI/CD Documentation](../.github/MOBILE_DEPLOYMENT.md) for details. + +### Manual Release Process + +For hotfixes or manual releases: + +```bash +# 1. Create a release (bumps version, creates tag, generates changelog) +yarn release:patch + +# 2. Push to remote +git push && git push --tags + +# 3. Deploy via GitHub Actions (happens automatically on merge to main) ``` -The built apk it located at `android/app/build/outputs/apk/release/app-release.apk` +The release script will: +- Check for uncommitted changes +- Bump the version in package.json +- Update iOS and Android native versions +- Generate a changelog +- Create a git tag +- Optionally push everything to remote + +### Version Management + +Versions are tracked in multiple places: + +1. **`package.json`** - Semantic version (e.g., "2.5.5") +2. **`version.json`** - Platform build numbers: + ```json + { + "ios": { "build": 148 }, + "android": { "build": 82 } + } + ``` +3. **Native files** - Auto-synced during build: + - iOS: `Info.plist`, `project.pbxproj` + - Android: `build.gradle` -#### Publish on the Play Store +### Local Testing -As explained [here](https://reactnative.dev/docs/signed-apk-android), first setup `android/app/my-upload-key.keystore` and the private vars in `~/.gradle/gradle.properties`, then run: +#### Android Release Build +```bash +# Build release APK +cd android && ./gradlew assembleRelease + +# Or build AAB for Play Store +cd android && ./gradlew bundleRelease + +# Test release build on device +yarn android --mode release ``` -npx react-native build-android --mode=release + +#### iOS Release Build + +```bash +# Using Fastlane (recommended) +bundle exec fastlane ios build_local + +# Or using Xcode +# 1. Open ios/OpenPassport.xcworkspace +# 2. Product โ†’ Archive +# 3. Follow the wizard ``` -This builds `android/app/build/outputs/bundle/release/app-release.aab`. +### Troubleshooting Deployments -Then to test the release on an android phone, delete the previous version of the app and run: +#### Version Already Exists +The build system auto-increments build numbers. If you get version conflicts: +```bash +# Check current versions +node scripts/version.cjs status +# Force bump build numbers +node scripts/version.cjs bump-build ios +node scripts/version.cjs bump-build android ``` -yarn android --mode release + +#### Certificate Issues (iOS) +```bash +# Check certificate validity +bundle exec fastlane ios check_certs + +# For local development, ensure you have: +# - Valid Apple Developer account +# - Certificates in Keychain +# - Correct provisioning profiles ``` -Don't forget to bump `versionCode` in `android/app/build.gradle`. +#### Play Store Upload Issues +If automated upload fails, the AAB is saved locally: +- Location: `android/app/build/outputs/bundle/release/app-release.aab` +- Upload manually via Play Console -### iOS +### Build Optimization + +The CI/CD pipeline uses extensive caching: +- **iOS builds**: ~15 minutes (with cache) +- **Android builds**: ~10 minutes (with cache) +- **First build**: ~25 minutes (no cache) -In Xcode, go to `Product>Archive` then follow the flow. +To speed up local builds: +```bash +# Clean only what's necessary +yarn clean:build # Clean build artifacts only +yarn clean # Full clean (use sparingly) -Don't forget to bump the build number. +# Use Fastlane for consistent builds +bundle exec fastlane ios internal_test test_mode:true +bundle exec fastlane android internal_test test_mode:true +``` ## FAQ diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 1bfdfcb77..2a1e0b211 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -60,7 +60,7 @@ platform :ios do desc "Push a new build to TestFlight Internal Testing" lane :internal_test do |options| test_mode = options[:test_mode] == true || options[:test_mode] == "true" - + result = prepare_ios_build(prod_release: false) if test_mode @@ -77,7 +77,7 @@ platform :ios do skip_waiting_for_build_processing: false, ) if result[:should_upload] end - + # Update deployment timestamp in version.json if result[:should_upload] Fastlane::Helpers.update_deployment_timestamp('ios') @@ -99,6 +99,78 @@ platform :ios do end end + desc "Deploy iOS app with automatic version management" + lane :deploy_auto do |options| + deployment_track = options[:deployment_track] || 'internal' + version_bump = options[:version_bump] || 'build' + test_mode = options[:test_mode] == true || options[:test_mode] == "true" + + UI.message("๐Ÿš€ Starting iOS deployment") + UI.message(" Track: #{deployment_track}") + UI.message(" Version bump: #{version_bump}") + UI.message(" Test mode: #{test_mode}") + + # Handle version bumping + if !test_mode + require_relative "helpers/version_manager" + version_manager = Fastlane::Helpers::VersionManager.new + + case version_bump + when 'major', 'minor', 'patch' + version_manager.bump_version(version_bump) + UI.success("โœ… Bumped #{version_bump} version") + when 'build' + # Build number is automatically incremented during build + UI.message("๐Ÿ“ฆ Build number will be incremented") + end + end + + # Prepare and build + result = prepare_ios_build(prod_release: deployment_track == 'production') + + # Handle deployment based on track + if test_mode + UI.important("๐Ÿงช TEST MODE: Skipping App Store upload") + UI.success("โœ… Build completed successfully!") + elsif deployment_track == 'internal' + upload_to_testflight( + api_key: result[:api_key], + distribute_external: true, + groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), + changelog: "", + skip_waiting_for_build_processing: false, + ) if result[:should_upload] + elsif deployment_track == 'production' + # For production, upload to TestFlight first, then promote + upload_to_testflight( + api_key: result[:api_key], + distribute_external: false, + skip_waiting_for_build_processing: true, + ) if result[:should_upload] + + # TODO: Add app store submission when ready + UI.important("โš ๏ธ Production deployment uploaded to TestFlight. Manual App Store submission required.") + end + + # Update deployment info + if result[:should_upload] && !test_mode + Fastlane::Helpers.update_deployment_timestamp('ios') + end + + # Slack notification + if ENV["SLACK_CHANNEL_ID"] && !test_mode + deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Actions (Auto)" : "Local Deploy" + track_emoji = deployment_track == 'production' ? '๐Ÿš€' : '๐Ÿงช' + + Fastlane::Helpers.upload_file_to_slack( + file_path: result[:ipa_path], + channel_id: ENV["SLACK_CHANNEL_ID"], + initial_comment: "#{track_emoji} iOS v#{package_version} (Build #{result[:build_number]}) deployed to #{deployment_track} via #{deploy_source}", + title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa", + ) + end + end + private_lane :prepare_ios_build do |options| if local_development # app breaks with Xcode 16.3 @@ -139,13 +211,13 @@ platform :ios do # Get build number from version.json and increment it build_number = Fastlane::Helpers.bump_ios_build_number - + # Update Xcode project with new build number increment_build_number( build_number: build_number, xcodeproj: "ios/#{ENV["IOS_PROJECT_NAME"]}.xcodeproj" ) - + # Verify build number is higher than TestFlight Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path) Fastlane::Helpers.ios_verify_provisioning_profile @@ -227,6 +299,44 @@ platform :android do upload_android_build(track: "production") end + desc "Deploy Android app with automatic version management" + lane :deploy_auto do |options| + deployment_track = options[:deployment_track] || 'internal' + version_bump = options[:version_bump] || 'build' + test_mode = options[:test_mode] == true || options[:test_mode] == "true" + + UI.message("๐Ÿš€ Starting Android deployment") + UI.message(" Track: #{deployment_track}") + UI.message(" Version bump: #{version_bump}") + UI.message(" Test mode: #{test_mode}") + + # Handle version bumping + if !test_mode + require_relative "helpers/version_manager" + version_manager = Fastlane::Helpers::VersionManager.new + + case version_bump + when 'major', 'minor', 'patch' + version_manager.bump_version(version_bump) + UI.success("โœ… Bumped #{version_bump} version") + # Sync the new version to build.gradle + android_set_version_name( + version_name: version_manager.get_current_version, + gradle_file: android_gradle_file_path.gsub("../", ""), + ) + when 'build' + # Build number is automatically incremented during build + UI.message("๐Ÿ“ฆ Build number will be incremented") + end + end + + # Map deployment track to Play Store track + play_store_track = deployment_track == 'production' ? 'production' : 'internal' + + # Build and deploy + upload_android_build(track: play_store_track, test_mode: test_mode, deployment_track: deployment_track) + end + private_lane :upload_android_build do |options| test_mode = options[:test_mode] == true || options[:test_mode] == "true" if local_development @@ -253,7 +363,7 @@ platform :android do # Get version code from version.json and increment it version_code = Fastlane::Helpers.bump_android_build_number - + # Update build.gradle with new version code increment_version_code( version_code: version_code, @@ -288,18 +398,33 @@ platform :android do UI.success("โœ… Build completed successfully!") UI.message("๐Ÿ“ฆ AAB path: #{android_aab_path}") else - upload_to_play_store( - track: options[:track], - json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"], - package_name: ENV["ANDROID_PACKAGE_NAME"], - skip_upload_changelogs: true, - skip_upload_images: true, - skip_upload_screenshots: true, - track_promote_release_status: "completed", - aab: android_aab_path, - ) if should_upload && android_has_permissions + if should_upload + begin + upload_to_play_store( + track: options[:track], + json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"], + package_name: ENV["ANDROID_PACKAGE_NAME"], + skip_upload_changelogs: true, + skip_upload_images: true, + skip_upload_screenshots: true, + track_promote_release_status: "completed", + aab: android_aab_path, + ) + rescue => e + if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions") + UI.error("โŒ Play Store upload failed: Insufficient permissions") + UI.error("Please fix permissions in Google Play Console") + UI.important("Build saved at: #{android_aab_path}") + else + # Re-raise if it's a different error + raise e + end + end + else + UI.message("Skipping Play Store upload (should_upload: false)") + end end - + # Update deployment timestamp in version.json if should_upload Fastlane::Helpers.update_deployment_timestamp('android') @@ -307,11 +432,14 @@ platform :android do # Notify Slack about the new build if ENV["SLACK_CHANNEL_ID"] && !test_mode - deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy" + deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Actions (Auto)" : "Local Deploy" + deployment_track = options[:deployment_track] || options[:track] + track_emoji = deployment_track == 'production' ? '๐Ÿš€' : '๐Ÿงช' + Fastlane::Helpers.upload_file_to_slack( file_path: android_aab_path, channel_id: ENV["SLACK_CHANNEL_ID"], - initial_comment: "๐Ÿค– Android v#{package_version} (Build #{version_code}) deployed to #{target_platform} via #{deploy_source}", + initial_comment: "#{track_emoji} Android v#{package_version} (Build #{version_code}) deployed to #{deployment_track || target_platform} via #{deploy_source}", title: "#{APP_NAME}-#{package_version}-#{version_code}.aab", ) elsif test_mode diff --git a/app/package.json b/app/package.json index 45bf06f60..d67cc6069 100644 --- a/app/package.json +++ b/app/package.json @@ -11,6 +11,10 @@ "bump-version:major": "npm version major && yarn sync-versions", "bump-version:minor": "npm version minor && yarn sync-versions", "bump-version:patch": "npm version patch && yarn sync-versions", + "release": "./scripts/release.sh", + "release:major": "./scripts/release.sh major", + "release:minor": "./scripts/release.sh minor", + "release:patch": "./scripts/release.sh patch", "clean": "yarn clean:watchman && yarn clean:build && yarn clean:ios && yarn clean:android && yarn clean:xcode && yarn clean:pod-cache && yarn clean:node", "clean:android": "rm -rf android/app/build android/build", "clean:build": "rm -rf ios/build android/app/build android/build", diff --git a/app/scripts/generate-changelog.sh b/app/scripts/generate-changelog.sh new file mode 100755 index 000000000..8cee9a05a --- /dev/null +++ b/app/scripts/generate-changelog.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Simple changelog generator that creates release notes from git history +# Usage: ./generate-changelog.sh [from_tag] [to_tag] + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Get tags +FROM_TAG=${1:-$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")} +TO_TAG=${2:-HEAD} + +if [ -z "$FROM_TAG" ]; then + echo "No previous tag found. Generating changelog from beginning..." + FROM_TAG=$(git rev-list --max-parents=0 HEAD) +fi + +echo -e "${YELLOW}Generating changelog from $FROM_TAG to $TO_TAG...${NC}" + +# Get current version from package.json +VERSION=$(cat package.json | jq -r .version 2>/dev/null || echo "Unknown") +DATE=$(date +"%Y-%m-%d") + +# Start changelog +CHANGELOG="## Release v${VERSION} (${DATE})\n\n" + +# Group commits by type +FEATURES="" +FIXES="" +CHORES="" +OTHER="" + +# Process commits +while IFS= read -r line; do + HASH=$(echo "$line" | cut -d' ' -f1) + MESSAGE=$(echo "$line" | cut -d' ' -f2-) + + # Skip merge commits + if [[ "$MESSAGE" =~ ^Merge ]]; then + continue + fi + + # Categorize commits + if [[ "$MESSAGE" =~ ^feat ]]; then + FEATURES="${FEATURES}- ${MESSAGE}\n" + elif [[ "$MESSAGE" =~ ^fix ]]; then + FIXES="${FIXES}- ${MESSAGE}\n" + elif [[ "$MESSAGE" =~ ^chore ]]; then + CHORES="${CHORES}- ${MESSAGE}\n" + else + OTHER="${OTHER}- ${MESSAGE}\n" + fi +done < <(git log --oneline --no-merges ${FROM_TAG}..${TO_TAG}) + +# Build changelog sections +if [ -n "$FEATURES" ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿš€ Features\n${FEATURES}\n" +fi + +if [ -n "$FIXES" ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ› Bug Fixes\n${FIXES}\n" +fi + +if [ -n "$CHORES" ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ”ง Maintenance\n${CHORES}\n" +fi + +if [ -n "$OTHER" ]; then + CHANGELOG="${CHANGELOG}### ๐Ÿ“ Other Changes\n${OTHER}\n" +fi + +# Add deployment info +CHANGELOG="${CHANGELOG}### ๐Ÿ“ฑ Deployment Info\n" +CHANGELOG="${CHANGELOG}- iOS Build: $(cat version.json | jq -r .ios.build)\n" +CHANGELOG="${CHANGELOG}- Android Build: $(cat version.json | jq -r .android.build)\n" + +# Output to file +echo -e "$CHANGELOG" > RELEASE_NOTES.md +echo -e "${GREEN}โœ… Changelog generated in RELEASE_NOTES.md${NC}" + +# Also output to console +echo -e "\n${YELLOW}Release Notes:${NC}" +echo -e "$CHANGELOG" \ No newline at end of file diff --git a/app/scripts/release.sh b/app/scripts/release.sh new file mode 100755 index 000000000..ccc031a92 --- /dev/null +++ b/app/scripts/release.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Simple release script for manual version bumping and tagging +# Usage: ./release.sh [major|minor|patch] + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if we're in the app directory +if [ ! -f "package.json" ]; then + echo -e "${RED}Error: Must run from app directory${NC}" + exit 1 +fi + +# Check for uncommitted changes +if ! git diff-index --quiet HEAD --; then + echo -e "${RED}Error: You have uncommitted changes. Please commit or stash them first.${NC}" + exit 1 +fi + +# Get version bump type +BUMP_TYPE=${1:-patch} +if [[ ! "$BUMP_TYPE" =~ ^(major|minor|patch)$ ]]; then + echo -e "${RED}Error: Invalid version bump type. Use: major, minor, or patch${NC}" + exit 1 +fi + +echo -e "${YELLOW}๐Ÿš€ Starting release process...${NC}" +echo "Version bump type: $BUMP_TYPE" + +# Get current version +CURRENT_VERSION=$(cat package.json | jq -r .version) +echo "Current version: $CURRENT_VERSION" + +# Bump version using existing script +echo -e "\n${YELLOW}1. Bumping version...${NC}" +node scripts/version.cjs bump $BUMP_TYPE + +# Get new version +NEW_VERSION=$(cat package.json | jq -r .version) +echo -e "${GREEN}โœ… Version bumped: $CURRENT_VERSION โ†’ $NEW_VERSION${NC}" + +# Sync native versions +echo -e "\n${YELLOW}2. Syncing native versions...${NC}" +cd .. # Go to workspace root for Fastlane +bundle exec fastlane ios sync_version +bundle exec fastlane android sync_version +cd app # Back to app directory + +# Generate changelog +echo -e "\n${YELLOW}3. Generating changelog...${NC}" +./scripts/generate-changelog.sh +echo -e "${GREEN}โœ… Changelog generated${NC}" + +# Stage all version-related files +echo -e "\n${YELLOW}4. Committing changes...${NC}" +git add package.json version.json RELEASE_NOTES.md +git add ios/Self.xcodeproj/project.pbxproj ios/OpenPassport/Info.plist +git add android/app/build.gradle + +# Create commit +git commit -m "chore: release v${NEW_VERSION} + +- Bump version from ${CURRENT_VERSION} to ${NEW_VERSION} +- Update iOS and Android native versions +- Sync build numbers across platforms" + +echo -e "${GREEN}โœ… Changes committed${NC}" + +# Create tags +echo -e "\n${YELLOW}5. Creating git tags...${NC}" +git tag -a "v${NEW_VERSION}" -m "Release ${NEW_VERSION}" +echo -e "${GREEN}โœ… Created tag: v${NEW_VERSION}${NC}" + +# Summary +echo -e "\n${GREEN}๐ŸŽ‰ Release prepared successfully!${NC}" +echo -e "\nNext steps:" +echo -e " 1. Review the changes: ${YELLOW}git show HEAD${NC}" +echo -e " 2. Push to remote: ${YELLOW}git push && git push --tags${NC}" +echo -e " 3. Deploy via GitHub Actions (will happen automatically on merge to main)" + +# Ask if user wants to push now +echo -e "\n${YELLOW}Push changes and tags now? (y/N)${NC}" +read -r response +if [[ "$response" =~ ^[Yy]$ ]]; then + git push && git push --tags + echo -e "${GREEN}โœ… Pushed to remote!${NC}" + echo -e "\n๐Ÿš€ Release v${NEW_VERSION} is ready for deployment!" +else + echo -e "${YELLOW}Changes not pushed. Run 'git push && git push --tags' when ready.${NC}" +fi \ No newline at end of file diff --git a/app/version.json b/app/version.json index c2c790995..16b8f84c1 100644 --- a/app/version.json +++ b/app/version.json @@ -1,6 +1,6 @@ { "ios": { - "build": 149, + "build": 150, "lastDeployed": null }, "android": {