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!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容