Featured image of post 用 Jenkins+Fastlane+Slack 達成 CI/CD

用 Jenkins+Fastlane+Slack 達成 CI/CD

因為 Jenkins 的 Xcode Integration 有點難用而且專案一多,要個別維護有點麻煩,如果 Provisioning Profile 有新增裝置 就要重新上傳

用 Fastlane 用相同設定 帶入不同的 Scheme 就可以完成 ProvisioningProfile 可直接讀取/Library 底下的內容 CodeSign 用 Xcode 的自動下載就好可以完成

Jenkins

  • 負責角色

Bitbucket -> Jenkins -> Fastlane -> Slack …

安裝 Jenkins

1
2
brew install java8
brew install Jenkins

Configure

  1. Source Code Management
  • Git
    • Repositories
      • URL : …
      • Credentials : …
    • Branches to build
      • Brahch : …
  1. Execute shell

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    #!/bin/sh
    cd ${WORKSPACE}
    
    source ~/.bash_profile
    
    git checkout Your_Branch_Name
    #先切換 Branch 不然 increase commit 會跑到 HEAD detached
    
    git fetch
    
    git merge your_remote/your_branch
    
    fastlane your_lane
    

Fastlane

Ruby 語言寫成的

安裝

1
brew cask install Fastlane

安裝完要更新一下 bash_profiles

1
2
3
#~/.bash_profiles

export PATH="$HOME/.fastlane/bin

執行

在專案資料夾初始化 Fastlane

1
fastlane init

在專案目錄下執行

1
fastlane your_lane

在 /your/proj/path/fastlane 下執行也可以

執行 Fastlane 主要是根據 Fastfile 會在

1
/your/proj/path/fastlane/Fastfile

還有其他安裝與執行方式 rbenv,bundler etc.,

Code Signing

打包要用的 Provision Profile and Cert. 有很多種方式,這邊使用手動的方法

或是使用 match

在 Xocde > Preference > Accounts 登入,然後下載全部的 Provisioning Profile Provisioning Profile 預設位置在 ~/Library/MobileDevice/Provisioning Profiles Keychain 匯入 Distribuion Cert. Project Target > Gerenal > Signing 憑證必須選對

設定檔

  • Fastfile Quick Start
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
update_fastlane

default_platform(:ios)

platform :ios do
    lane :your_lane do
       build_ios_app(
          scheme: "YourScheme",
          workspace: "YourProject.xcworkspace",
          configuration: "Release", # or Debug
          export_options: {
            method: "ad-hoc",
            provisioningProfiles: {
              "your.domain.udid" => "project1_adhoc_name"
            },
            iCloudContainerEnvironment: "Production", # or 'Development'
          },
          clean: true,
          output_directory: "build",
          output_name: "ota.ipa",
          include_bitcode: false,
        )
    end
end
  • 切換目錄
1
2
3
4
Dir.chdir ".." do
    # your script
end
#效果等於 "cd .."
  • 執行 shell
1
2
3
4
5
6
7
8
#在一般command line
sh abcd.sh arg1 arg2
pwd
echo AA
#在Fastfile
sh "bash 'arg1' 'arg2'"
sh 'pwd'
sh "echo 'AA'"

使用 sh 起始位置會在/your/proj/path/fastlane 使用 fastlane 其他 action 起始位置在 /your/proj/path

  • 使用變數
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#宣告
var1 = "foo"
var2 = "bar"
var3 = var1 + "123" + var2
#var3 = foo123bar

#傳入參數到 bash 雙引號加上單引號的 Nested
sh "bash script.sh '#{var1}' ''$PWD'/'#{var2}'/' '#{var3}'"

#在Fastfile內 用雙引號就可以
yourFunc("ZXC#{var1}QSD")

def yourFunc(A)
#傳入參數 A 為 -> ZXCfooQSD
end
  • 使用自定義 function
1
2
3
4
5
6
7
8
def yourfuncA(arg1,arg2,arg3)
    #some script
end

#使用
lane :your_lane do
    yourfuncA("A","B","C")
end
  • 自動增加 build number
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 #宣告成function
 def increase_build_number_and_git_push(schemeName,target)
    increment_build_number_in_plist(target: schemeName)
    info_plist_path = get_info_plist_path(xcodeproj: 'YourProj.xcodeproj', # optional
                    target: schemeName, # optional, or `scheme`
                    # optional, must be specified if you have different Info.plist build settings
                    # for different build configurations
                    build_configuration_name: 'Release')
    app_name = get_info_plist_value(path: info_plist_path, key: "CFBundleDisplayName")
    ver_number = get_info_plist_value(path: info_plist_path, key: "CFBundleShortVersionString")
    build_number = get_info_plist_value(path: info_plist_path, key: "CFBundleVersion")
    git_commit(path:info_plist_path, message:"[CI-Skip] Updated [#{app_name}] v#{ver_number} Build #{build_number} for #{target}")
    push_to_git_remote(
      remote: "origin",         # optional, default: "origin"
      # local_branch: "release",  # optional, aliased by "branch", default is set to current branch
      remote_branch: "release", # optional, default is set to local_branch
      force: true,    # optional, default: false
      force_with_lease: true,   # optional, default: false
      tags: false,    # optional, default: true
      no_verify: true,# optional, default: false
      set_upstream: true        # optional, default: false
    )
  end

