什么是持續集成
持續集成是一種軟件開發實踐,即團隊開發成員經常集成它們的工作,通過每個成員每天至少集成一次,也就意味著每天可能會發生多次集成。每次集成都通過自動化的構建(包括編譯,發布,自動化測試)來驗證,從而盡早地發現集成錯誤。
為什么使用持續集成
1.減少風險
2.減少重復過程
3.任何時間、任何地點生成可部署的軟件
4.增強項目的可見性
本篇文章主要介紹在Mac環境下通過Jenkins搭建iOS持續集成平臺
Jenkins的安裝
- 在Mac環境下,我們需要先安裝JDK,然后在Jenkins的官網下載最新的war包。
下載完成后,打開終端,進入到war包所在目錄,執行以下命令:
java -jar jenkins.war --httpPort=8080
httpPort指的就是Jenkins所使用的http端口,這里指定8080,可根據具體情況來修改。
- 通過Homebrew安裝
前提是Mac電腦上安裝homebrew和JDK,打開終端,輸入brew install jenkins
安裝成功后再輸入jenkins
啟動
待Jenkins啟動后,在瀏覽器頁面輸入以下地址:
http://localhost:8080
這樣就打開Jenkins管理頁面了。
Jenkins的配置
安裝GitLab插件
因為我們用的是GitLab來管理源代碼,Jenkins本身并沒有自帶GitLab插件,所以我們需要依次選擇 系統管理->管理插件,在“過濾”輸入框中搜索“GitLab Plugin”和“Gitlab Hook Plugin”這兩項,選中然后安裝。安裝Xcode插件
同安裝GitLab插件的步驟一樣,我們依次選擇系統管理->管理插件,在“過濾”輸入框中搜索“Xcode integration”選中安裝。安裝腳本插件
這個插件的功能主要是用于在build后執行相關腳本。在系統管理->管理插件,在“過濾”輸入框中搜索“Post-Build Script Plug-in”選中安裝。安裝CocoaPods插件
這個插件的功能主要是用于在build后執行相關腳本。在系統管理->管理插件,在“過濾”輸入框中搜索“CocoaPods Jenkins Integration”選中安裝。
自動化構建
點擊“新建”,輸入item的名稱,選擇“構建一個自由風格的軟件項目”,然后點擊“OK”。
點擊新建好的項目,進來配置一下General參數。
這里可以設置包的保留天數還有天數。
接著設置源碼管理。
由于現在我用到的是GitLab,先配置SSH Key,在Jenkins的證書管理中添加SSH。在Jenkins管理頁面,選擇“Credentials”,然后選擇“Global credentials (unrestricted)”,點擊“Add Credentials”,如下圖所示,我們填寫自己的SSH信息,然后點擊“Save”,這樣就把SSH添加到Jenkins的全局域中去了。
源碼管理中選擇Git, 輸入Reponsitory URL, Credentials選項選擇none, 如果正常的配置正確的話,是不會出現下圖中的那段紅色的警告。如果有下圖的提示,就說明Jenkins還沒有連通GitLab或者SVN,那就請再檢查SSH Key是否配置正確。
構建觸發器設置
這里是設置自動化測試的地方。這里涉及的內容很多,暫時我也沒有深入研究,這里暫時先不設置。有自動化測試需求的可以好好研究研究這里的設置。
Xcode配置
點擊“增加構建步驟”,選擇“Xcode”。依次按下圖填寫項目信息:
Keychain path配置的是你電腦上login.keychain的路徑
PS: 如果項目是Cocoapods項目,需要先Scheme Shared
,再調整配置如下:
到這一步我們就實現了自動打包的所有配置了。
這時候你就可以進入工程頁面點擊立即構建,檢驗下自動打包配置是否正確,如果構建失敗了,可以去查看Console Output可以查看log日志。
構建一次,各個顏色代表的意義如下:
天氣的晴雨表代表了項目的質量,這也是Jenkins的一個特色。
不過,當iOS應用打包好后,我們還想發給其他相關人員安裝,包括公司內部的,外網的,都需要。這時我們還需配置OTA服務和內網FTP。
外網安裝App我們需要用到現在市面上比較流行的免費平臺,蒲公英。在上面蒲公英官網設置相關信息后,我們可以寫一個簡單的腳本,來實現App打包后,上傳到蒲公英和公司內網以及郵件提醒相關人員這一系列操作。
我們先用Jenkins的插件配置FTP信息。進入系統管理頁面,選擇系統設置,找到“Publish over FTP”,按下圖填好相關信息:
FTP Server Name:給你自己看的名字,愛叫什么叫什么
Hostname:主機IP或者域名
Username:ftp登陸用戶名
Password:ftp密碼
Remote Directory:遠程根目錄
回到任務配置頁面,點擊“增加構建后操作步驟”,然后選擇“Send build artifacts over FTP”,填寫:
這樣FTP服務就配置好了,到這里可以構建一個試下你的ipa文件是否正常傳到FTP遠程服務器
接下來我們再點擊“增加構建后操作步驟”,選擇“Execute a set of scripts”,配置腳本所在路徑,如下圖所示:
附上腳本:注意Python對于空格要求的嚴格性,附件下載地址:https://pan.baidu.com/s/1ctaC7k
PS:需安裝email,打開終端執行pip install email
。發送的網易或QQ等郵箱所需填寫的密碼應該填授權碼,而不是郵箱本身密碼,附上QQ郵箱如何生成授權碼:http://jingyan.baidu.com/article/29697b91072c51ab20de3c3f.html
# -*- coding:utf-8 -*-
import time
import urllib2
import json
import mimetypes
import os
import smtplib
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
# 運行時環境變量字典
environsDict = os.environ
#此次 jenkins 構建版本號
jenkins_build_number = environsDict['BUILD_NUMBER']
#App相關
app_name = 'xxx'
app_version = 'x.x.x'
local_time = time.strftime('%Y.%m.%d',time.localtime(time.time()))
app_ipa_name = app_name + '-V' + app_version + '-' + local_time + '-' + jenkins_build_number + '.ipa'
app_ipa_ftp_path = 'ftp://192.168.5.110/Jenkins/ftp/' + app_ipa_name
app_ipa_workspace_path = '/Users/James/.jenkins/workspace/JMSTabBarKitTest/ipa/' + app_ipa_name
#蒲公英應用上傳地址
url = 'http://www.pgyer.com/apiv1/app/upload'
#蒲公英提供的 用戶Key
uKey = 'xxx'
#上傳文件的文件名(這個可隨便取,但一定要以 ipa 結尾)
file_name = app_ipa_name
#蒲公英提供的 API Key
_api_key = 'xxx'
#安裝應用時需要輸入的密碼,這個可不填
installPassword = ''
#項目名稱,用在拼接 tomcat 文件地址
project_name = app_name + "_" + app_version + "_" + local_time
#ipa 文件在 tomcat 服務器上的地址
ipa_file_tomcat_http_url = app_ipa_ftp_path
#獲取 ipa 文件路徑
def get_ipa_file_path():
#工作目錄下面的 ipa 文件
ipa_file_workspace_path = app_ipa_workspace_path
#tomcat 上的 ipa 文件
ipa_file_tomcat_path = ipa_file_tomcat_http_url
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)
#請求字典編碼
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
#處理蒲公英上傳結果
def handle_resule(result):
json_result = json.loads(result)
if json_result['code'] is 0:
send_Email(json_result)
#發送郵件
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 = ['xx@xx.com','xx@xx.com']
#根據不同郵箱配置 host,user,和pwd
mail_host = 'smtp.xx.com'
mail_user = 'xx@xx.com'
mail_pwd = 'xx'
mail_to = ','.join(mail_receivers)
msg = MIMEMultipart()
pgyerUrl = 'http://www.pgyer.com/' + str(appShortcutUrl)
environsString = '<h3>' + app_name + 'iOS端安裝包</h3>'
environsString += '<p>FTP ipa包下載地址 : <a href="' + ipa_file_tomcat_http_url + '">' + ipa_file_tomcat_http_url + '</a></p>'
environsString += '<p>蒲公英應用托管平臺在線安裝: <a href="' + pgyerUrl + '">' + pgyerUrl + '</a></p>'
environsString += '<li><a href="itms-services://?action=download-manifest&url=https://ssl.pgyer.com/app/plist/' + str(appKey) + '">手機直接安裝</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'] = app_name + 'iOS端最新打包文件'
try:
s = smtplib.SMTP_SSL(mail_host)
s.login(mail_user, mail_pwd)
s.sendmail(mail_user, mail_receivers, msg.as_string())
s.close()
print('success')
except Exception as e:
print(e)
#############################################################
def main():
print("uploading....")
#請求參數字典
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)
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
try:
resp = urllib2.urlopen(req)
body = resp.read().decode('utf-8')
handle_resule(body)
except urllib2.HTTPError as e:
print(e.fp.read())
if __name__ == '__main__':
main()
PS:郵件附帶二維碼腳本,需安裝pillow和qrcode,打開終端執行pip install pillow
和pip install qrcode
,附件下載地址:https://pan.baidu.com/s/1c1XXPzq
# -*- coding:utf-8 -*-
import time
import urllib2
import json
import mimetypes
import os
import smtplib
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
from email.mime.base import MIMEBase
#二維碼
from PIL import Image
import qrcode
# 運行時環境變量字典
environsDict = os.environ
#此次 jenkins 構建版本號
jenkins_build_number = environsDict['BUILD_NUMBER']
#App相關
app_name = 'xxx'
app_version = 'x.x.x'
local_time = time.strftime('%Y.%m.%d',time.localtime(time.time()))
app_ipa_name = app_name + '-V' + app_version + '-' + local_time + '-' + jenkins_build_number + '.ipa'
app_ipa_ftp_path = 'ftp://192.168.5.110/Jenkins/ftp/' + app_ipa_name
app_ipa_workspace_path = '/Users/James/.jenkins/workspace/JMSTabBarKitTest/ipa/' + app_ipa_name
app_logo_path = 'xxx.png'
#蒲公英應用上傳地址
url = 'http://www.pgyer.com/apiv1/app/upload'
#蒲公英提供的 用戶Key
uKey = 'xxx'
#上傳文件的文件名(這個可隨便取,但一定要以 ipa 結尾)
file_name = app_ipa_name
#蒲公英提供的 API Key
_api_key = 'xxx'
#安裝應用時需要輸入的密碼,這個可不填
installPassword = ''
#項目名稱,用在拼接 tomcat 文件地址
project_name = app_name + "_" + app_version + "_" + local_time
#ipa 文件在 tomcat 服務器上的地址
ipa_file_tomcat_http_url = app_ipa_ftp_path
#獲取 ipa 文件路徑
def get_ipa_file_path():
#工作目錄下面的 ipa 文件
ipa_file_workspace_path = app_ipa_workspace_path
#tomcat 上的 ipa 文件
ipa_file_tomcat_path = ipa_file_tomcat_http_url
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)
#請求字典編碼
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
#處理蒲公英上傳結果
def handle_resule(result):
json_result = json.loads(result)
if json_result['code'] is 0:
send_Email(json_result)
#發送郵件
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 = ['xx@xx.com','xx@xx.com']
#根據不同郵箱配置 host,user,和pwd
mail_host = 'smtp.xx.com'
mail_user = 'xx@xx.com'
mail_pwd = 'xx'
mail_to = ','.join(mail_receivers)
msg = MIMEMultipart()
#二維碼
qrCodePath = 'ipa/' + app_name + '-V' + app_version + '-' + local_time + '-' + jenkins_build_number + '.png'
pgyerUrl = 'http://www.pgyer.com/' + str(appShortcutUrl)
gen_qrcode(pgyerUrl, qrCodePath, app_logo_path)
environsString = '<h3>' + app_name + 'iOS端安裝包</h3>'
environsString += '<p>FTP ipa包下載地址 : <a href="' + ipa_file_tomcat_http_url + '">' + ipa_file_tomcat_http_url + '</a></p>'
environsString += '<p>蒲公英應用托管平臺在線安裝: <a href="' + pgyerUrl + '">' + pgyerUrl + '</a></p>'
environsString += '<p>附二維碼,可直接用微信掃描安裝<br/><img src="cid:0" alt="" /></p>'
environsString += '<li><a href="itms-services://?action=download-manifest&url=https://ssl.pgyer.com/app/plist/' + str(appKey) + '">手機直接安裝</a></li>'
message = environsString
body = MIMEText(message, _subtype='html', _charset='utf-8')
#添加附件
with open(qrCodePath, 'rb') as f:
# 設置附件的MIME和文件名,這里是png類型:
mime = MIMEBase('image', 'png', filename=qrCodePath)
# 加上必要的頭信息:
mime.add_header('Content-Disposition', 'attachment', filename=qrCodePath)
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的內容讀進來:
mime.set_payload(f.read())
# 用Base64編碼:
encoders.encode_base64(mime)
msg.attach(mime)
msg.attach(body)
msg['To'] = mail_to
msg['from'] = mail_user
msg['subject'] = app_name + 'iOS端最新打包文件'
try:
s = smtplib.SMTP_SSL(mail_host)
s.login(mail_user, mail_pwd)
s.sendmail(mail_user, mail_receivers, msg.as_string())
s.close()
print('success')
except Exception as e:
print(e)
#生成二維碼
def gen_qrcode(string, path, logo=""):
qr = qrcode.QRCode(
version=2,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=8,
border=1
)
qr.add_data(string)
qr.make(fit=True)
img = qr.make_image()
img = img.convert("RGBA")
if logo and os.path.exists(logo):
try:
icon = Image.open(logo)
img_w, img_h = img.size
except Exception as e:
print(e)
sys.exit(1)
factor = 4
size_w = int(img_w / factor)
size_h = int(img_h / factor)
icon_w, icon_h = icon.size
if icon_w > size_w:
icon_w = size_w
if icon_h > size_h:
icon_h = size_h
icon = icon.resize((icon_w, icon_h), Image.ANTIALIAS)
w = int((img_w - icon_w) / 2)
h = int((img_h - icon_h) / 2)
icon = icon.convert("RGBA")
img.paste(icon, (w, h), icon)
img.save(path)
# 調用系統命令打開圖片
# xdg - open(opens a file or URL in the user's preferred application)
os.system('xdg-open %s' % path)
#############################################################
def main():
print("uploading....")
#請求參數字典
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)
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
try:
resp = urllib2.urlopen(req)
body = resp.read().decode('utf-8')
handle_resule(body)
except urllib2.HTTPError as e:
print(e.fp.read())
if __name__ == '__main__':
main()
SUCCESS!!!