iOS持续集成(包括Cocoapods项目):Jenkins+GitLab+蒲公英+FTP+邮件提醒(邮件附带二维码)

什么是持续集成


持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

为什么使用持续集成


1.减少风险
2.减少重复过程
3.任何时间、任何地点生成可部署的软件
4.增强项目的可见性

本篇文章主要介绍在Mac环境下通过Jenkins搭建iOS持续集成平台


Jenkins的安装


  1. 在Mac环境下,我们需要先安装JDK,然后在Jenkins的官网下载最新的war包。
    下载完成后,打开终端,进入到war包所在目录,执行以下命令:
    java -jar jenkins.war --httpPort=8080
    httpPort指的就是Jenkins所使用的http端口,这里指定8080,可根据具体情况来修改。
  1. 通过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”选中安装。

插件安装1.png
插件安装2.png

自动化构建


点击“新建”,输入item的名称,选择“构建一个自由风格的软件项目”,然后点击“OK”。

新建.png

点击新建好的项目,进来配置一下General参数。

配置General.png

这里可以设置包的保留天数还有天数。

接着设置源码管理

由于现在我用到的是GitLab,先配置SSH Key,在Jenkins的证书管理中添加SSH。在Jenkins管理页面,选择“Credentials”,然后选择“Global credentials (unrestricted)”,点击“Add Credentials”,如下图所示,我们填写自己的SSH信息,然后点击“Save”,这样就把SSH添加到Jenkins的全局域中去了。

全局SSHkey配置.png

源码管理中选择Git, 输入Reponsitory URL, Credentials选项选择none, 如果正常的配置正确的话,是不会出现下图中的那段红色的警告。如果有下图的提示,就说明Jenkins还没有连通GitLab或者SVN,那就请再检查SSH Key是否配置正确。

SSH配置错误提示.png

构建触发器设置
这里是设置自动化测试的地方。这里涉及的内容很多,暂时我也没有深入研究,这里暂时先不设置。有自动化测试需求的可以好好研究研究这里的设置。

Xcode配置
点击“增加构建步骤”,选择“Xcode”。依次按下图填写项目信息:

XCode构建配置1.png

Keychain path配置的是你电脑上login.keychain的路径

XCode构建配置2.png
XCode构建配置3.png
XCode构建配置4.png

PS: 如果项目是Cocoapods项目,需要先Scheme Shared,再调整配置如下:

Scheme Shared.png
配置Cocoapods项目1.png
配置Cocoapods项目2.png

到这一步我们就实现了自动打包的所有配置了。
这时候你就可以进入工程页面点击立即构建,检验下自动打包配置是否正确,如果构建失败了,可以去查看Console Output可以查看log日志。

构建一次,各个颜色代表的意义如下:


1470193644501777.png

天气的晴雨表代表了项目的质量,这也是Jenkins的一个特色。


1470193655452812.jpg
立即构建.png
构建日志.png

不过,当iOS应用打包好后,我们还想发给其他相关人员安装,包括公司内部的,外网的,都需要。这时我们还需配置OTA服务和内网FTP。

外网安装App我们需要用到现在市面上比较流行的免费平台,蒲公英。在上面蒲公英官网设置相关信息后,我们可以写一个简单的脚本,来实现App打包后,上传到蒲公英和公司内网以及邮件提醒相关人员这一系列操作。

我们先用Jenkins的插件配置FTP信息。进入系统管理页面,选择系统设置,找到“Publish over FTP”,按下图填好相关信息:

FTP Server Name:给你自己看的名字,爱叫什么叫什么
Hostname:主机IP或者域名
Username:ftp登陆用户名
Password:ftp密码
Remote Directory:远程根目录

配置FTP1.png

回到任务配置页面,点击“增加构建后操作步骤”,然后选择“Send build artifacts over FTP”,填写:

配置FTP2.png

这样FTP服务就配置好了,到这里可以构建一个试下你的ipa文件是否正常传到FTP远程服务器

接下来我们再点击“增加构建后操作步骤”,选择“Execute a set of scripts”,配置脚本所在路径,如下图所示:

设置脚步路径.png

附上脚本:注意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 pillowpip 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!!!

推荐阅读更多精彩内容