diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml index 40dfc05e5448..aadf433fba2f 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml +++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml @@ -17,12 +17,18 @@ inputs: ANDROID: description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + ANDROID_HYBRID: + description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true DESKTOP: description: "Desktop job result ('success', 'failure', 'cancelled', or 'skipped')" required: true IOS: description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + IOS_HYBRID: + description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true WEB: description: "Web job result ('success', 'failure', 'cancelled', or 'skipped')" required: true diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 62d326c9af3a..c67f38ca1f3e 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -12710,8 +12710,10 @@ async function run() { const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); const version = core.getInput('DEPLOY_VERSION', { required: true }); const androidResult = getDeployTableMessage(core.getInput('ANDROID', { required: true })); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', { required: true })); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true })); const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true })); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', { required: true })); const webResult = getDeployTableMessage(core.getInput('WEB', { required: true })); const date = core.getInput('DATE'); const note = core.getInput('NOTE'); @@ -12724,6 +12726,7 @@ async function run() { message += `šŸš€`; message += `\n\nplatform | result\n---|---\nšŸ¤– android šŸ¤–|${androidResult}\nšŸ–„ desktop šŸ–„|${desktopResult}`; message += `\nšŸŽ iOS šŸŽ|${iOSResult}\nšŸ•ø web šŸ•ø|${webResult}`; + message += `\nšŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|${androidHybridResult}\nšŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len message += diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 9c2defebd01d..ba61c31a6bb2 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -51,8 +51,10 @@ async function run() { const version = core.getInput('DEPLOY_VERSION', {required: true}); const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true}) as PlatformResult); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', {required: true}) as PlatformResult); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true}) as PlatformResult); const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', {required: true}) as PlatformResult); const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult); const date = core.getInput('DATE'); @@ -67,6 +69,7 @@ async function run() { message += `šŸš€`; message += `\n\nplatform | result\n---|---\nšŸ¤– android šŸ¤–|${androidResult}\nšŸ–„ desktop šŸ–„|${desktopResult}`; message += `\nšŸŽ iOS šŸŽ|${iOSResult}\nšŸ•ø web šŸ•ø|${webResult}`; + message += `\nšŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|${androidHybridResult}\nšŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index ca9a128e848d..04ce28792724 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -101,3 +101,63 @@ jobs: uses: ./.github/actions/composite/announceFailedWorkflowInSlack with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + createNewHybridVersion: + runs-on: macos-latest + needs: [validateActor, createNewVersion] + if: ${{ fromJSON(needs.validateActor.outputs.HAS_WRITE_ACCESS) }} + defaults: + run: + working-directory: Mobile-Expensify + steps: + - name: Run turnstyle + uses: softprops/turnstyle@49108bdfa571e62371bd2c3094893c547ab3fc03 + with: + poll-interval-seconds: 10 + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} + + - name: Update submodule + run: | + cd react-native + git submodule update --init + + - name: Setup git for OSBotify + uses: ./.github/actions/composite/setupGitForOSBotify + id: setupGitForOSBotify + with: + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Generate version + run: | + SHORT_APP_VERSION=$(echo "$NEW_VERSION" | awk -F'-' '{print $1}') + BUILD_NUMBER=$(echo "$NEW_VERSION" | awk -F'-' '{print $2}') + tools/buildtools/bump-version-automatically.sh "$SHORT_APP_VERSION" "$BUILD_NUMBER" + env: + NEW_VERSION: ${{ needs.createNewVersion.outputs.NEW_VERSION }} + + - name: Commit new version + run: | + git add \ + ./Android/AndroidManifest.xml \ + ./app/config/config.json \ + ./iOS/Expensify/Expensify-Info.plist\ + ./iOS/SmartScanExtension/Info.plist \ + git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + + - name: Update main branch + run: git push origin main + + - name: Announce failed workflow in Slack + if: ${{ failure() }} + uses: ./.github/actions/composite/announceFailedWorkflowInSlack + with: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4ff1a2004d8f..5790348f28c7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -163,6 +163,133 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + android_hybrid: + name: Build and deploy Android HybridApp + needs: prep + runs-on: ubuntu-latest-xl + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + defaults: + run: + working-directory: Mobile-Expensify/react-native + env: + RUBYOPT: '-rostruct' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Update submodule + run: | + git submodule update --init + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - uses: actions/setup-node@v4 + with: + node-version-file: 'Mobile-Expensify/react-native/.nvmrc' + cache: npm + cache-dependency-path: 'Mobile-Expensify/react-native' + + - name: Install node modules + run: | + npm install + cd .. && npm install + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: '17' + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + working-directory: 'Mobile-Expensify/react-native' + + - name: Install New Expensify Gems + run: bundle install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./upload-key.keystore upload-key.keystore + op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json + # Copy the keystore to the Android directory for Fullstory + cp ./upload-key.keystore ../Android + + - name: Load Android upload keystore credentials from 1Password + id: load-credentials + uses: 1password/load-secrets-action@v2 + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD + ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS + ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD + + - name: Get Android native version + id: getAndroidVersion + run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" + + - name: Build Android app + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane android build_hybrid + env: + ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + + - name: Upload Android app to Google Play + run: bundle exec fastlane android upload_google_play_internal_hybrid + env: + VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + + - name: Upload Android build to Browser Stack + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.aabPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload Android build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-build-artifact + path: ${{ env.aabPath }} + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Warn deployers if Android production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `šŸ’„ Android HybridApp production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . šŸ’„`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + desktop: name: Build and deploy Desktop needs: prep @@ -329,6 +456,154 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + iOS_hybrid: + name: Build and deploy iOS HybridApp + needs: prep + runs-on: macos-13-xlarge + env: + DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + defaults: + run: + working-directory: Mobile-Expensify/react-native + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Update submodule + run: | + git submodule update --init + + - name: Configure MapBox SDK + run: | + ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - uses: actions/setup-node@v4 + id: setup-node + with: + node-version-file: 'Mobile-Expensify/react-native/.nvmrc' + cache-dependency-path: 'Mobile-Expensify/react-native' + + - name: Install node modules + run: | + npm install + cd .. && npm install + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + working-directory: 'Mobile-Expensify/react-native' + + - name: Install New Expensify Gems + run: bundle install + + - name: Cache Pod dependencies + uses: actions/cache@v4 + id: pods-cache + with: + path: ios/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} + + - name: Compare Podfile.lock and Manifest.lock + id: compare-podfile-and-manifest + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Install cocoapods + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' + with: + timeout_minutes: 10 + max_attempts: 5 + command: cd Mobile-Expensify/iOS && pod install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./OldApp_AppStore.mobileprovision OldApp_AppStore + op document get --output ./OldApp_AppStore_Share_Extension.mobileprovision OldApp_AppStore_Share_Extension + + - name: Decrypt AppStore profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt AppStore Notification Service profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt certificate + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt App Store Connect API key + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Get iOS native version + id: getIOSVersion + run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" + + - name: Build iOS HybridApp + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios build_hybrid + + - name: Upload release build to TestFlight + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios upload_testflight_hybrid + env: + APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} + APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }} + APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} + APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} + + - name: Upload iOS build to Browser Stack + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@${{ env.ipaPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload iOS build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: ios-build-artifact + path: ${{ env.ipaPath }} + + - name: Warn deployers if iOS production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `šŸ’„ iOS HybridApp production deploy failed. Please manually submit ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the . šŸ’„`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + web: name: Build and deploy Web needs: prep @@ -425,23 +700,12 @@ jobs: with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - # Build a version of iOS and Android HybridApp if we are deploying to staging - hybridApp: - runs-on: ubuntu-latest - needs: prep - if: ${{ github.ref == 'refs/heads/staging' }} - steps: - - name: 'Deploy HybridApp' - run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true -f build_version="${{ needs.prep.outputs.APP_VERSION }}" - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - checkDeploymentSuccess: runs-on: ubuntu-latest outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform @@ -457,8 +721,10 @@ jobs: isAtLeastOnePlatformDeployed="true" fi fi - + if [ "${{ needs.iOS.result }}" == "success" ] || \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] || \ + [ "${{ needs.android_hybrid.result }}" == "success" ] || \ [ "${{ needs.desktop.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" @@ -471,6 +737,8 @@ jobs: run: | isAllPlatformsDeployed="false" if [ "${{ needs.iOS.result }}" == "success" ] && \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] && \ + [ "${{ needs.android_hybrid.result }}" == "success" ] && \ [ "${{ needs.desktop.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" @@ -521,10 +789,12 @@ jobs: gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \ ./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./android-build-artifact/app-production-release.aab \ + ./android-build-artifact/Expensify-release.aab#Android-HybridApp.aab \ ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./desktop-staging-build-artifact/NewExpensify.dmg#NewExpensifyStaging.dmg \ ./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./ios-build-artifact/New\ Expensify.ipa \ + ./ios-build-artifact/Expensify.ipa#iOS-HybridApp.ipa \ ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./web-staging-build-tar-gz-artifact/webBuild.tar.gz#stagingWebBuild.tar.gz \ ./web-staging-build-zip-artifact/webBuild.zip#stagingWebBuild.zip @@ -605,7 +875,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -659,11 +929,13 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} + android_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} + ios_hybrid: ${{ needs.iOS_hybrid.result }} web: ${{ needs.web.result }} desktop: ${{ needs.desktop.result }} diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml index 3893d3cf3f7c..ca138be0888b 100644 --- a/.github/workflows/postDeployComments.yml +++ b/.github/workflows/postDeployComments.yml @@ -15,10 +15,18 @@ on: description: Android deploy status required: true type: string + android_hybrid: + description: Android HybridApp deploy status + required: true + type: string ios: description: iOS deploy status required: true type: string + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: string web: description: Web deploy status required: true @@ -49,6 +57,15 @@ on: - failure - cancelled - skipped + android_hybrid: + description: Android HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped ios: description: iOS deploy status required: true @@ -58,6 +75,15 @@ on: - failure - cancelled - skipped + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped web: description: Web deploy status required: true @@ -110,9 +136,11 @@ jobs: IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }} DEPLOY_VERSION: ${{ inputs.version }} GITHUB_TOKEN: ${{ github.token }} + ANDROID_HYBRID: ${{ inputs.android_hybrid }} ANDROID: ${{ inputs.android }} DESKTOP: ${{ inputs.desktop }} IOS: ${{ inputs.ios }} + IOS_HYBRID: ${{ inputs.ios_hybrid }} WEB: ${{ inputs.web }} DATE: ${{ inputs.date }} NOTE: ${{ inputs.note }} diff --git a/Gemfile.lock b/Gemfile.lock index 00232570d5de..35920fc3e988 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,20 +20,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.948.0) - aws-sdk-core (3.199.0) + aws-partitions (1.979.0) + aws-sdk-core (3.209.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.87.0) - aws-sdk-core (~> 3, >= 3.199.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.154.0) - aws-sdk-core (~> 3, >= 3.199.0) + aws-sdk-kms (1.94.0) + aws-sdk-core (~> 3, >= 3.207.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.166.0) + aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -90,7 +90,7 @@ GEM ethon (0.16.0) ffi (>= 1.15.0) excon (0.111.0) - faraday (1.10.3) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -116,7 +116,7 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) fastlane (2.222.0) @@ -187,7 +187,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -208,14 +208,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.7.2) - jwt (2.8.2) + jwt (2.9.1) base64 mime-types (3.5.1) mime-types-data (~> 3.2015) @@ -241,8 +241,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.9) - strscan + rexml (3.3.7) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -256,7 +255,6 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -270,15 +268,15 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/fastlane/Appfile b/fastlane/Appfile index 66955822aab7..43c8da9bddd5 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -3,3 +3,7 @@ apple_id("ios@expensify.com") # Your Apple email address itc_team_id("152696") # App Store Connect Team ID team_id("368M544MTT") # Developer Portal Team ID + +for_lane :build_hybrid, :build_unsigned_hybrid, :upload_testflight_hybrid do + app_identifier("com.expensify.expensifylite") +end diff --git a/fastlane/Fastfile b/fastlane/Fastfile index eed84acdc916..dfaa3920491a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,6 +68,23 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a production HybridApp AAB" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + gradle( + project_dir: '../Android', + task: "bundleRelease", + flags: "--refresh-dependencies", + properties: { + "android.injected.signing.store.file" => './upload-key.keystore', + "android.injected.signing.store.password" => ENV["ANDROID_UPLOAD_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["ANDROID_UPLOAD_KEYSTORE_ALIAS"], + "android.injected.signing.key.password" => ENV["ANDROID_UPLOAD_KEY_PASSWORD"], + } + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK" lane :build_local do ENV["ENVFILE"]=".env.production" @@ -80,6 +97,18 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a new local HybridApp APK" + lane :build_local_hybrid do + ENV["ENVFILE"]=".env.production" + gradle( + project_dir: '../Android', + task: 'assemble', + flavor: 'Production', + build_type: 'Release', + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK for e2e testing" lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" @@ -151,6 +180,19 @@ platform :android do ) end + desc "Upload HybridApp to Google Play for internal testing" + lane :upload_google_play_internal_hybrid do + # Google is very unreliable, so we retry a few times + ENV["SUPPLY_UPLOAD_MAX_RETRIES"]="5" + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + aab: ENV[KEY_GRADLE_AAB_PATH], + track: 'alpha', + rollout: '1.0' + ) + end + desc "Deploy app to Google Play production" lane :upload_google_play_production do # Google is very unreliable, so we retry a few times @@ -228,6 +270,37 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp production build" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AppStore.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AppStore_Share_Extension.mobileprovision" + ) + + build_app( + workspace: "../iOS/Expensify.xcworkspace", + scheme: "Expensify", + output_name: "Expensify.ipa", + export_method: "app-store", + export_options: { + manageAppVersionAndBuildNumber: false, + provisioningProfiles: { + "com.expensify.expensifylite" => "(OldApp) AppStore", + "com.expensify.expensifylite.SmartScanExtension" => "(OldApp) AppStore: Share Extension" + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -238,6 +311,16 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an unsigned iOS HybridApp production build" + lane :build_unsigned_hybrid do + ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + build_app( + workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + scheme: "Expensify" + ) + setIOSBuildOutputsInEnv() + end + desc "Build AdHoc app for testing" lane :build_adhoc do ENV["ENVFILE"]=".env.adhoc" @@ -316,6 +399,39 @@ platform :ios do ) end + desc "Upload HybridApp to TestFlight" + lane :upload_testflight_hybrid do + upload_to_testflight( + api_key_path: "./ios/ios-fastlane-json-key.json", + distribute_external: true, + notify_external_testers: true, + changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", + groups: ["Beta"], + demo_account_required: true, + beta_app_review_info: { + contact_email: ENV["APPLE_CONTACT_EMAIL"], + contact_first_name: "Andrew", + contact_last_name: "Gable", + contact_phone: ENV["APPLE_CONTACT_PHONE"], + demo_account_name: ENV["APPLE_DEMO_EMAIL"], + demo_account_password: ENV["APPLE_DEMO_PASSWORD"], + notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me' + 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above + 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify' + 4. Open the email and copy the 6-digit sign-in code provided within + 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field" + } + ) + + puts "dsym path: #{ENV[KEY_DSYM_PATH]}" + upload_symbols_to_crashlytics( + app_id: "1:1008697809946:ios:3ffad71f664f2886", + dsym_path: ENV[KEY_DSYM_PATH], + gsp_path: "./ios/GoogleService-Info.plist", + binary_path: "./ios/Pods/FirebaseCrashlytics/upload-symbols" + ) + end + desc "Submit app to App Store Review" lane :submit_for_review do deliver( diff --git a/tests/unit/markPullRequestsAsDeployedTest.ts b/tests/unit/markPullRequestsAsDeployedTest.ts index 45fa83a36734..0118f42f6554 100644 --- a/tests/unit/markPullRequestsAsDeployedTest.ts +++ b/tests/unit/markPullRequestsAsDeployedTest.ts @@ -80,7 +80,9 @@ function mockGetInputDefaultImplementation(key: string): boolean | string { case 'DEPLOY_VERSION': return version; case 'IOS': + case 'IOS_HYBRID': case 'ANDROID': + case 'ANDROID_HYBRID': case 'DESKTOP': case 'WEB': return 'success'; @@ -88,7 +90,7 @@ function mockGetInputDefaultImplementation(key: string): boolean | string { case 'NOTE': return ''; default: - throw new Error('Trying to access invalid input'); + throw new Error(`Trying to access invalid input: ${key}`); } } @@ -196,7 +198,9 @@ platform | result šŸ¤– android šŸ¤–|success āœ… šŸ–„ desktop šŸ–„|success āœ… šŸŽ iOS šŸŽ|success āœ… -šŸ•ø web šŸ•ø|success āœ…`, +šŸ•ø web šŸ•ø|success āœ… +šŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|success āœ… +šŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|success āœ…`, issue_number: PR.issue_number, owner: 'Expensify', repo: 'App', @@ -226,7 +230,9 @@ platform | result šŸ¤– android šŸ¤–|success āœ… šŸ–„ desktop šŸ–„|success āœ… šŸŽ iOS šŸŽ|success āœ… -šŸ•ø web šŸ•ø|success āœ…`, +šŸ•ø web šŸ•ø|success āœ… +šŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|success āœ… +šŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|success āœ…`, issue_number: PRList[i + 1].issue_number, owner: 'Expensify', repo: 'App', @@ -289,6 +295,8 @@ platform | result šŸ–„ desktop šŸ–„|success āœ… šŸŽ iOS šŸŽ|success āœ… šŸ•ø web šŸ•ø|success āœ… +šŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|success āœ… +šŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|success āœ… @Expensify/applauseleads please QA this PR and check it off on the [deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) if it passes.`, issue_number: 3, @@ -325,7 +333,9 @@ platform | result šŸ¤– android šŸ¤–|skipped šŸš« šŸ–„ desktop šŸ–„|cancelled šŸ”Ŗ šŸŽ iOS šŸŽ|failed āŒ -šŸ•ø web šŸ•ø|success āœ…`, +šŸ•ø web šŸ•ø|success āœ… +šŸ¤–šŸ”„ android HybridApp šŸ¤–šŸ”„|success āœ… +šŸŽšŸ”„ iOS HybridApp šŸŽšŸ”„|success āœ…`, issue_number: PR.issue_number, owner: 'Expensify', repo: 'App',