# 執行
increase_build_number_and_git_push("Your_Proj_Scheme","iTunesConnect")

# # 執行結果:
# 在Release Branch 會自動 git push -f
# commit 訊息:[CI-Skip] Updated [AppName] v1.9.18 Build 1 for iTunesConnect

也可以使用 increment_build_number 但是會把 xcworkspace 裡面全部 scheme 的 build number 都加一 個別指定 target 就用 increment_build_number_in_plist 參考 必須先安裝第三方外掛:fastlane add_plugin versioning 套件網址

  • 錯誤擷取
1
2
3
4
5
begin
    functionA("arg1","arg2")
rescue => exception
    on_error_Func(exception)
end
  • 上傳 dSYM 檔案
1
2
3
4
 upload_symbols_to_crashlytics(
    dsym_path: "./build/yourapp.app.dSYM.zip",
    gsp_path: "./your/firebase/GoogleService-info.plist"
  )

除錯

  • dSYM 上傳失敗的問題 根據這兩個討論 https://github.com/fastlane/fastlane/issues/13096 https://github.com/fastlane/fastlane/issues/14176 使用 upload_symbols_to_crashlytics 時候會出現 invalid byte sequence in UTF-8 之後雖然 Fastlane 表示上傳成功 但是 Firebase 後台還是沒有收到 dSYM 2019-03-16 沒有解 先用手動上傳

  • 執行 increment_build_number 出現

    1
    
    Please remove $(SRCROOT) in your Xcode target build settings
    

    解決方式: info.plist 不使用 $(SRCROOT) 改用絕對路徑 參考

    1
    2
    3
    4
    5
    
    #make this
    INFOPLIST_FILE = "$(SRCROOT)/Project/info.plist";
    
    #into this
    INFOPLIST_FILE = "Project/info.plist";
    
  • 同時匯出 AppStore and AdHoc 可以使用 resign 的方法,但是 Fastlane 建議跑兩次 lane

Slack

傳訊息進去 Slack 是利用 Webhook 方式 再依照他的API傳入對應參數 Fastlane 有封裝好的 plugin 只要填入 Slack Webhook 就可以 https://docs.fastlane.tools/actions/slack/

幾個 Slack 範例

  • 上傳 iTunes Connect 成功
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

def slack_itunes(appName,appScheme,ipaPath)
  ver = get_ipa_info_plist_value(ipa: ipaPath, key: "CFBundleShortVersionString")
  build = get_ipa_info_plist_value(ipa: ipaPath, key: "CFBundleVersion")
  slack(
   message: "["+appName + "] uploaded to iTunesConnect. 🚀🚀🚀",
   success: true,
   channel: "#your_channel",
   slack_url: "https://your/slack/webhook",
   username: "Jenkins_CI",
   icon_url: "http://Bot大頭貼/icon/jenkins.png",
   default_payloads: [],
   link_names: true,
   payload: {
      "Version" =>  "#{ver}" + " (" + "#{build}" + ")",
      "Upload Date" => Time.new.to_s,
    },
  attachment_properties: {
    thumb_url: "https://i.imgur.com/STnXPFy.png",
  }
)
end

效果:

attachment_properties 代表包裝好的參數之外 可傳入 Slack API 有的參數 https://api.slack.com/docs/message-attachments

  • 上傳 OTA
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def slack_ota(appName,appScheme,ipaPath)
  ver = get_ipa_info_plist_value(ipa: ipaPath, key: "CFBundleShortVersionString")
  build = get_ipa_info_plist_value(ipa: ipaPath, key: "CFBundleVersion")
  slack(
    slack_url: "https://your/slack/webhook",
    message: "["+appName + "] uploaded to OTA ! 🎉",
    username: "Jenkins_CI",
    link_names: "http://Your.CI.Server.domain",
    icon_url: "http://Jenkins.icon/jenkins.png",
    channel: "#jenkins_bot",
    success: true,
    payload: {
      "Version" =>  "#{ver}" + " (" + "#{build}" + ")",
      "Release Date" => Time.new.to_s,
    },
    default_payloads: [],
    attachment_properties: {
      color: "#2eb886",
      title: "OTA Download Page",
      title_link: "https://Your.OTA.download/page",
     },
    )
end

效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def on_error(exception,appName,appScheme)

  slack(
      message: "[" +appName+ "] some thing goes wrong",
      success: false,
      channel: "#Your_Error_Msg_Channel",
      slack_url: "https://Your.webhook",
      username: "Jenkins_CI",
      icon_url: "http://Jenkins.Thumbnail.icon/jenkins.png",
      default_payloads: [],
      payload: {
        "Version" =>  get_version_number(xcodeproj: "YourProj.xcodeproj",target: appScheme),
      },
      attachment_properties: {
          fields: [
              {
                  title: "Error message",
                  value: exception.to_s,
                  short: false
              }
          ],
          title: "Jenkins",
          title_link: "http://Your.CI.Server.domain/",
      }
  )
end

效果:

dinostack.cc
使用 Hugo 建立
主題 StackJimmy 設計