0
\$\begingroup\$

Mobile development is quite new for me, and I would like to ask about a solution review. I'm not sure the way I publish the application is correct, I think good companies may do it differently.

I have two environments: development and production. For each environment there are different URLs for API services. I publish the application to App Store and to Google Play store using the Azure DevOps pipeline. Each environment version uses different .env.* file with configuration.

Versions and short versions are the same for both platforms so that they follow the rules of both stores (with combined rules for both stores both versions must be numbers separated by dots and must be unique):

  • develop:
    • short version (version name): {year}{month}{day}.{dayCounter}.0
    • version (version code): 2 * buildId
  • production:
    • short version (version name): {year}{month}{day}.{dayCounter}.1
    • version (version code): 2 * buildId + 1

The pipeline has two stages:

  • development build
  • production build - asking for a permission to run

A tester is first testing a development version and when it works - they approve a production stage and then test it. Production stage does not have to be approved and run - it depends of changes on backend services and if they were already merged to main branches. When tests are passed - production version is promoted to production (development version is never promoted, it is only in internal tests phase).

For each platform tester is using:

  • iOS: TestFlight to install a specific version of the app
  • Android: URL copied from Google Play Console for a specific version of the app

My pipeline YAML:

trigger:
  - main

variables:
  - group: mobileApps
  - name: version
    value: $[format('{0:yyyyMMdd}', pipeline.startTime)]
  - name: revision
    value: $[counter(variables['version'], 1)]
  - name: versionNameTest
    value: $[format('{0}.{1}.0', variables['version'], variables['revision'])]
  - name: versionNameProd
    value: $[format('{0}.{1}.1', variables['version'], variables['revision'])]
  - name: buildId
    value: $[variables['Build.BuildId']]  

