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