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