jenkins自动化部署(ios企签,打包,提审)
导读
背景
-
因为项目需要,未来除了主线产品外,还会有30几个马甲,所以为了减轻开发工作量,将所有打包项目都转移的Jenkins,单独部署一台专门打包,还有各种自动化的机器上.
-
目前大概主要用到打包,提审,自动化蒲公英,企业签名….等等,主要是ios为主,还有部分Android的脚本配置,基本上都是一件自动化的
需要技能
-
1.ios开发基础.
-
2.shell,Python入门 (由于本人比较熟悉这2个所有就用来,当然你熟悉nodsjs也是可以的)
Jenkins必备插件
环境安装
- brew安装及换源,brew安装方法请自行百度,并保持最新版
cd "$(brew --repo)"
git remote set-url origin https://mirrors.ustc.edu.cn/brew.git
# 替换homebrew-core.git:
cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core"
git remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git
- jenkins采用brew方法安装,另外macOS自待python2的版本,这里就不配置了
brew install jenkins
- jq,一个shell解析json的工具,我这里是解析蒲公英上传后的json使用
brew install jq
插件安装
ios
- 以下是必须的插件,还会自动配置一堆依赖
Email Extension Plugin //邮件配置
Role-based Authorization Strategy //权限管理
Git //代码拉取
android
- 环境不是我部,所有不清楚,我这里只是提供了一个资源替换脚本而已
插件配置
邮件配置
-
jenkins邮件插件Email Extension Plugin的安装与配置 这篇就可以了说得很全
-
另外补充一下,job邮件配置有几个注意点看下图.第一个可以指定其他特殊的邮箱,如qa组;第二个,可以设置附件,如蒲公英二维码,企签后的包…目录都是在workspace/项目名称的为根目录;具体见下
job参数配置
-
建议能用可选参数尽量用可选
-
文件参数上传后,必须指定生成路径及文件名.注意,如果需要zip上传一些资源,然后跑脚本更改配置,记得跑好脚本后删除原来资源,这样可以减小项目冲突,或某情况下,打包判断
git配置
- 这个不说了,也没啥好说的.注意使用ssh链接的话要在本机先配公匙
ios脚本构建项目
-
脚本打包主要涉及测试包,公测包,提审包…也就这三类…
-
我们这里是使用脚本构建的,核心也就是xcodebuild.也没使用Jenkins,也没使用fastlane作为构建工具.都是用xcode自带的工具,签名也是采用自动化签名的.
-
另外, debug/release构建没啥特别,提审需要引用xcode的一个工具链接到bin目录下.
debug/release 脚本
-
脚本放在 *.xcworkspace同级目录 7个参数按需填写
-
会在桌面生成对Ipa文件夹,里面会包含所有的打包
#./build-export.sh "debug" "day" "描述" "邮箱" "password" "ukey" "apikey"
#参数1 debug/release/publish
#参数2 day:2019-4-8/I0f05f3e33dac3cbc64da2a737145a96b12850137
#参数3 ""/"蒲公英自定义描述"
#参数4 苹果上传账号 appleId
#参数5 苹果上传密码 apple password
#参数6 蒲公英ukey 默认 f46...83b
#参数7 蒲公英apikey 默认 b98...58f
#工程名 *.xcworkspace
project_name=Aaaa
#scheme名
scheme_name= Aaaa
#打包模式 debug/release/publish
pack_mode=${1:-"debug"}
# 1.type:day:2018-9-10 按日输出 2.change-id 最后一次上传日志
LastBuildCommitId=${2:-"day"}
#蒲公英-上传描述
PgyerUpdateDescription=${3:-""}
#提审需要信息
AppleId=${4:-}
ApplePsw=${5:-}
##蒲公英-触电内侧账号
PgyerUKey=${6:-"xxx"}
PgyerApiKey=${7:-"xxx"}
#plist文件所在路径
exportOptionsPlistPath=""
#版本类型中文名
development_name=""
#当前目录路径
sourcePath=$(pwd)
#build目录
buildPath="${sourcePath}/build"
#brew install jq
#ln -s /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool /usr/local/bin/altool
#mkdir -p /usr/local/itms/bin/
#ln -s /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter /usr/local/itms/bin/iTMSTransporter
## 版本选择
development_mode=""
if [ "$pack_mode" = "release" ]; then
development_name="公测版"
development_mode="Release"
exportOptionsPlistPath=./export-achot.plist
elif [ "$pack_mode" = "publish" ]; then
development_name="提审版"
exportOptionsPlistPath=./export-appstore.plist
development_mode="Release"
else
development_name="测试版"
exportOptionsPlistPath=./export-debug.plist
development_mode="Debug"
fi
#分支名称
branch_name=$(git symbolic-ref --short -q HEAD)
#Info.plist路径
plistPath=${project_name}/Info.plist
#直播助手版本号必须为全数字不然有坑
buildTime=$(date +%Y%m%d%H%M)
#appVersion
appVersion=$(/usr/libexec/Plistbuddy -c "Print:CFBundleShortVersionString" "${plistPath}")
#displayName
appDisplayName=$(/usr/libexec/Plistbuddy -c "Print:CFBundleDisplayName" "${plistPath}")
#导出.ipa文件所在文件夹路径
exportFilePath=~/Desktop/Ipa/${appDisplayName}-${appVersion}-${pack_mode}-${buildTime}
#导出.ipa路径想
exportIpaPath=${exportFilePath}/${project_name}.ipa
#计时
SECONDS=0
#ask-commit() {
# #commit没有传值
# if [ -z "$LastBuildCommit"]; then
# read -p "commit-id:" input
# LAST_BUILD_COMMIT=$input
# fi
#}
rm -rf ${buildPath} || true
get_log(){
echo "*** 获取日志开始 ***"
if [ "$LastBuildCommitId" = "" ]; then
echo "编译版本: $development_name"
echo "程序分支: ${branch_name}"
echo "日志:"
echo "$PgyerUpdateDescription"
return
fi
#获取log
gitlog=''
result=$(echo $LastBuildCommitId | grep "day")
if [ "$result" != "" ]; then
currentDate="$(date +%Y-%m-%d) 0:0:0"
result=$(echo $LastBuildCommitId | grep "-")
if [ "$result" != "" ]; then
currentDate="$LastBuildCommitId 0:0:0"
fi
git_log_format="%s"
gitlog=$(git log --pretty=format:"$git_log_format" --date=short --since="$currentDate")
else
git_log_format="%s"
gitlog=$(git log --pretty="$git_log_format" $LastBuildCommitId..HEAD)
fi
echo "编译版本: $development_name"
# 过滤出以 [ 开头的行
gitlog=$(echo "$gitlog" | grep "^\[" )
# 如果没有符合条件的更新日志
if [ -z "${gitlog}" ]; then
return
fi
# gitlog=$(echo "$gitlog" | grep "^\[" )
# 翻转 LOG,因为 Git log 最开始是倒序的
gitlog=$(echo "$gitlog" | sed '1!G;h;$!d')
# 在末尾加一 br 标签,邮件中会换行显示
#gitlog=$(echo "$flip_changelog" | sed 's/$/&<br>/g')
# 添加行号
changelog_with_line_number=$(echo "$gitlog" | awk '{printf NR"."" "}1')
PgyerUpdateDescription=$changelog_with_line_number
echo "程序分支: ${branch_name}"
echo "日志:"
echo "$PgyerUpdateDescription"
}
package_ipa(){
/usr/libexec/Plistbuddy -c "Set CFBundleVersion $buildTime" "${plistPath}"
echo "*** 当前信息:app:${appDisplayName} verison:${appVersion} bulid:${buildTime} ***"
echo "*** 正在 清理工程 ***"
xcodebuild \
clean -configuration ${development_mode} -quiet || exit
echo "*** 清理完成 ***"
echo "*** 正在 编译工程 For "${development_mode}
xcodebuild \
archive -workspace ${project_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${development_mode} \
-archivePath build/${project_name}.xcarchive -quiet || exit
echo "*** 编译完成 ***"
}
export_ipa(){
echo "*** 正在 打包 ***"
xcodebuild -exportArchive -archivePath build/${project_name}.xcarchive \
-configuration ${development_mode} \
-exportPath ${exportFilePath} \
-exportOptionsPlist ${exportOptionsPlistPath} \
-allowProvisioningUpdates \
-quiet || exit
echo "*** 复制打包文件 ***"
cp -rf build/* ${exportFilePath}
}
del_build(){
# 删除build包
if [[ -d buildPath ]]; then
rm -rf buildPath -r
fi
}
upload_pgyer_ipa(){
#json格式化工具安装
#sudo chown -R username /usr/local
#brew install jq
# PgyerUpdateDescriptionAll=${development_name}${branch_name}${PgyerUpdateDescription}
PgyerUpdateDescriptionAll=$(echo "编译版本:${development_name} verison:${appVersion} bulid:${buildTime}" &&echo "程序分支:${branch_name}" &&echo "日志:" &&echo "${PgyerUpdateDescription}")
# echo "$PgyerUpdateDescriptionAll"
if [ -e $exportFilePath/$scheme_name.ipa ]; then
echo "*** .ipa文件已导出 ***"
#此处上传分发应用-开始
echo "*** 开始上传.ipa文件 *** ${exportIpaPath}"
if which jq 2>/dev/null; then
jqResult=$(curl -F "file=@${exportIpaPath}" -F "updateDescription=${PgyerUpdateDescriptionAll}" -F "uKey=${PgyerUKey}" -F "_api_key=${PgyerApiKey}" https://www.pgyer.com/apiv1/app/upload | jq ".")
cd ${sourcePath}
echo "${jqResult}" > pgyer_tmp_info.txt
cd ${exportFilePath}
echo "${jqResult}"
echo "${PgyerUpdateDescriptionAll}" > readme.txt
echo "${jqResult}" >> readme.txt
else
result=$(curl -F "file=@${exportIpaPath}" -F "updateDescription=${PgyerUpdateDescriptionAll}" -F "uKey=${PgyerUKey}" -F "_api_key=${PgyerApiKey}" https://www.pgyer.com/apiv1/app/upload)
cd ${exportFilePath}
echo "${result}"
echo "${PgyerUpdateDescriptionAll}" > readme.txt
echo "${result}" >> readme.txt
fi
echo "*** .ipa文件上传成功 ***"
#此处上传分发应用-结束
else
echo "*** 创建.ipa文件失败 ***"
fi
}
upload_appstore(){
echo "验证IPA中..."
altool -v -f ${exportIpaPath} -u ${AppleId} -p ${ApplePsw} -t ios --output-format xml
echo "开始上传苹果商店..."
altool --upload-app -f ${exportIpaPath} -t ios -u ${AppleId} -p ${ApplePsw}
}
if [ "$pack_mode" = "release" ]; then
get_log
package_ipa
export_ipa
upload_pgyer_ipa
del_build
elif [ "$pack_mode" = "publish" ]; then
get_log
package_ipa
#生成公测包
cd ${sourcePath}
exportOptionsPlistPath=./export-achot.plist
development_mode="Release"
pack_mode="release"
exportFilePath=~/Desktop/Ipa/${appDisplayName}-${appVersion}-${pack_mode}-${buildTime}
exportIpaPath=${exportFilePath}/${project_name}.ipa
export_ipa
upload_pgyer_ipa
#生成提审包
cd ${sourcePath}
exportOptionsPlistPath=./export-appstore.plist
development_mode="Release"
pack_mode="publish"
exportFilePath=~/Desktop/Ipa/${appDisplayName}-${appVersion}-${pack_mode}-${buildTime}
exportIpaPath=${exportFilePath}/${project_name}.ipa
export_ipa
upload_appstore
del_build
else
get_log
package_ipa
export_ipa
upload_pgyer_ipa
del_build
fi
echo "*** 打包完成 *** Total time: ${SECONDS}s"
app store
- 发苹果商店也是用上面的脚本但是需要配多一个环节.从xcode.app里链接一个上传工具,其他也没特别了
ln -s /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool /usr/local/bin/altool
mkdir -p /usr/local/itms/bin/
ln -s /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter /usr/local/itms/bin/iTMSTransporter
发邮件
-
这里是先用脚本打包好ipa,然后将蒲公英上传成功后返回的json写入一个本地文件,然后在用Python,转成功一个html的网页,当附件发送到邮箱
-
这个Python脚本,如下
#!/usr/bin/python2.6
# -*- coding: UTF-8 -*-
import os, sys
import shutil
import time
import codecs
import argparse
import shutil
import zipfile
import json
reload(sys)
def parse_args():
global script_path, proj_ios_path
parser = argparse.ArgumentParser(description='融媒打包项目.\n')
parser.add_argument('--json_file',
default="pgyer_tmp_info.txt",
dest='json_file',
help='蒲公英生成的json文件', type=str, required=False)
args = parser.parse_args()
return args
def main():
startTime = time.time()
app_args = parse_args()
json_file_name = app_args.json_file
workspacePath = os.getcwd() + os.sep
json_file_path = workspacePath + json_file_name
# 写入HTML界面中
appQRCodeURL = ""
appShortcutUrl = ""
appUpdateDescription = ""
appName = ""
appVersion = ""
appVersionNo = ""
with open(json_file_path) as f:
js = json.load(f, encoding="utf-8", strict=False)
appQRCodeURL = js["data"]["appQRCodeURL"].encode("utf8")
appShortcutUrl = "https://www.pgyer.com/" + js["data"]["appShortcutUrl"].encode("utf8")
appUpdateDescription = js["data"]["appUpdateDescription"].encode("utf8")
appName = js["data"]["appName"].encode("utf8")
appVersion = js["data"]["appVersion"].encode("utf8")
appVersionNo = js["data"]["appVersionNo"].encode("utf8")
message = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>蒲公英邮件</title>
</head>
<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0" >
<table width="95%%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif;margin:auto" >
<tr>
<td> <br/>
<b><font color="#0B610B">构建信息</font></b>
<hr size="2" width="100%%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>项目名称 :%s</li>
<li>版 本 号 :%s</li>
<li>构 建 号 :%s</li>
<li>链接地址 :<a href="%s">%s</a></li>
<li><img src="%s" alt=""/></li>
</ul>
</td>
</tr>
<tr>
<td><b><font color="#0B610B">构建日志:</font></b>
<hr size="2" width="100%%" align="center" /></td>
</tr>
<tr>
<td>
<textarea cols="80" rows="30" readonly="readonly" style="font-family: Courier New">%s</textarea>
</td>
</tr>
</table>
</body>
</html>
""" % (appName, appVersion, appVersionNo, appShortcutUrl, appShortcutUrl, appQRCodeURL, appUpdateDescription)
# 移除旧文件
if (os.path.exists(json_file_path)):
print("remove: " + json_file_path)
os.remove(json_file_path)
html_file_name = "pgyer_tmp_info.html"
html_path = workspacePath + html_file_name
if (os.path.exists(html_path)):
print("remove: " + html_path)
os.remove(html_path)
f = open(html_file_name, 'w')
f.write(message)
f.close()
endTime = time.time()
print("总耗时: %0.2f" % (endTime - startTime))
if __name__ == "__main__":
main()
ios企签构建
-
配置这个请先看企业证书重签名发布APP(支持APNS),搞清概念在做下一步.
-
红框就是必须用到的文件,会在桌面生成一个Ipa_enterprise文件夹,里面会有企签后的APP及,下载plist,这个可能需要手动调整一下plist,文件下的ipa
-
发布证书及其p12
-
签名脚本
-
第三方签名工具ota-tools
#!/usr/bin/python2.6
# -*- coding: UTF-8 -*-
import os, sys
import time
import codecs
import argparse
import shutil
import zipfile
import socket
reload(sys)
workspace_path = os.getcwd() + os.sep
workspace_desktop_path = os.path.expanduser("~/Desktop/Ipa_enterprise/")
class Error(Exception):
pass
class Task(object):
def __init__(self):
self.config = Config()
# ios 导出打包配置文件
def export_info_list(self, team_id, bundle_id):
pass
# 上传蒲公英
def run_pgyer_upload(self, ipa_path, pgyer_ukey, pgyer_apikey, pgyer_desc="描述"):
print("开始上传路径 :" + ipa_path)
cmd_workspace = "cd " + workspace_path
cmd_curl_text = "curl -F \"file=@%s\" -F \"updateDescription=%s\" -F \"uKey=%s\" -F \"_api_key=%s\" \"https://www.pgyer.com/apiv1/app/upload\" | jq \".\"" \
% (ipa_path, pgyer_desc, pgyer_ukey, pgyer_apikey)
cmd_curl = "echo $(%s) > pgyer_tmp_info.txt " % (cmd_curl_text)
cmd = cmd_workspace + " && " + cmd_curl
p = os.popen(cmd)
p.close()
# 企签证书
def run_sign(self, bin_floder_path, ipa_path, mobileprovision_path, certificate_name, ipa_http_url):
if len(ipa_http_url) == 0:
ipa_http_url = "http://" + socket.gethostbyname(socket.getfqdn(socket.gethostname())) + ":8000/"
cmd_workspace = "cd " + workspace_path
cmd_sign = bin_floder_path + "ipa_sign" + " " + ipa_path + " " + mobileprovision_path + " " + "\"" + certificate_name + "\""
cmd = cmd_workspace + " && " + cmd_sign
# APP重签处理
p = os.popen(cmd)
text = p.read()
p.close()
print(text)
lines = text.split("\n")
line1_words = lines[1].replace('\'', '').split(" ")
line2_words = lines[2].replace('\'', '').split(" ")
line5_words = lines[5].replace('\'', '').split(" ")
BundleDisplayName = line1_words[3]
ShortVersionString = line1_words[6]
BundleId = line2_words[4]
BundleVersion = line2_words[7]
export_file_name = (BundleId + "_" + ShortVersionString + "_" + BundleVersion).replace('.', '_') + ".ipa"
export_info_name = export_file_name.replace('.ipa', '.plist')
# export_folder_name = BundleDisplayName + "-" + ShortVersionString + "-" + "resigned" + "-" + BundleVersion
export_folder_name = export_file_name.replace('.ipa', '')
# export_tmp_ipa = line5_words[4]
export_resigned_path = line5_words[4] # bin_floder_path + export_file_name
export_info_path = workspace_path + export_info_name
# shutil.move(export_tmp_ipa, export_resigned_path)
export_desktop_folder_path = workspace_desktop_path + export_folder_name + os.sep
export_desktop_ipa_path = export_desktop_folder_path + export_file_name
export_desktop_info_path = export_desktop_folder_path + export_info_name
if os.path.exists(export_desktop_folder_path):
shutil.rmtree(export_desktop_folder_path)
os.mkdir(export_desktop_folder_path)
else:
os.mkdir(export_desktop_folder_path)
# 复制ipa文件
shutil.copyfile(export_resigned_path, export_desktop_ipa_path)
# 生成下载info.plist
ipa_http_serve_floder_url = ipa_http_url + export_folder_name + "/"
cmd_ota = bin_floder_path + "ipa_ota" + " " + export_desktop_ipa_path + " " + ipa_http_serve_floder_url
cmd = cmd_workspace + " && " + cmd_ota
# APP重签处理
p = os.popen(cmd)
text = p.read()
p.close()
print(text)
# 复制info.plist文件
shutil.copyfile(export_info_path, export_desktop_info_path)
os.rename(export_info_path, export_resigned_path.replace(".ipa", ".plist"))
# http_cmd = "itms-services://?action=download-manifest&url=http://aaa.list.ppp/com_touchtv_midou_1.0.1_201904281818.plist"
# http_cmd = "itms-services://?action=download-manifest&url=%s%s" % (ipa_http_serve_floder_url, export_info_name)
pgyer_desc = BundleDisplayName + "(企签版)" + "\n" + "版本:" + ShortVersionString + "\n" + "构建号:" + BundleVersion
print("==========================================\n")
print("ipa 生成路径 :" + export_desktop_ipa_path)
print(".plist 生成路径 :" + export_desktop_info_path)
print("安装连接 :" + ipa_http_serve_floder_url)
return export_resigned_path, export_desktop_ipa_path, export_info_path, export_desktop_info_path, pgyer_desc
# 生成邮件附件
def create_mail_file(self, app_args):
pass
def parse_args():
parser = argparse.ArgumentParser(description='企签项目.\n')
parser.add_argument('--soucre_ipa_name',
default="t_source.ipa",
dest='soucre_ipa_name',
help='需要签名的ipa', type=str, required=False)
parser.add_argument('--pgyer_ukey',
default="f46...83b",
dest='pgyer_ukey',
help='pgyer_ukey', type=str, required=False)
parser.add_argument('--pgyer_apikey',
default="b98...58f",
dest='pgyer_apikey',
help='pgyer_apikey', type=str, required=False)
parser.add_argument('--mobileprovision_path',
default="c_certificate/touchtvInhouse.mobileprovision",
dest='mobileprovision_path',
help='mobileprovision_path', type=str, required=False)
parser.add_argument('--certificate_name',
# default="iPhone Distribution: Huge Enterprises Inc.",
default="Beijing Gehua CATV Network Co., Ltd.",
dest='certificate_name',
help='certificate_name', type=str, required=False)
parser.add_argument('--ipa_http_url',
default="",
dest='ipa_http_url',
help='ipa_http_url ex(https://aaa.list.ppp/)', type=str, required=False)
args = parser.parse_args()
return args
def main():
task = Task()
startTime = time.time()
# 参数
app_args = parse_args()
soucre_ipa_name = app_args.soucre_ipa_name
pgyer_ukey = app_args.pgyer_ukey
pgyer_apikey = app_args.pgyer_apikey
mobileprovision_path = app_args.mobileprovision_path
certificate_name = app_args.certificate_name
ipa_http_url = app_args.ipa_http_url
# 校验路径
if os.path.exists(workspace_desktop_path) == False:
os.mkdir(workspace_desktop_path)
# 路径
workspace_ota_path = workspace_path + "s_ota-tools" + os.sep
# 签名
source_ipa_path = workspace_path + soucre_ipa_name
export_resigned_path, export_desktop_ipa_path, export_info_path, export_desktop_info_path, pgyer_desc = \
task.run_sign(
workspace_ota_path,
source_ipa_path,
workspace_path + mobileprovision_path,
certificate_name,
ipa_http_url)
# 上传
task.run_pgyer_upload(export_desktop_ipa_path, pgyer_ukey, pgyer_apikey, pgyer_desc)
# 移除文件
# if os.path.exists(export_resigned_path):
# os.remove(export_resigned_path)
# if os.path.exists(export_info_path):
# os.remove(export_info_path)
# if os.path.exists(source_ipa_path):
# os.remove(source_ipa_path)
endTime = time.time()
print("总耗时: %0.2f" % (endTime - startTime))
if __name__ == "__main__":
main()
总结
- 这篇涉及jenkin的job配置,其中涉及打包的一些配置,另外还涉及企签一些知识,另外,hexo_blog也是部署在另外一台jenkins上的,使用码云的git-hook,发送post自动打包,上传到七牛.