stages:
  - stage:
    displayName: Test version
    jobs:
    - job: DisplayVersions
      displayName: Display versions
      steps:
        - script: |            
            echo versionName=$(versionNameTest)
            echo versionCode=$((2 * $(buildId)))
          displayName: Display versions

    - job: IOS
      displayName: iOS bundle release
      dependsOn: DisplayVersions
      pool:
        vmImage: "macos-latest"
      steps:
        - script: |
            /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(versionNameTest)" ios/SomeFolder/Info.plist
            /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $((2 * $(buildId)))" ios/SomeFolder/Info.plist
            cat ios/SomeFolder/Info.plist
          displayName: Set versions in Info.plist
          
        - script: |
            npm install
          displayName: Install node modules

        - task: CocoaPods@0
          inputs:
            workingDirectory: 'ios/'
            forceRepoUpdate: false
          displayName: Install pods

        - task: InstallAppleCertificate@2
          inputs:
            certSecureFile: 'someName.p12' 
            # kv password not working, reading stars?
            certPwd: 'some password'
        
        - task: InstallAppleProvisioningProfile@1
          inputs:
            provProfileSecureFile: 'someName.mobileprovision'

        - script: |
            rm .env.production
          displayName: Use .env config

        - task: Xcode@5
          inputs:
            actions: 'build'
            scheme: 'SomeScheme'
            sdk: 'iphoneos'
            configuration: 'Release'
            xcWorkspacePath: 'ios/someName.xcworkspace'
            packageApp: true
            exportPath: '.'
            signingOption: 'manual'
            signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)'
            provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)'

        - task: CopyFiles@2
          inputs:
            contents: "**/*.ipa"
            targetFolder: "$(build.artifactStagingDirectory)"
          displayName: Copy iOS bundle to artifacts

        - task: PublishBuildArtifacts@1
          displayName: Publish artifacts

        - task: AppStoreRelease@1
          displayName: 'Publish to the App Store TestFlight track'
          inputs:
            serviceEndpoint: 'Apple App Store service connection'
            appIdentifier: com.some-name.some-name
            ipaPath: '$(build.artifactstagingdirectory)/**/*.ipa'
            shouldSkipWaitingForProcessing: true
            shouldSkipSubmission: true

    - job: Android
      displayName: Android bundle release
      dependsOn: DisplayVersions #IOS
      pool:
        vmImage: "ubuntu-latest"
      steps:
        - script: |
            npm install
          displayName: Install node modules

        - script: |
            keystorePath=$(pwd)/infrastructure/keystores/upload-keystore.jks
            cd android
            chmod +x gradlew

            ENVFILE=.env ./gradlew \
              -Pandroid.injected.signing.store.file="$keystorePath" \
              -Pandroid.injected.signing.store.password=$(java-key-store-password) \
              -Pandroid.injected.signing.key.alias=$(java-key-store-alias) \
              -Pandroid.injected.signing.key.password=$(java-key-store-password) \
              -PversionName=$(versionNameTest) \
              -PversionCode=$((2 * $(buildId))) \
              bundleRelease
          displayName: Build Android bundle

        - task: CopyFiles@2
          inputs:
            contents: "android/app/build/outputs/bundle/release/*.aab"
            targetFolder: "$(build.artifactStagingDirectory)"
          displayName: Copy Android bundle to artifacts

        - task: PublishBuildArtifacts@1
          displayName: Publish artifacts

        - task: GooglePlayRelease@4
          inputs:
            serviceConnection: "Some Service Account"
            applicationId: "com.some-name.some-name"
            action: "SingleBundle"
            bundleFile: "android/app/build/outputs/bundle/release/*.aab"
            track: "internal"
            releaseName: "$(versionNameTest)"
            changesNotSentForReview: true
          displayName: Send Android bundle to Google Play for internal testing

  - stage:
    displayName: Prod version
    jobs:
    - job: DisplayVersions
      displayName: Display versions
      steps:
        - script: |
            echo versionName=$(versionNameProd)
            echo versionCode=$((2 * $(buildId) + 1))
          displayName: Display versions

    - deployment: IOS
      displayName: iOS bundle release
      dependsOn: DisplayVersions
      pool:
        vmImage: "macos-latest"
      environment: SomeEnvironment
      strategy:
        runOnce:
          deploy:
            steps:
            - checkout: self

            - script: |
                /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(versionNameProd)" ios/SomeFolder/Info.plist
                /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $((2 * $(buildId) + 1))" ios/SomeName/Info.plist
                cat ios/SomeFolder/Info.plist
              displayName: Set versions in Info.plist
          
            - script: |
                npm install
              displayName: Install node modules

            - task: CocoaPods@0
              inputs:
                workingDirectory: 'ios/'
                forceRepoUpdate: false
              displayName: Install pods

            - task: InstallAppleCertificate@2
              inputs:
                certSecureFile: 'someName.p12' 
                # kv password not working, reading stars?
                certPwd: 'some-password'
            
            - task: InstallAppleProvisioningProfile@1
              inputs:
                provProfileSecureFile: 'someName.mobileprovision'

            - script: |
                mv -f .env.production .env
              displayName: Use .env.production config

            - task: Xcode@5
              inputs:
                actions: 'build'
                scheme: 'SomeScheme'
                sdk: 'iphoneos'
                configuration: 'Release'
                xcWorkspacePath: 'ios/someName.xcworkspace'
                packageApp: true
                exportPath: '.'
                signingOption: 'manual'
                signingIdentity: '$(APPLE_CERTIFICATE_SIGNING_IDENTITY)'
                provisioningProfileUuid: '$(APPLE_PROV_PROFILE_UUID)'

            - task: CopyFiles@2
              inputs:
                contents: "**/*.ipa"
                targetFolder: "$(build.artifactStagingDirectory)"
              displayName: Copy iOS bundle to artifacts

            - task: PublishBuildArtifacts@1
              displayName: Publish artifacts

            - task: AppStoreRelease@1
              displayName: 'Publish to the App Store TestFlight track'
              inputs:
                serviceEndpoint: 'Apple App Store service connection'
                appIdentifier: com.some-name.some-name
                ipaPath: '$(build.artifactstagingdirectory)/**/*.ipa'
                shouldSkipWaitingForProcessing: true
                shouldSkipSubmission: true

    - deployment: Android
      displayName: Android bundle release
      dependsOn: DisplayVersions #IOS
      pool:
        vmImage: "ubuntu-latest"
      environment: SomeEnvironment
      strategy:
        runOnce:
          deploy:
            steps:
            - checkout: self

            - script: |
                npm install
              displayName: Install node modules

            - script: |
                keystorePath=$(pwd)/infrastructure/keystores/upload-keystore.jks
                cd android
                chmod +x gradlew

                ENVFILE=.env.production ./gradlew \
                  -Pandroid.injected.signing.store.file="$keystorePath" \
                  -Pandroid.injected.signing.store.password=$(java-key-store-password) \
                  -Pandroid.injected.signing.key.alias=$(java-key-store-alias) \
                  -Pandroid.injected.signing.key.password=$(java-key-store-password) \
                  -PversionName=$(versionNameProd) \
                  -PversionCode=$((2 * $(buildId) + 1)) \
                  bundleRelease
              displayName: Build Android bundle

            - task: CopyFiles@2
              inputs:
                contents: "android/app/build/outputs/bundle/release/*.aab"
                targetFolder: "$(build.artifactStagingDirectory)"
              displayName: Copy Android bundle to artifacts

            - task: PublishBuildArtifacts@1
              displayName: Publish artifacts

            - task: GooglePlayRelease@4
              inputs:
                serviceConnection: "Some Service Account"
                applicationId: "com.some-name.some-name"
                action: "SingleBundle"
                bundleFile: "android/app/build/outputs/bundle/release/*.aab"
                track: "internal"
                releaseName: "$(versionNameProd)"
                changesNotSentForReview: true
              displayName: Send Android bundle to Google Play for internal testing
\$\endgroup\$
1
  • \$\begingroup\$ I added new application for each environment with different schemes/variants and configurations. \$\endgroup\$ Commented Jan 26 at 15:18

0

Browse other questions tagged or ask your own question.