Gitlab CI 使用笔记 Gitlab CI是如何帮助我们的项目的 Python开发的项目,除了享受Python简介的语法和丰富的第三方库之外,会遇到几个痛点
动态类型带来的问题
第三方库,甚至包括Python本身的迭代速度快,不兼容更新是家常便饭,由此导致库的依赖较难管理
解决第一个痛点,常见的作法是
使用3.7首次引入的typing模块,手动给代码加上类型,这其实跟TypeScript很像,而TypeScript的成功证明typing未来一定是Python不可或缺的一部分,类型标记带来的便利肯定会让Python在大型项目中的实用性 得到很大的提升
单元测试覆盖,让bug在测试这个环节尽可能的显现出来
解决第二个痛点,有常见的这些方案
众所周知,Python库的相互依赖已经到了一个可以说是一团糟的地步,pip在早先版本中,几乎没有处理库的依赖关系的功能,这是不建议你在复杂的环境中使用pip install的原因,如果你的项目里引用了20个库,其中包括了Panda或者Numpy这样庞大的库,那你的下一个pip install XXX是有很大可能break掉你的代码的。pip在 20.X版本后加入了新的Dependency resolve模块,可能会让这样的烦恼少一些。但我们现在就可以使用Pipenv代替
pip,管理我们的项目依赖,一个Pipfile的作用,参见JavaScript里的packages.json。
Conda也是一个较成熟的Python环境管理工具,Anaconda Python分发版已经是很多人的Python首选了,使用conda install会让Anaconda的官方repository来帮你解析依赖关系
使用持续集成CI完成编译-测试-发布流程 我司有一个假设在内网里的私有代码仓库,使用的是Gitlab,用Nginx做反向代理后,可以在内网里通过
https://gitlab.xuanlingasset.com访问。
Gitlab中的每个项目都有一个CI/CD区域,这个区域的功能是定义一些你想要经常运行的编译/测试/发布…的(无聊)工作,让
Gitlab来帮你执行,比如你经常有这样一个需求:
经过一天的代码修改,你提交了一个commit,把commit push到Gitlab服务器上,这时你想要运行所有的单元测试,确保你新更改的
代码没有破坏原有的功能,或者引入了一些bug。如果单元测试通过的话,把项目编译打包后发布。
这时一个.gitlab-ci.yml文件就可以帮到你了。Gitlab CI,简单来说就是在Gitlab服务器之外,你可以建立一个gitlab-runner
服务器,这个服务器负责代理一个容器管理工具,比如Docker/Virtual Box等,我们选用的是Docker。commit和tag这样的时间可以
触发了CI工作流,假设你push了一个commit到Gitlab服务器上,runner就会探测到需要运行相关的工作流,它建立一个新的Docker容器,
从Gitlab上拉取新的代码,然后运行预先定义的scripts,如果没有错误产生,工作流就成功完成了。
定义Gitlab CI流程 假设我们的项目结构是这样的:
1 2 3 4 5 6 - foo |- src |- tests |- .gitlab-ci.yml |- Pipfile |- setup.py
.gitlab-ci.yml的一个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 image: local/python:3.8.7 variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt" cache: paths: - .cache/pip - venv/ stages: - build - upload - release build_job: stage: build rules: - if: '$CI_COMMIT_BRANCH == "master"' - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install virtualenv - virtualenv venv - source venv/bin/activate - git config fetch.prune true - git config fetch.pruneTags true - git fetch --tags -f - git tag - python setup.py bdist_wheel - pip install dist/* artifacts: paths: - dist/*.whl upload_job: stage: upload image: local/python:3.8.7 rules: - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install requests - pip install packaging - pip install simplejson - python -m py_ci upload release_job: stage: release rules: - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install requests - pip install packaging - pip install simplejson - python -m py_ci release
下面是详细解释
1 2 3 4 stages: - build - upload - release
定义了工作流中的顺序,三个阶段build -> upload -> release
第一个job:初始化环境,安装需要的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 build_job: stage: build rules: - if: '$CI_COMMIT_BRANCH == "master"' - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install virtualenv - virtualenv venv - source venv/bin/activate - git config fetch.prune true - git config fetch.pruneTags true - git fetch --tags -f - git tag - python setup.py bdist_wheel - pip install dist/* artifacts: paths: - dist/*.whl
如果你有一些单元测试,在这一步可以加入一个test阶段
1 2 3 4 5 6 7 8 9 10 11 12 stages: - build - test - upload - release test_job: stage: test script: - python setup.py test - pytest test/
test全部成功的话,test阶段会成功返回,开始下一个upload工作,把打包好的项目wheel文件上传到该Gitlab仓库的Package registry里
1 2 3 4 5 6 7 8 9 10 upload_job: stage: upload image: local/python:3.8.7 rules: - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install requests - pip install packaging - pip install simplejson - python -m py_ci upload
我们可以在CI工作中调用Gitlab的API,这也是最简单使用的方法,你可以用任何语言做HTTP请求,我们自己写了一个Python帮助脚本来执行upload,也就是这一步python -m py_ci upload。
py_ci的内容很简答,py_ci/upload.py的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import os import requests from .helper import PACKAGE_URL, WHEEL_FILE_NAME def upload(): print('Post target: {}'.format(PACKAGE_URL)) dist_path = 'dist/{}'.format(WHEEL_FILE_NAME) files = {'upload_file': open(dist_path, 'rb')} print('Upload file: {}'.format(dist_path)) headers = { "JOB-TOKEN": os.environ['CI_JOB_TOKEN'] # authenciate with Gitlab API } res = requests.put(PACKAGE_URL, headers=headers, files=files ) print(res.content.decode()) if res.status_code == 201: print('Package upload to package registry (generic)!') else: raise RuntimeError(res.status_code)
简单来说就是把dist/下的whl文件put到相应的url上,就完成了package的上传。
Gitalb的API访问需要权限,权限验证有很多种方法,这里的JOB-TOKEN是CI容器中自动定义的环境变量,可以用来验证权限;如果你需要在
其他地方调用API,可以在项目的页面生成一个Personal token
最后一步,建立一个Release
1 2 3 4 5 6 7 8 9 release_job: stage: release rules: - if: '$CI_COMMIT_TAG =~ /release\/(.*)/' script: - pip install requests - pip install packaging - pip install simplejson - python -m py_ci release
py_ci/release.py的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import os import requests import simplejson from .helper import PACKAGE_URL, TAG_NAME, WHEEL_FILE_NAME, CI_API_V4_URL, CI_PROJECT_ID, FULL_VERSION def release(): post_target = CI_API_V4_URL + f"/projects/{CI_PROJECT_ID}/releases" headers = { 'JOB-TOKEN': os.environ['CI_JOB_TOKEN'], 'Content-Type': 'application/json' } payload = simplejson.dumps(release_data()) res = requests.post(post_target, data=payload, headers=headers ) print('Response code: {}'.format(res.status_code)) print(res.content.decode()) if res.status_code == 201: print('Release succesful! Receipt:') print(res.content) else: raise ValueError(res.status_code) def release_data(): data = { "name": f"Release {FULL_VERSION}", "tag_name": TAG_NAME, "description": f"Version {FULL_VERSION}", # "milestones": "" # if specified must exist "assets": { "links": [ { "name": WHEEL_FILE_NAME, "url": PACKAGE_URL } ] } } return data
我们按照Gitlab API的要求,在release_data函数中提供了必要的release信息,最重要的是assets"字段:assets`字段是选填的,如果你不提供的话,产生的release只会包含一份repo内源码的打包,那我们怎么让release中也包含上一步
upload里上传的whl文件呢?只需要提供一个URL连接到package registry里的对应whl文件的地址就可以
私有CA证书 在部署Runner的时候,遇到了一个比较棘手的问题:我们的Gitlab实例只能在内网里可见,因此无法取得一个公开的SSL证书,我们只得
使用一张自签发证书来加密Gitlab服务器,但是CI里的Docker容器环境里没有这张私有证书,会导致无法和Gitlab服务器通讯。
Gitlab官方给出了解决方法 ,简单来说就是指定一个
tls-ca-file配置给Docker容器,然后Docker容器会从该文件读取CA证书,遗憾的是这个方法经测试不能奏效。
我们在python:3.8.7-alpine官方镜像上,叠加了一层,把自签名的CA证书打包到镜像中,因此你可以看到.gitlab-ci.yml中
1 image: local/python:3.8.7
用的是这张本地镜像,local/python:3.8.7和普通的python镜像在功能上没有任何区别
如果你的Gitlab服务器用的是经过认证的公有证书,那你完全可以使用其他任何的Docker镜像,local/python:3.8.7只是为了解决
SSL认证的无奈之举