一步一步構(gòu)建iOS持續(xù)集成:Jenkins+GitLab+蒲公英+FTP

什么是持續(xù)集成

持續(xù)集成是一種軟件開發(fā)實(shí)踐,即團(tuán)隊(duì)開發(fā)成員經(jīng)常集成它們的工作,通過每個(gè)成員每天至少集成一次,也就意味著每天可能會(huì)發(fā)生多次集成。每次集成都通過自動(dòng)化的構(gòu)建(包括編譯,發(fā)布,自動(dòng)化測(cè)試)來驗(yàn)證,從而盡早地發(fā)現(xiàn)集成錯(cuò)誤。

為什么使用持續(xù)集成

1.減少風(fēng)險(xiǎn)
2.減少重復(fù)過程
3.任何時(shí)間、任何地點(diǎn)生成可部署的軟件
4.增強(qiáng)項(xiàng)目的可見性

常用的持續(xù)集成工具


市面上的持續(xù)集成工具有很多,考慮到Jenkins的穩(wěn)定性,我們還是選擇以Jenkins來開始iOS的持續(xù)集成。

好吧,接下來就正式開始搭建iOS持續(xù)集成平臺(tái)了。

Jenkins的安裝


在Mac環(huán)境下,我們需要先安裝JDK,然后在Jenkins的官網(wǎng)下載最新的war包。
下載完成后,打開終端,進(jìn)入到war包所在目錄,執(zhí)行以下命令:

java -jar jenkins.war --httpPort=8888

httpPort指的就是Jenkins所使用的http端口,這里指定8888,可根據(jù)具體情況來修改。待Jenkins啟動(dòng)后,在瀏覽器頁面輸入以下地址:

http://localhost:8888

這樣就打開Jenkins管理頁面了。

Jenkins的配置


  • 安裝GitLab插件
    因?yàn)槲覀冇玫氖荊itLab來管理源代碼,Jenkins本身并沒有自帶GitLab插件,所以我們需要依次選擇 系統(tǒng)管理->管理插件,在“可選插件”中選中“GitLab Plugin”和“Gitlab Hook Plugin”這兩項(xiàng),然后安裝。

  • 安裝Xcode插件
    同安裝GitLab插件的步驟一樣,我們依次選擇系統(tǒng)管理->管理插件,在“可選插件”中選中“Xcode integration”安裝。

  • 安裝簽名證書管理插件
    iOS打包內(nèi)測(cè)版時(shí),需要發(fā)布證書及相關(guān)簽名文件,因此這兩個(gè)插件對(duì)于管理iOS證書非常方便。還是在系統(tǒng)管理->管理插件,在“可選插件”中選中“Credentials Plugin”和“Keychains and Provisioning Profiles Management”安裝。

  • 安裝FTP插件
    系統(tǒng)管理->管理插件,在“可選插件”中選中“Publish over FTP”安裝。

  • 安裝腳本插件
    這個(gè)插件的功能主要是用于在build后執(zhí)行相關(guān)腳本。在系統(tǒng)管理->管理插件,在“可選插件”中選中“Post-Build Script Plug-in”安裝。

好了,插件安裝完,可以正式開始自動(dòng)化構(gòu)建了!??!

自動(dòng)化構(gòu)建


在Jenkins中,所有的任務(wù)都是以“item”為單位的。接下來我們就新建一個(gè)iOS的項(xiàng)目來開始自動(dòng)化構(gòu)建。點(diǎn)擊“新建”,輸入item的名稱,選擇“構(gòu)建一個(gè)自由風(fēng)格的軟件項(xiàng)目”,然后點(diǎn)擊“OK”。

然后按下圖設(shè)置構(gòu)建信息,

源碼管理:
這里用到的是GitLab,先需要配置SSH,我們可以在Jenkins的證書管理中添加SSH。在Jenkins管理頁面,選擇“Credentials”,然后選擇“Global credentials (unrestricted)”,點(diǎn)擊“Add Credentials”,如下圖所示,我們填寫自己的SSH信息,然后點(diǎn)擊“Save”,這樣就把SSH添加到Jenkins的全局域中去了。

接下來,我們?cè)倩氐絼倓傂陆ǖ娜蝿?wù)中,在源碼管理中,選擇Git,按下圖填好相關(guān)信息。PS:Credentials不需要選擇。

構(gòu)建觸發(fā)器設(shè)置
因?yàn)榇私坛滩簧婕白詣?dòng)測(cè)試這塊的流程,所以不需要設(shè)置觸發(fā)器。(以后會(huì)有另外的自動(dòng)測(cè)試教程_)

構(gòu)建環(huán)境設(shè)置
iOS打包需要簽名文件和證書,所以這部分我們勾選“Keychains and Code Signing Identities”和“Mobile Provisioning Profiles”。

