Jenkins – Fastlane build iOS and Android apps

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'

 

Tagged: Tags

Subscribe
Notify of
guest

4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Prejith
Prejith
4 years ago

Want more hlep regarding this tutorial

Dock
Dock
4 years ago

Я хочу больше помощи от вас относительно этого урока. Дайте мне знать больше об изображении, которое вы использовали внутри этого конвейера.