[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# IPython Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# dotenv\n.env\n\n# virtualenv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\n# Mac OS\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Leo Lee\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "## Overview\n\n**Jenkins job overview:**\n\n![](images/Jenkins_Job_Overview.jpg)\n\n**Jenkins job build page view:**\n\n![](images/Jenkins_Job_Build_View.jpg)\n\n\n## 开箱即用\n\n**1，添加构建脚本；**\n\n- 在构建脚本中配置`PROVISIONING_PROFILE`和`pgyer/fir.im`账号；\n- 将`build_scripts`文件夹及其文件拷贝至目标构建代码库的根目录下；\n- 将`build_scripts`提交到项目的仓库中。\n\n除了与Jenkins实现持续集成，构建脚本还可单独使用，使用方式如下：\n\n```bash\n$ python ${WORKSPACE}/build_scripts/build.py \\\n    --scheme ${SCHEME} \\\n    --workspace ${WORKSPACE}/Store.xcworkspace \\\n    --sdk ${SDK}\n    --configuration ${CONFIGURATION} \\\n    --output_folder ${WORKSPACE}/${OUTPUT_FOLDER}\n```\n\n需要特别说明的是，若要构建生成可在移动设备中运行的`.ipa`文件，则要将`${SDK}`设置为`iphoneos`；若要构建生成可在模拟器中运行的`.app`文件，则要将`${SDK}`设置为`iphonesimulator`。\n\n**2、运行jenkins，安装必备插件；**\n\n```bash\n$ nohup java -jar jenkins_located_path/jenkins.war &\n```\n\n**3、创建Jenkins Job；**\n\n- 在Jenkins中创建一个`Freestyle project`类型的Job，先不进行任何配置；\n- 然后将`config.xml`文件拷贝到`~/.jenkins/jobs/YourProject/`中覆盖原有配置文件，重启Jenkins；\n- 完成配置文件替换和重启后，刚创建好的Job就已完成了大部分配置；\n- 在`Job Configure`中根据项目实际情况调整配置，其中`Git Repositories`是必须修改的，其它配置项可选择性地进行调整。\n\n**4、done！**\n\n## Read More ...\n\n- [《使用Jenkins搭建iOS/Android持续集成打包平台》](http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins)\n- [《关于持续集成打包平台的Jenkins配置和构建脚本实现细节》](http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details)\n- 微信公众号：[DebugTalk](http://debugtalk.com/images/wechat_qrcode.png)\n"
  },
  {
    "path": "build_scripts/build.py",
    "content": "#coding=utf-8\nfrom __future__ import print_function\nimport os\nimport sys\nimport argparse\nfrom ios_builder import iOSBuilder\nfrom pgyer_uploader import uploadIpaToPgyer\nfrom pgyer_uploader import saveQRCodeImage\n\ndef parse_args():\n    parser = argparse.ArgumentParser(description='iOS app build script.')\n\n    parser.add_argument('--build_method', dest=\"build_method\", default='xcodebuild',\n                        help=\"Specify build method, xctool or xcodebuild.\")\n    parser.add_argument('--workspace', dest=\"workspace\", default=None,\n                        help=\"Build the workspace name.xcworkspace\")\n    parser.add_argument(\"--scheme\", dest=\"scheme\", default=None,\n                        help=\"Build the scheme specified by schemename. \\\n                        Required if building a workspace\")\n    parser.add_argument(\"--project\", dest=\"project\", default=None,\n                        help=\"Build the project name.xcodeproj\")\n    parser.add_argument(\"--target\", dest=\"target\", default=None,\n                        help=\"Build the target specified by targetname. \\\n                        Required if building a project\")\n    parser.add_argument(\"--sdk\", dest=\"sdk\", default='iphoneos',\n                        help=\"Specify build SDK, iphoneos or iphonesimulator, \\\n                        default is iphonesimulator\")\n    parser.add_argument(\"--build_version\", dest=\"build_version\", default='1.0.0.1',\n                        help=\"Specify build version number\")\n    parser.add_argument(\"--provisioning_profile\", dest=\"provisioning_profile\", default=None,\n                        help=\"specify provisioning profile\")\n    parser.add_argument(\"--plist_path\", dest=\"plist_path\", default=None,\n                        help=\"Specify build plist path\")\n    parser.add_argument(\"--configuration\", dest=\"configuration\", default='Release',\n                        help=\"Specify build configuration, Release or Debug, \\\n                        default value is Release\")\n    parser.add_argument(\"--output_folder\", dest=\"output_folder\", default='BuildProducts',\n                        help=\"specify output_folder folder name\")\n    parser.add_argument(\"--update_description\", dest=\"update_description\",\n                        help=\"specify update description\")\n\n    args = parser.parse_args()\n    print(\"args: {}\".format(args))\n    return args\n\ndef main():\n    args = parse_args()\n\n    if args.plist_path is None:\n        plist_file_name = '%s-Info.plist' % args.scheme\n        args.plist_path = os.path.abspath(\n            os.path.join(os.path.dirname(__file__),\n                         os.path.pardir,\n                         plist_file_name\n                        )\n        )\n\n    ios_builder = iOSBuilder(args)\n\n    if args.sdk.startswith(\"iphonesimulator\"):\n        app_zip_path = ios_builder.build_app()\n        print(\"app_zip_path: {}\".format(app_zip_path))\n        sys.exit(0)\n\n    ipa_path = ios_builder.build_ipa()\n    app_download_page_url = uploadIpaToPgyer(ipa_path, args.update_description)\n    try:\n        output_folder = os.path.dirname(ipa_path)\n        saveQRCodeImage(app_download_page_url, output_folder)\n    except Exception as e:\n        print(\"Exception occured: {}\".format(str(e)))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "build_scripts/ios_builder.py",
    "content": "#coding=utf-8\nfrom __future__ import print_function\nimport os\nimport subprocess\nimport shutil\nimport plistlib\n\nclass iOSBuilder(object):\n    \"\"\"docstring for iOSBuilder\"\"\"\n    def __init__(self, options):\n        self._build_method = options.build_method\n        self._sdk = options.sdk\n        self._configuration = options.configuration\n        self._provisioning_profile = options.provisioning_profile\n        self._output_folder = options.output_folder\n        self._plist_path = options.plist_path\n        self._build_version = options.build_version\n        self._archive_path = None\n        self._build_params = self._get_build_params(\n            options.project, options.target, options.workspace, options.scheme)\n        self._prepare()\n\n    def _prepare(self):\n        \"\"\" get prepared for building.\n        \"\"\"\n        self._change_build_version()\n\n        print(\"Output folder for ipa ============== {}\".format(self._output_folder))\n        try:\n            shutil.rmtree(self._output_folder)\n        except OSError:\n            pass\n        finally:\n            os.makedirs(self._output_folder)\n\n        self._udpate_pod_dependencies()\n        self._build_clean()\n\n    def _udpate_pod_dependencies(self):\n        podfile = os.path.join(os.getcwd(), 'Podfile')\n        podfile_lock = os.path.join(os.getcwd(), 'Podfile.lock')\n        if os.path.isfile(podfile) or os.path.isfile(podfile_lock):\n            print(\"Update pod dependencies =============\")\n            cmd_shell = 'pod repo update'\n            self._run_shell(cmd_shell)\n            print(\"Install pod dependencies =============\")\n            cmd_shell = 'pod install'\n            self._run_shell(cmd_shell)\n\n    def _change_build_version(self):\n        \"\"\" set CFBundleVersion and CFBundleShortVersionString.\n        \"\"\"\n        build_version_list = self._build_version.split('.')\n        cf_bundle_short_version_string = '.'.join(build_version_list[:3])\n        with open(self._plist_path, 'rb') as fp:\n            plist_content = plistlib.load(fp)\n            plist_content['CFBundleShortVersionString'] = cf_bundle_short_version_string\n            plist_content['CFBundleVersion'] = self._build_version\n        with open(self._plist_path, 'wb') as fp:\n            plistlib.dump(plist_content, fp)\n\n    def _run_shell(self, cmd_shell):\n        process = subprocess.Popen(cmd_shell, shell=True)\n        process.wait()\n        return_code = process.returncode\n        assert return_code == 0\n\n    def _get_build_params(self, project, target, workspace, scheme):\n        if project is None and workspace is None:\n            raise \"project and workspace should not both be None.\"\n        elif project is not None:\n            build_params = '-project %s -scheme %s' % (project, scheme)\n            # specify package name\n            self._package_name = \"{0}_{1}\".format(scheme, self._configuration)\n            self._app_name = scheme\n        elif workspace is not None:\n            build_params = '-workspace %s -scheme %s' % (workspace, scheme)\n            # specify package name\n            self._package_name = \"{0}_{1}\".format(scheme, self._configuration)\n            self._app_name = scheme\n\n        build_params += ' -sdk %s -configuration %s' % (self._sdk, self._configuration)\n        return build_params\n\n    def _build_clean(self):\n        cmd_shell = '{0} {1} clean'.format(self._build_method, self._build_params)\n        print(\"build clean ============= {}\".format(cmd_shell))\n        self._run_shell(cmd_shell)\n\n    def _build_archive(self):\n        \"\"\" specify output xcarchive location\n        \"\"\"\n        self._archive_path = os.path.join(\n            self._output_folder, \"{}.xcarchive\".format(self._package_name))\n        cmd_shell = '{0} {1} archive -archivePath {2}'.format(\n            self._build_method, self._build_params, self._archive_path)\n        print(\"build archive ============= {}\".format(cmd_shell))\n        self._run_shell(cmd_shell)\n\n    def _export_ipa(self):\n        \"\"\" export archive to ipa file, return ipa location\n        \"\"\"\n        if self._provisioning_profile is None:\n            raise \"provisioning profile should not be None!\"\n        ipa_path = os.path.join(self._output_folder, \"{}.ipa\".format(self._package_name))\n        cmd_shell = 'xcodebuild -exportArchive -archivePath {}'.format(self._archive_path)\n        cmd_shell += ' -exportPath {}'.format(ipa_path)\n        cmd_shell += ' -exportFormat ipa'\n        cmd_shell += ' -exportProvisioningProfile \"{}\"'.format(self._provisioning_profile)\n        print(\"build archive ============= {}\".format(cmd_shell))\n        self._run_shell(cmd_shell)\n        return ipa_path\n\n    def build_ipa(self):\n        \"\"\" build ipa file for iOS device\n        \"\"\"\n        self._build_archive()\n        ipa_path = self._export_ipa()\n        return ipa_path\n\n    def _build_archive_for_simulator(self):\n        cmd_shell = '{0} {1} -derivedDataPath {2}'.format(\n            self._build_method, self._build_params, self._output_folder)\n        print(\"build archive for simulator ============= {}\".format(cmd_shell))\n        self._run_shell(cmd_shell)\n\n    def _archive_app_to_zip(self):\n        app_path = os.path.join(\n            self._output_folder,\n            \"Build\",\n            \"Products\",\n            \"{0}-iphonesimulator\".format(self._configuration),\n            \"{0}.app\".format(self._app_name)\n        )\n        app_zip_filename = os.path.basename(app_path) + \".zip\"\n        app_zip_path = os.path.join(self._output_folder, app_zip_filename)\n        cmd_shell = \"zip -r {0} {1}\".format(app_zip_path, app_path)\n        self._run_shell(cmd_shell)\n        print(\"app_zip_path: %s\" % app_zip_path)\n        return app_zip_path\n\n    def build_app(self):\n        \"\"\" build app file for iOS simulator\n        \"\"\"\n        self._build_archive_for_simulator()\n        app_zip_path = self._archive_app_to_zip()\n        return app_zip_path\n"
  },
  {
    "path": "build_scripts/pgyer_uploader.py",
    "content": "#coding=utf-8\nfrom __future__ import print_function\nimport os\nimport requests\nimport time\nimport re\nfrom datetime import datetime\n\n# configuration for pgyer\nUSER_KEY = \"9667e5933d************540b83ed7c\"\nAPI_KEY = \"d2e517468e7************e24310b65\"\nPGYER_UPLOAD_URL = \"https://www.pgyer.com/apiv1/app/upload\"\n\n\ndef parseUploadResult(jsonResult):\n    print('post response: %s' % jsonResult)\n    resultCode = jsonResult['code']\n\n    if resultCode != 0:\n        print(\"Upload Fail!\")\n        raise Exception(\"Reason: %s\" % jsonResult['message'])\n\n    print(\"Upload Success\")\n    appKey = jsonResult['data']['appKey']\n    app_download_page_url = \"https://www.pgyer.com/%s\" % appKey\n    print(\"appDownloadPage: %s\" % app_download_page_url)\n    return app_download_page_url\n\ndef uploadIpaToPgyer(ipaPath, updateDescription):\n    print(\"Begin to upload ipa to Pgyer: %s\" % ipaPath)\n    headers = {'enctype': 'multipart/form-data'}\n    payload = {\n        'uKey': USER_KEY,\n        '_api_key': API_KEY,\n        'publishRange': '2', # 直接发布\n        'isPublishToPublic': '2', # 不发布到广场\n        'updateDescription': updateDescription  # 版本更新描述\n    }\n\n    try_times = 0\n    while try_times < 5:\n        try:\n            print(\"uploading ... %s\" % datetime.now())\n            ipa_file = {'file': open(ipaPath, 'rb')}\n            resp = requests.post(PGYER_UPLOAD_URL, headers=headers, files=ipa_file, data=payload)\n            assert resp.status_code == requests.codes.ok\n            result = resp.json()\n            app_download_page_url = parseUploadResult(result)\n            return app_download_page_url\n        except requests.exceptions.ConnectionError:\n            print(\"requests.exceptions.ConnectionError occured!\")\n            time.sleep(60)\n            print(\"try again ... %s\" % datetime.now())\n            try_times += 1\n        except Exception as e:\n            print(\"Exception occured: %s\" % str(e))\n            time.sleep(60)\n            print(\"try again ... %s\" % datetime.now())\n            try_times += 1\n\n        if try_times >= 5:\n            raise Exception(\"Failed to upload ipa to Pgyer, retried 5 times.\")\n\ndef parseQRCodeImageUrl(app_download_page_url):\n    try_times = 0\n    while try_times < 3:\n        try:\n            response = requests.get(app_download_page_url)\n            regex = '<img src=\\\"(.*?)\\\" style='\n            m = re.search(regex, response.content)\n            assert m is not None\n            appQRCodeURL = m.group(1)\n            print(\"appQRCodeURL: %s\" % appQRCodeURL)\n            return appQRCodeURL\n        except AssertionError:\n            try_times += 1\n            time.sleep(60)\n            print(\"Can not locate QRCode image. retry ... %s: %s\" % (try_times, datetime.now()))\n\n        if try_times >= 3:\n            raise Exception(\"Failed to locate QRCode image in download page, retried 3 times.\")\n\ndef saveQRCodeImage(app_download_page_url, output_folder):\n    appQRCodeURL = parseQRCodeImageUrl(app_download_page_url)\n    response = requests.get(appQRCodeURL)\n    qr_image_file_path = os.path.join(output_folder, 'QRCode.png')\n    if response.status_code == 200:\n        with open(qr_image_file_path, 'wb') as f:\n            f.write(response.content)\n    print('Save QRCode image to file: %s' % qr_image_file_path)\n"
  },
  {
    "path": "build_scripts/requirements.txt",
    "content": "requests==2.12.2"
  },
  {
    "path": "jobs/job_template/config.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<project>\n  <actions/>\n  <description></description>\n  <keepDependencies>false</keepDependencies>\n  <properties>\n    <hudson.model.ParametersDefinitionProperty>\n      <parameterDefinitions>\n        <hudson.model.StringParameterDefinition>\n          <name>SCHEME</name>\n          <description>scheme configuration of this project</description>\n          <defaultValue>StoreCI</defaultValue>\n        </hudson.model.StringParameterDefinition>\n        <hudson.model.StringParameterDefinition>\n          <name>CONFIGURATION</name>\n          <description>configuration of packing, Release/Debug</description>\n          <defaultValue>Release</defaultValue>\n        </hudson.model.StringParameterDefinition>\n        <hudson.model.StringParameterDefinition>\n          <name>OUTPUT_FOLDER</name>\n          <description>output folder for build artifacts, it is located in workspace/project root dir.</description>\n          <defaultValue>build_outputs</defaultValue>\n        </hudson.model.StringParameterDefinition>\n        <hudson.model.StringParameterDefinition>\n          <name>BRANCH</name>\n          <description>git repository branch</description>\n          <defaultValue>NPED_2.6</defaultValue>\n        </hudson.model.StringParameterDefinition>\n      </parameterDefinitions>\n    </hudson.model.ParametersDefinitionProperty>\n  </properties>\n  <scm class=\"hudson.plugins.git.GitSCM\" plugin=\"git@2.4.4\">\n    <configVersion>2</configVersion>\n    <userRemoteConfigs>\n      <hudson.plugins.git.UserRemoteConfig>\n        <url>https://github.com/debugtalk/XXXX</url>\n        <credentialsId>483f9c64-3560-4977-a0a7-71509f718fd4</credentialsId>\n      </hudson.plugins.git.UserRemoteConfig>\n    </userRemoteConfigs>\n    <branches>\n      <hudson.plugins.git.BranchSpec>\n        <name>refs/heads/${BRANCH}</name>\n      </hudson.plugins.git.BranchSpec>\n    </branches>\n    <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>\n    <submoduleCfg class=\"list\"/>\n    <extensions>\n      <hudson.plugins.git.extensions.impl.CloneOption>\n        <shallow>false</shallow>\n        <noTags>false</noTags>\n        <reference></reference>\n        <timeout>120</timeout>\n        <depth>0</depth>\n      </hudson.plugins.git.extensions.impl.CloneOption>\n    </extensions>\n  </scm>\n  <canRoam>true</canRoam>\n  <disabled>false</disabled>\n  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>\n  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>\n  <triggers>\n    <hudson.triggers.SCMTrigger>\n      <spec>H H/3 * * *</spec>\n      <ignorePostCommitHooks>false</ignorePostCommitHooks>\n    </hudson.triggers.SCMTrigger>\n  </triggers>\n  <concurrentBuild>false</concurrentBuild>\n  <builders>\n    <hudson.tasks.Shell>\n      <command>python ${WORKSPACE}/Build_scripts/build.py \\\n\t--scheme ${SCHEME} \\\n    --workspace ${WORKSPACE}/Store.xcworkspace \\\n    --configuration ${CONFIGURATION} \\\n    --output ${WORKSPACE}/${OUTPUT_FOLDER}</command>\n    </hudson.tasks.Shell>\n    <hudson.plugins.descriptionsetter.DescriptionSetterBuilder plugin=\"description-setter@1.10\">\n      <regexp>appDownloadPage: (.*)$</regexp>\n      <description>&lt;img src=&apos;${BUILD_URL}artifact/build_outputs/QRCode.png&apos;&gt;\\n&lt;a href=&apos;\\1&apos;&gt;&#x8;Install Online&lt;/a&gt;</description>\n    </hudson.plugins.descriptionsetter.DescriptionSetterBuilder>\n  </builders>\n  <publishers>\n    <hudson.tasks.ArtifactArchiver>\n      <artifacts>${OUTPUT_FOLDER}/*.ipa,${OUTPUT_FOLDER}/QRCode.png,${OUTPUT_FOLDER}/*.xcarchive/Info.plist</artifacts>\n      <allowEmptyArchive>true</allowEmptyArchive>\n      <onlyIfSuccessful>true</onlyIfSuccessful>\n      <fingerprint>false</fingerprint>\n      <defaultExcludes>true</defaultExcludes>\n      <caseSensitive>true</caseSensitive>\n    </hudson.tasks.ArtifactArchiver>\n  </publishers>\n  <buildWrappers>\n    <org.jenkinsci.plugins.buildnamesetter.BuildNameSetter plugin=\"build-name-setter@1.6.5\">\n      <template>${SCHEME}_${CONFIGURATION}_#${BUILD_NUMBER}</template>\n      <runAtStart>true</runAtStart>\n      <runAtEnd>true</runAtEnd>\n    </org.jenkinsci.plugins.buildnamesetter.BuildNameSetter>\n  </buildWrappers>\n</project>\n"
  }
]