一个计算机技术爱好者与学习者

0%

GitHub Actions配置代码质量检查

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
# continue-on-error: true

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
# Use always() to always run this step to publish test results when there are test failures
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、查看测试覆盖率

1
coverage report -m

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加一道审批,增强安全性,弥补缺陷。

1
2
3
4
5
6
7
8
on:
pull_request_target:

jobs:
build:
runs-on: ubuntu-latest
environment: unittest
...

pull_requestpull_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执行。