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:
- develop:
- short version: {year}{month}{day}.{dayCounter}.0
- version: 2 * buildId
- production:
- short version: {year}{month}{day}.{dayCounter}.1
- version: 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 interval 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