這里我們又需要用到Jenkins的插件,在系統(tǒng)管理頁面,選擇“Keychains and Provisioning Profiles Management”。

進(jìn)入Keychains and Provisioning Profiles Management頁面,點(diǎn)擊“瀏覽”按鈕,分別上傳自己的keychain和證書。
上傳成功后,我們?cè)贋閗eychain指明簽名文件的名稱。點(diǎn)擊“Add Code Signing Identity”,最后添加成功后如下圖所示:

這樣我們的Adhoc證書和簽名文件就已經(jīng)在Jenkins中配置好了,接下來我們只需要在item設(shè)置中指定相關(guān)文件即可。

回到我們新建的item,找到構(gòu)建環(huán)境,按下圖選好自己的相關(guān)證書和簽名文件。

Xcode配置
點(diǎn)擊“增加構(gòu)建步驟”,選擇“Xcode”。
依次按下圖填寫項(xiàng)目信息:

腳本設(shè)置
我們沒有勾選“Pack application and build.ipa”的原因是,Jenkins的Xcode插件不支持Mac10.10以上的打包了。所以,我們需要用腳本來自己實(shí)現(xiàn)iOS打包。
仍然是點(diǎn)擊“增加構(gòu)建步驟”,選擇“Execute Shell”。
輸入下列腳本:
if [ -d "${WORKSPACE}/builds" ]; then rm -rf ${WORKSPACE}/builds; fi; mkdir ${WORKSPACE}/builds; if [ -d "${WORKSPACE}/builds/${BUILD_NUMBER}" ]; then rm -rf ${WORKSPACE}/builds/${BUILD_NUMBER}; fi; mkdir ${WORKSPACE}/builds/${BUILD_NUMBER}; xcodebuild -project ${WORKSPACE}/testForiOS/testForiOS.xcodeproj -scheme "testForiOS" -sdk iphoneos archive -archivePath ${WORKSPACE}/builds/${BUILD_NUMBER}/archive CODE_SIGN_IDENTITY="iPhone Distribution: xxxxxxx" xcodebuild -exportArchive -exportFormat IPA -archivePath ${WORKSPACE}/builds/${BUILD_NUMBER}/archive.xcarchive -exportPath ${WORKSPACE}/builds/${BUILD_NUMBER}/${JOB_NAME}_${BUILD_NUMBER}.ipa -exportProvisioningProfile "XC Ad Hoc: com.xxxxx.xxx"

這樣我們就簡(jiǎn)單的實(shí)現(xiàn)了自動(dòng)打包的所有配置了。

不過,當(dāng)iOS應(yīng)用打包好后,我們還想發(fā)給其他相關(guān)人員安裝,包括公司內(nèi)部的,外網(wǎng)的,都需要。這時(shí)我們還需配置OTA服務(wù)和內(nèi)網(wǎng)FTP。

外網(wǎng)安裝App我們需要用到現(xiàn)在市面上比較流行的免費(fèi)平臺(tái),蒲公英。在上面蒲公英官網(wǎng)設(shè)置相關(guān)信息后,我們可以寫一個(gè)簡(jiǎn)單的腳本,來實(shí)現(xiàn)App打包后,上傳到蒲公英和公司內(nèi)網(wǎng)以及郵件提醒相關(guān)人員這一系列操作。

我們先用Jenkins的插件配置FTP信息。進(jìn)入系統(tǒng)管理頁面,選擇系統(tǒng)設(shè)置,找到“Publish over FTP”,按下圖填好相關(guān)信息:

回到任務(wù)配置頁面,點(diǎn)擊“增加構(gòu)建后操作步驟”,然后選擇“Send build artifacts over FTP”,填寫:

這樣FTP服務(wù)就配置好了。

接下來我們?cè)冱c(diǎn)擊“增加構(gòu)建后操作步驟”,選擇“Execute a set of scripts”,如下圖所示:

所以配置都已經(jīng)設(shè)置好,點(diǎn)擊“保存”,好了,我們可以試試點(diǎn)擊“立即構(gòu)建”按鈕了。

SUCCESS?。?!

以上就是iOS持續(xù)集成的簡(jiǎn)單內(nèi)容,教程中暫未涉及到自動(dòng)測(cè)試,以后會(huì)推出自動(dòng)測(cè)試的內(nèi)容,未完待續(xù)。。。。

附(腳本內(nèi)容):
#coding=utf-8
import time
import urllib2
import time
import json
import mimetypes
import os
import smtplib
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
import json

#蒲公英應(yīng)用上傳地址
url = 'http://www.pgyer.com/apiv1/app/upload'
#蒲公英提供的 用戶Key
uKey = 'xxxx'
#上傳文件的文件名(這個(gè)可隨便取,但一定要以 ipa 結(jié)尾)
file_name = 'xxxx.ipa'
#蒲公英提供的 API Key
_api_key = 'xxxxx'
#安裝應(yīng)用時(shí)需要輸入的密碼,這個(gè)可不填
installPassword = '123456'

