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