Using Jenkins and Fastlane, we will build applications on iOS and Android, send artifacts to Slack, and also automatically send an iOS application to Testflight. The build is configured from the develop and release branches, and reads the release version (major and minor) from them, and adds the build number.
For example: the branch is release/1.0 and the Jenkins number of build 25, then the application version will be 1.0.25
For the building will be needed:
For convenience, my application is everywhere called: "my_mobile_app"
jenkinsfile:
pipeline { agent none parameters { booleanParam(name: 'del_cache', defaultValue: false, description: 'Toggle this value to build with cache clearing.') } environment { REPO = 'https://[email protected]/artem/my_mobile_app.git' REPO_CRED = 'artem-jenkins-bitbucket' IOS_KEYCHAIN = 'my_mobile_app-db' SLACK_TOKEN = 'my-slack-token' SLACK_CHANNEL = 'artem-debug' } options { ansiColor('xterm') timeout(time: 60, unit:'MINUTES') timestamps() } stages { stage('Parallel Stage test') { parallel { stage('Android.') { agent any stages { stage('Android. Define versions') { steps { script { BR_VER = sh(script: "echo ${BRANCH_NAME} | cut -d '/' -f 2 | tr -d \"\n\"", returnStdout: true) VERS_FULL = "${BR_VER}.${BUILD_NUMBER}" } } } stage ('Clearing cache.') { when { expression { return params.del_cache } } steps { deleteDir() } } stage('Android. Get code') { steps { echo "Get code for android." git branch: "${BRANCH_NAME}", credentialsId: "${REPO_CRED}", url: "${REPO}" } } stage ('Android. Build') { agent { docker { image 'repo.artem.services/android_studio/base-image-jenkins:latest' args "--name my_mobile_app-worker-${BUILD_NUMBER} \ -e RELEASE_KEYSTORE_FILE=\"my_mobile_app-release-keystore\" \ -e RELEASE_KEYSTORE_PASSWORD=\"MY_PASSWORD\" \ -e RELEASE_KEY_ALIAS=\"my_mobile_app\" \ -e RELEASE_KEY_PASSWORD=\"MY_PASSWORD\"" reuseNode true } } environment { APP_VERSION = "${VERS_FULL}" BUILD_NUMBER = "${BUILD_NUMBER}" HOME = "${WORKSPACE}" } steps { script { sh 'sed -i \"s/versionCode 1/versionCode 1${BUILD_NUMBER}/\" ${WORKSPACE}/android/app/build.gradle' sh 'sed -i \"s/versionName \\"1.0\\"/versionName \\"${APP_VERSION}\\"/\" ${WORKSPACE}/android/app/build.gradle' sh "yarn && yarn cache clean && bundle install --path=vendor/bundle && \ env && bundle exec fastlane android release" } } } stage ('Android. Upload apk to slack and make artifacts') { steps { script { sh "mv -f ${WORKSPACE}/android/app/build/outputs/apk/release/app-release.apk ${WORKSPACE}/android/app/build/outputs/apk/release/my_mobile_app_${VERS_FULL}.apk" sh "curl -F \"file=@${WORKSPACE}/android/app/build/outputs/apk/release/my_mobile_app_${VERS_FULL}.apk\" -F \"initial_comment=Android: my_mobile_app_adhoc_${VERS_FULL}.apk file from \"${BRANCH_NAME}\" branch\" -F \"filetype=auto\" -F \"channels=${SLACK_CHANNEL}\" -H \"Authorization: Bearer ${SLACK_TOKEN}\" https://slack.com/api/files.upload" } } post { always { archiveArtifacts artifacts: "android/app/build/outputs/apk/release/my_mobile_app_${VERS_FULL}.apk", onlyIfSuccessful: true } } } } } stage('iOS.') { agent {node 'ios'} stages { stage('iOS. Define versions') { steps { script { BR_VER = sh(script: "echo ${BRANCH_NAME} | cut -d '/' -f 2 | tr -d \"\n\"", returnStdout: true) VERS_FULL = "${BR_VER}.${BUILD_NUMBER}" } } } stage ('Clearing cache.') { when { expression { return params.del_cache } } steps { deleteDir() } } stage('IOS. Get code') { steps { git branch: "${BRANCH_NAME}", credentialsId: "${REPO_CRED}", url: "${REPO}" } } stage('iOS. Build') { environment { APP_VERSION = "${VERS_FULL}" MATCH_GIT_URL = "[email protected]:artem/certificates.git" } steps { // Delete keychain if it exist: sh "rm -rf ./ios/Podfile.lock | true" sh "if git diff HEAD^ HEAD fastlane/devices.txt | grep -q 'fastlane/devices.txt'; then bundle exec fastlane run \ create_keychain name:'my_mobile_appkey' password:'MY_PASSWORD' \ default_keychain:true unlock:true timeout:false && \ bundle exec fastlane run match force:true keychain_name:my_mobile_appkey type:adhoc; fi" sh "/usr/bin/security delete-keychain ${IOS_KEYCHAIN} | true" sh "yarn && bundle install" script { sh "cp $HOME/jenkins/files_dotenv/.my_mobile_app.dotenv $WORKSPACE/.env" if (BRANCH_NAME ==~ "release/.*") { // Build adhock sh "bundle exec fastlane ios adhoc" sh "mv -f ios_build/my_mobile_app_adhoc.ipa ios_build/my_mobile_app_adhoc_${VERS_FULL}.ipa" sh "curl -F \"file=@ios_build/my_mobile_app_adhoc_${VERS_FULL}.ipa\" -F \"initial_comment=iOS: my_mobile_app_adhoc_${VERS_FULL}.ipa file from \"${BRANCH_NAME}\" branch\" -F \"filetype=auto\" -F \"channels=${SLACK_CHANNEL}\" -H \"Authorization: Bearer ${SLACK_TOKEN}\" https://slack.com/api/files.upload" // Build appstore sh "bundle exec fastlane ios release" sh "mv -f ios_build/my_mobile_app_appstore.ipa ios_build/my_mobile_app_appstore_${VERS_FULL}.ipa" } else if (BRANCH_NAME ==~ "develop/.*") { sh "bundle exec fastlane ios adhoc" sh "mv -f ios_build/my_mobile_app_adhoc.ipa ios_build/my_mobile_app_adhoc_${VERS_FULL}.ipa" sh "curl -F \"file=@ios_build/my_mobile_app_adhoc_${VERS_FULL}.ipa\" -F \"initial_comment=iOS: my_mobile_app_adhoc_${VERS_FULL}.ipa file from \"${BRANCH_NAME}\" branch\" -F \"filetype=auto\" -F \"channels=${SLACK_CHANNEL}\" -H \"Authorization: Bearer ${SLACK_TOKEN}\" https://slack.com/api/files.upload" } } } } stage ('iOS. Upload ipa to slack and make artifacts') { steps { echo "Making Artifacts..." } post { always { archiveArtifacts artifacts: "ios_build/*.ipa", onlyIfSuccessful: false sh "rm -rf $WORKSPACE/ios_build" sh "rm -f ${WORKSPACE}/.env" } } } } } } } } post { success { slackSend channel: "${SLACK_CHANNEL}", color: 'good', message: "Job: ${JOB_NAME}${BUILD_NUMBER} build was successful." } failure { slackSend channel: "${SLACK_CHANNEL}", color: 'danger', message: "Job: ${JOB_NAME}${BUILD_NUMBER} was finished with some error. It may occurs because of the build was rollbacked by docker swarm, or because of other error (watch the Jenkins Console Output): ${JOB_URL}${BUILD_ID}/consoleFull" } unstable { slackSend channel: "${SLACK_CHANNEL}", color: 'warning', message: "Job: ${JOB_NAME}${BUILD_NUMBER} was finished with some error. Please watch the Jenkins Console Output: ${JOB_URL}${BUILD_ID}/console." } } }
Gemfile
Находится в корне
source 'https://rubygems.org' gem 'fastlane' gem 'cocoapods' gem 'dotenv'
Android
build.grandge
Находится в android/app
apply plugin: "com.android.application" import com.android.build.OutputFile project.ext.react = [ entryFile: "index.js" ] apply from: "../../node_modules/react-native/react.gradle" def enableSeparateBuildPerCPUArchitecture = false def enableProguardInReleaseBuilds = false android { compileSdkVersion 26 buildToolsVersion '27.0.3' defaultConfig { applicationId "com.mymobileapp" minSdkVersion 16 targetSdkVersion 26 versionCode 1 versionName "1.0" ndk { abiFilters "armeabi-v7a", "x86" } } signingConfigs { release { storeFile file(String.valueOf(System.getenv("RELEASE_KEYSTORE_FILE"))) storePassword System.getenv("RELEASE_KEYSTORE_PASSWORD") keyAlias System.getenv("RELEASE_KEY_ALIAS") keyPassword System.getenv("RELEASE_KEY_PASSWORD") } debug { storeFile file(String.valueOf(System.getenv("DEBUG_KEYSTORE_FILE"))) storePassword System.getenv("DEBUG_KEYSTORE_PASSWORD") keyAlias System.getenv("DEBUG_KEY_ALIAS") keyPassword System.getenv("DEBUG_KEY_PASSWORD") } } lintOptions { checkReleaseBuilds false abortOnError false } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false include "armeabi-v7a", "x86" } } buildTypes { release { minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } } applicationVariants.all { variant -> variant.outputs.each { output -> def versionCodes = ["armeabi-v7a":1, "x86":2] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode } } } } dependencies { compile project(':react-native-mixpanel') compile project(':react-native-vector-icons') compile project(':react-native-svg') compile project(':react-native-splash-screen') compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:26.0.1" compile "com.facebook.react:react-native:+" compile project(':react-native-push-notification') } task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' } apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
iOS
Podfile
Located in the ios folder
platform :ios, '9.0' target 'My_Mobile_App' do pod 'Mixpanel' end
Fastlane
All files are in the fastlane folder
Matchfile
git_url("[email protected]:artem/certificates.git") git_branch("my_mobile_app")
devices.txt
Device ID Device Name XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX "Artem iPhone"
Appfile
app_identifier "com.mymobileapp" # The bundle identifier of your app apple_id "[email protected]" team_id "ABCDEFGHIJ"
Fastfile
default_platform(:ios) jenkinsBuildNumber = ENV["BUILD_NUMBER"] $buildNumber = "10" + jenkinsBuildNumber $versionNumber = ENV["APP_VERSION"] platform :ios do before_all do puts 'ios: before_all' cocoapods({ podfile: "./ios/Podfile" }) create_keychain( name: "my_mobile_app", password: "MY_PASSWORD", default_keychain: true, unlock: true, timeout: false ) end # Develop section lane :adhoc do register_devices(devices_file: "./fastlane/devices.txt") match(git_url: "[email protected]:artem/certificates.git", git_branch: "my_mobile_app", type: "adhoc", force_for_new_devices: true, keychain_name: "my_mobile_app", keychain_password: "MY_PASSWORD") increment_build_number( xcodeproj:"./ios/my_mobile_app.xcodeproj", build_number: $buildNumber ) increment_version_number( xcodeproj:"./ios/my_mobile_app.xcodeproj", version_number: $versionNumber ) automatic_code_signing( path: "./ios/my_mobile_app.xcodeproj", use_automatic_signing: false ) gym({ xcargs: "PROVISIONING_PROFILE_SPECIFIER='match AdHoc com.my_mobile_app' -UseNewBuildSystem='NO'", codesigning_identity: "iPhone Distribution: Artem Services (ABCDEFGHIJ)", workspace: "./ios/my_mobile_app.xcworkspace", scheme: "my_mobile_app", configuration: "Release", clean: true, silent: true, output_directory: "./ios_build", output_name: "my_mobile_app_adhoc.ipa" }) end lane :release do register_devices(devices_file: "./fastlane/devices.txt") match(git_url: "[email protected]:artem/certificates.git", git_branch: "my_mobile_app", type: "appstore", force_for_new_devices: true, keychain_name: "my_mobile_app", keychain_password: "MY_PASSWORD") increment_build_number( xcodeproj:"./ios/my_mobile_app.xcodeproj", build_number: $buildNumber ) increment_version_number( xcodeproj:"./ios/my_mobile_app.xcodeproj", version_number: $versionNumber ) gym({ xcargs: "PROVISIONING_PROFILE_SPECIFIER='match AppStore com.my_mobile_app' -UseNewBuildSystem='NO'", codesigning_identity: "iPhone Distribution: Artem Services (ABCDEFGHIJ)", workspace: "./ios/my_mobile_app.xcworkspace", scheme: "my_mobile_app", configuration: "Release", clean: true, silent: true, output_directory: "./ios_build/", output_name: "my_mobile_app_appstore.ipa" }) upload_to_testflight( ipa: "./ios_build/my_mobile_app_appstore.ipa" ) end after_all do |lane| puts 'ios: after_all' delete_keychain( name: "my_mobile_app" ) end error do |lane, exception| delete_keychain( name: "my_mobile_app" ) end end platform :android do before_all do puts 'android: before_all' end lane :release do gradle( task: "clean", project_dir: "android/" ) gradle( task: "assemble", build_type: "Release", project_dir: "android/", flags: "--no-daemon --max-workers 1", properties: { "versionCode" => $buildNumber, "versionName" => $versionNumber, "android.injected.signing.store.password" => "MY_PASSWORD", "android.injected.signing.key.alias" => "my_mobile_app", "android.injected.signing.key.password" => "MY_PASSWORD", } ) end after_all do |lane| puts 'android: after_all' end end
jenkins/files_dotenv/.my_mobile_app.dotenv
FASTLANE_PASSWORD='PASSWORD_FROM_MY_APPLE_ID_ACCOUNT' MATCH_PASSWORD='PASSWORD_FOR_DECRYPT_APPLE_CERTIFICATES'
Want more hlep regarding this tutorial
Hi! I suspect, that you and "Dock" the same person)
This is tutorial how to create Docker image, which I used in this article. Unfortunately I didn’t find free time to translate the article into English
https://artem.services/?p=453
Я хочу больше помощи от вас относительно этого урока. Дайте мне знать больше об изображении, которое вы использовали внутри этого конвейера.
В начале статьи оставил ссылку, на кастомный докер образ, вот:
https://artem.services/?p=453