При помощи 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'