Jenkins — Fastlane build iOS and Android apps

При помощи Jenkins и Fastlane будем собирать приложения на iOS и Android, будем отправлять артефакты в Slack, и так же автоматически приложение для iOS отправлять в Testflight. Сборка настроена с веток develop и release,  и считывает с них версию релиза (major и minor), и добавляет номер сборки.

К примеру: ветка — release/1.0 и номер Jenkins сборки 25, то версия приложения будет — 1.0.25

Для сборки будет нужны:

Для удобства мои приложение везде называется: «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

Находится в папке ios

platform :ios, '9.0'

target 'My_Mobile_App' do
  pod 'Mixpanel'
end

Fastlane

Все файлы находятся в папке fastlane

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'

 

Метки: Метки

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии