1. 前言
Gitlab CI中可以配置代码质量检查,同样的,GitHub Actions中也可以配置代码质量检查。
本文中,我们学习在GitHub Actions中配置Python的编码规范检查和运行单元测试。
参考文档:
2. Python编码规范检查
详情参考 《git pre-commit 代码质量检查》。
3. Python单元测试
3.1. Python通用单元测试
使用多个Python版本运行单元测试。
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
| name: Python application test
on: pull_request: branches: - '**' push: branches: - '**'
jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11']
steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e. - name: Test with pytest run: | pip install pytest # export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" OPENAI_API_MODEL="gpt-3.5-turbo-1106" pytest tests/ --doctest-modules --junitxml=junit/test-results-${{ matrix.python-version }}.xml
|
3.2. 单测并上传单测结果
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
| name: Python application test
on: pull_request: branches: - '**' push: branches: - '**'
jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11']
steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e. - name: Test with pytest run: | pip install pytest pytest tests/ --doctest-modules --junitxml=junit/test-results-${{ matrix.python-version }}.xml - name: Upload pytest test results uses: actions/upload-artifact@v3 with: name: pytest-results-${{ matrix.python-version }} path: junit/test-results-${{ matrix.python-version }}.xml retention-days: 3 if: ${{ always() }}
|
4. Python单元测试覆盖率
4.1. 生成单测覆盖率并展示
1、安装单测依赖
1
| pip install pytest pytest-cov coverage-badge
|
2、生成单测覆盖率结果文件 .coverage
和 html 格式报告
1
| pytest tests/ --doctest-modules --cov=. --cov-report=html
|
pytest参数说明:
tests/
指定测试文件的路径--doctest-modules
运行所有发现的doctest模块--junitxml=junit/test-results.xml
指定生成junitxml格式的测试报告--cov=.
生成单测覆盖率结果文件 .coverage
必须的参数,指定要收集哪些代码的覆盖率信息,.
表示收集当前目录下所有代码的覆盖率信息--cov-report=html
指定生成 html 格式的测试覆盖率报告,存到 htmlcov 目录中,等同于单独运行 coverage html -i
--cov-report=xml
指定生成 xml 格式的测试覆盖率报告,存储为 coverage.xml ,等同于单独运行 coverage xml -i
3、查看测试覆盖率
4、生成badge svg
1
| coverage-badge -o coverage.svg
|
5、github README 引用 badge
1
| ![Coverage Status](/path/to/coverage.svg)
|
或者:
1
| [![Coverage Status](/path/to/coverage.svg)](https://your-badge-link)
|
4.2. 单测覆盖率整合到github actions
1、github actions配置
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
| name: Python application test
on: pull_request: branches: - '**' push: branches: - '**'
jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11']
steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e. - name: Test with pytest run: | pip install pytest pytest-cov pytest-html coverage-badge # export OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" OPENAI_API_MODEL="gpt-3.5-turbo-1106" pytest tests/ --doctest-modules --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=. --cov-report=xml:cov.xml --cov-report=html:htmlcov coverage report -m coverage-badge -o coverage.svg # upload coverage.svg to a object storage - name: Upload pytest test results uses: actions/upload-artifact@v3 with: name: pytest-results-${{ matrix.python-version }} path: | ./junit/test-results-${{ matrix.python-version }}.xml ./htmlcov/ retention-days: 3 if: ${{ always() }}
|
2、github README 引用 badge
1
| ![Coverage Status](https://object-storage-domain/path/to/coverage.svg)
|
4.3. 忽略指定目录
项目更目录中,创建 coverage 配置文件 .coveragerc
1 2 3 4 5 6 7 8 9 10 11
| [run] omit = */tests/* */migrations/* exclude_lines = # 忽略异常抛出代码 raise NotImplementedError # 忽略抽象基类中的函数定义 @abstractmethod # 忽略带有pragma: no cover注释的行 pragma: no cover
|
omit 用于指定不应该包含在覆盖率统计中的文件或目录,exclude_lines 用于指定不应该包含在覆盖率统计中的代码行。
4.4. 使用Codecov
1 2 3 4 5 6 7 8 9
| jobs: build: steps: - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./cov.xml if: ${{ always() }}
|
5. PR单测审批
用户提交PR后,想要触发审批,配置是很简单的,例如:
1 2 3 4
| on: pull_request: branches: - '**'
|
但是,如果我们在workflow中用到了secret,那配置就没有这么简单了。
因为github限制了pull_request
触发的workflow读取secret的权限,所以副本仓库提交到主仓库的pr,读取不到主仓库secret。
这是一种对主仓库secret的保护,详情参考文档github actions main repository secret not picked up from pull request build
如果确实需要让pr的workflow读取到secret,该怎么处理?Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests 这篇文章提供了两种方法可供参考。
方法一:pr事件触发workflow1,workflow1结束事件触发workflow2,此时workflow2就有权限读取secret了。但是,这种方法中,workflow1和workflow2之间缺少直观的关联,在pr页面和github actions页面上都看不出来联系,不友好。
方法二:使用 pull_request_target
代替 pull_request
,此时触发的workflow有权限读取secret。配置简单,页面也能直观看到,但是,这种方法降低了安全性。
方法一和方法二各有优劣,个人更推荐方法二,因为方法二可以配合environment
加一道审批,增强安全性,弥补缺陷。
此外,使用 pull_request_target
时, actions/checkout@v4
默认切分支是切到 target 分支,想要测试pr分支,需要指定切分支到pr head。详情参考Checkout V4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| on: pull_request_target:
jobs: build: runs-on: ubuntu-latest environment: unittest strategy: matrix: python-version: ['3.9']
steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }}
|
pull_request
和 pull_request_target
是有很大区别的,Events that trigger workflows - pull_request_target 文档中说:
This event runs in the context of the base of the pull request, rather than in the context of the merge commit, as the pull_request event does.
个人理解为:pull_request
的workflow是根据pr合并后的workflow yaml运行,而pull_request_target
的workflow是根据pr合并前的workflow yaml运行的,详情参考文档:What is the difference between pull_request and pull_request_target event in GitHubActions
注意:如果一个pr中修改了 workflow ,那么在github actions页面看到的 workflow yaml 就是修改后的。但是如果是pull_request_target
事件触发的workflow,那么虽然看到了修改后的yaml,实际执行会按照修改前的yaml执行。