# 運(yùn)行時(shí)環(huán)境變量字典
environsDict = os.environ
#此次 jenkins 構(gòu)建版本號(hào)
jenkins_build_number = environsDict['BUILD_NUMBER']

#項(xiàng)目名稱,用在拼接 tomcat 文件地址
project_name = 'xxxx'
#ipa 文件在 tomcat 服務(wù)器上的地址
ipa_file_tomcat_http_url = 'ftp://192.168.10.4/jenkins/iOS/' + jenkins_build_number + '/' + project_name +'_' + jenkins_build_number + '.ipa'

#獲取 ipa 文件路徑
def get_ipa_file_path():
#工作目錄下面的 ipa 文件
ipa_file_workspace_path = '/Users/Shared/Jenkins/Home/jobs/' + project_name + '/workspace/builds/' + jenkins_build_number + '/' + project_name + '_' + jenkins_build_number + '.ipa'
#tomcat 上的 ipa 文件
ipa_file_tomcat_path = '/usr/local/tomcat/webapps/' + project_name + '/static/' + jenkins_build_number + '/' + jenkins_build_number + '.ipa'

if os.path.exists(ipa_file_workspace_path):
    return ipa_file_workspace_path
elif os.path.exists(ipa_file_tomcat_path):
    return ipa_file_tomcat_path

#ipa 文件路徑
ipa_file_path = get_ipa_file_path()
print ipa_file_path
#請(qǐng)求字典編碼
def _encode_multipart(params_dict):
boundary = '----------%s' % hex(int(time.time() * 1000))
data = []
for k, v in params_dict.items():
    data.append('--%s' % boundary)
    if hasattr(v, 'read'):
        filename = getattr(v, 'name', '')
        content = v.read()
        decoded_content = content.decode('ISO-8859-1')
        data.append('Content-Disposition: form-data; name="%s"; filename="SASDKDemo.ipa"' % k)
        data.append('Content-Type: application/octet-stream\r\n')
        data.append(decoded_content)
    else:
        data.append('Content-Disposition: form-data; name="%s"\r\n' % k)
        data.append(v if isinstance(v, str) else v.decode('utf-8'))
data.append('--%s--\r\n' % boundary)
return '\r\n'.join(data), boundary

#處理蒲公英上傳結(jié)果
def handle_resule(result):
json_result = json.loads(result)
if json_result['code'] is 0:
    send_Email(json_result)

#發(fā)送郵件
def send_Email(json_result):
appName = json_result['data']['appName']
appKey = json_result['data']['appKey']
appVersion = json_result['data']['appVersion']
appBuildVersion = json_result['data']['appBuildVersion']
appShortcutUrl = json_result['data']['appShortcutUrl']
#郵件接受者
mail_receivers = ['xxx@xxx.com', 'xxx@xxx.com']
#根據(jù)不同郵箱配置 host,user,和pwd
mail_host = 'smtp.xxx.com'
mail_user = 'xxx@xxx.com'
mail_pwd = '123'
mail_to = ','.join(mail_receivers)
msg = MIMEMultipart()

environsString = '<h3>移動(dòng)端iOS安裝包</h3><p>'
environsString += '<p>內(nèi)網(wǎng)ipa包下載地址 : ' + ipa_file_tomcat_http_url + '<p>'
environsString += '<p>外網(wǎng)在線安裝 : ' + 'http://www.pgyer.com/' + str(appShortcutUrl) + '<p>'
environsString += '<li><a href="itms-services://?action=download-manifest&url=https://ssl.pgyer.com/app/plist/' + str(appKey) + '">手機(jī)直接安裝</a></li>'
message = environsString
body = MIMEText(message, _subtype='html', _charset='utf-8')
msg.attach(body)
msg['To'] = mail_to
msg['from'] = mail_user
msg['subject'] = 'iOSxxx版本最新打包文件'

try:
    s = smtplib.SMTP()
    s.connect(mail_host)
    s.login(mail_user, mail_pwd)
    
    s.sendmail(mail_user, mail_receivers, msg.as_string())
    s.close()
    
    print 'success'
except Exception, e:
    print e
#############################################################

#請(qǐng)求參數(shù)字典
params = {
'uKey': uKey,
'_api_key': _api_key,
'file': open(ipa_file_path, 'rb'),
'publishRange': '2',
}
coded_params, boundary = _encode_multipart(params)
req = urllib2.Request(url, coded_params.encode('ISO-8859-1'))
req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
try:
resp = urllib2.urlopen(req)
body = resp.read().decode('utf-8')
handle_resule(body)
except urllib2.HTTPError as e:
print(e.fp.read())
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容